mirror of
https://github.com/bitwarden/server
synced 2025-12-10 05:13:48 +00:00
Update Swashbuckle and improve generated OpenAPI files (#6066)
* Improve generated OpenAPI files * Nullable * Fmt * Correct powershell command * Fix name * Add some tests * Fmt * Switch to using json naming policy
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"swashbuckle.aspnetcore.cli": {
|
"swashbuckle.aspnetcore.cli": {
|
||||||
"version": "7.3.2",
|
"version": "9.0.2",
|
||||||
"commands": ["swagger"]
|
"commands": ["swagger"]
|
||||||
},
|
},
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
|
|||||||
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -376,62 +376,23 @@ jobs:
|
|||||||
path: docker-stub-EU.zip
|
path: docker-stub-EU.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Build Public API Swagger
|
- name: Build Swagger files
|
||||||
run: |
|
run: |
|
||||||
cd ./src/Api
|
cd ./dev
|
||||||
echo "Restore tools"
|
pwsh ./generate_openapi_files.ps1
|
||||||
dotnet tool restore
|
|
||||||
echo "Publish"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
dotnet swagger tofile --output ../../swagger.json --host https://api.bitwarden.com \
|
|
||||||
./obj/build-output/publish/Api.dll public
|
|
||||||
cd ../..
|
|
||||||
env:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
|
||||||
swaggerGen: "True"
|
|
||||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
|
||||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
|
||||||
|
|
||||||
- name: Upload Public API Swagger artifact
|
- name: Upload Public API Swagger artifact
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: swagger.json
|
name: swagger.json
|
||||||
path: swagger.json
|
path: api.public.json
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Build Internal API Swagger
|
|
||||||
run: |
|
|
||||||
cd ./src/Api
|
|
||||||
echo "Restore API tools"
|
|
||||||
dotnet tool restore
|
|
||||||
echo "Publish API"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \
|
|
||||||
./obj/build-output/publish/Api.dll internal
|
|
||||||
|
|
||||||
cd ../Identity
|
|
||||||
|
|
||||||
echo "Restore Identity tools"
|
|
||||||
dotnet tool restore
|
|
||||||
echo "Publish Identity"
|
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \
|
|
||||||
./obj/build-output/publish/Identity.dll v1
|
|
||||||
cd ../..
|
|
||||||
env:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Development
|
|
||||||
swaggerGen: "True"
|
|
||||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
|
||||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
|
||||||
|
|
||||||
- name: Upload Internal API Swagger artifact
|
- name: Upload Internal API Swagger artifact
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: internal.json
|
name: internal.json
|
||||||
path: internal.json
|
path: api.json
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Identity Swagger artifact
|
- name: Upload Identity Swagger artifact
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -129,7 +129,7 @@ publish/
|
|||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
*.azurePubxml
|
*.azurePubxml
|
||||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
*.pubxml
|
*.pubxml
|
||||||
*.publishproj
|
*.publishproj
|
||||||
@@ -226,3 +226,8 @@ src/Notifications/Notifications.zip
|
|||||||
bitwarden_license/src/Portal/Portal.zip
|
bitwarden_license/src/Portal/Portal.zip
|
||||||
bitwarden_license/src/Sso/Sso.zip
|
bitwarden_license/src/Sso/Sso.zip
|
||||||
**/src/**/flags.json
|
**/src/**/flags.json
|
||||||
|
|
||||||
|
# Generated swagger specs
|
||||||
|
/identity.json
|
||||||
|
/api.json
|
||||||
|
/api.public.json
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -337,6 +339,10 @@ Global
|
|||||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -391,6 +397,7 @@ Global
|
|||||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
|||||||
19
dev/generate_openapi_files.ps1
Normal file
19
dev/generate_openapi_files.ps1
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Set-Location "$PSScriptRoot/.."
|
||||||
|
|
||||||
|
$env:ASPNETCORE_ENVIRONMENT = "Development"
|
||||||
|
$env:swaggerGen = "True"
|
||||||
|
$env:DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX = "2"
|
||||||
|
$env:GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING = "placeholder"
|
||||||
|
|
||||||
|
dotnet tool restore
|
||||||
|
|
||||||
|
# Identity
|
||||||
|
Set-Location "./src/Identity"
|
||||||
|
dotnet build
|
||||||
|
dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1"
|
||||||
|
|
||||||
|
# Api internal & public
|
||||||
|
Set-Location "../../src/Api"
|
||||||
|
dotnet build
|
||||||
|
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||||
|
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ public class Startup
|
|||||||
config.Conventions.Add(new PublicApiControllersModelConvention());
|
config.Conventions.Add(new PublicApiControllersModelConvention());
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSwagger(globalSettings);
|
services.AddSwagger(globalSettings, Environment);
|
||||||
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
|
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
|
||||||
services.AddHostedService<Jobs.JobsHostedService>();
|
services.AddHostedService<Jobs.JobsHostedService>();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Bit.Api.Utilities;
|
|||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings)
|
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
services.AddSwaggerGen(config =>
|
services.AddSwaggerGen(config =>
|
||||||
{
|
{
|
||||||
@@ -83,6 +83,14 @@ public static class ServiceCollectionExtensions
|
|||||||
// config.UseReferencedDefinitionsForEnums();
|
// config.UseReferencedDefinitionsForEnums();
|
||||||
|
|
||||||
config.SchemaFilter<EnumSchemaFilter>();
|
config.SchemaFilter<EnumSchemaFilter>();
|
||||||
|
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||||
|
|
||||||
|
// These two filters require debug symbols/git, so only add them in development mode
|
||||||
|
if (environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||||
|
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
||||||
config.IncludeXmlComments(apiFilePath, true);
|
config.IncludeXmlComments(apiFilePath, true);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -64,10 +64,19 @@ public class Startup
|
|||||||
config.Filters.Add(new ModelStateValidationFilterAttribute());
|
config.Filters.Add(new ModelStateValidationFilterAttribute());
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSwaggerGen(c =>
|
services.AddSwaggerGen(config =>
|
||||||
{
|
{
|
||||||
c.SchemaFilter<EnumSchemaFilter>();
|
config.SchemaFilter<EnumSchemaFilter>();
|
||||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" });
|
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||||
|
|
||||||
|
// These two filters require debug symbols/git, so only add them in development mode
|
||||||
|
if (Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||||
|
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!globalSettings.SelfHosted)
|
if (!globalSettings.SelfHosted)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
40
src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs
Normal file
40
src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace Bit.SharedWeb.Swagger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the format of any strings that are decorated with the <see cref="EncryptedStringAttribute"/> to "x-enc-string".
|
||||||
|
/// This will allow the generated bindings to use a more appropriate type for encrypted strings.
|
||||||
|
/// </summary>
|
||||||
|
public class EncryptedStringSchemaFilter : ISchemaFilter
|
||||||
|
{
|
||||||
|
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||||
|
{
|
||||||
|
if (context.Type == null || schema.Properties == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var prop in context.Type.GetProperties())
|
||||||
|
{
|
||||||
|
// Only apply to string properties
|
||||||
|
if (prop.PropertyType != typeof(string))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if the property has the EncryptedString attribute
|
||||||
|
if (prop.GetCustomAttributes(typeof(EncryptedStringAttribute), true).FirstOrDefault() != null)
|
||||||
|
{
|
||||||
|
// Convert prop.Name to camelCase for JSON schema property lookup
|
||||||
|
var jsonPropName = JsonNamingPolicy.CamelCase.ConvertName(prop.Name);
|
||||||
|
|
||||||
|
if (schema.Properties.TryGetValue(jsonPropName, out var value))
|
||||||
|
{
|
||||||
|
value.Format = "x-enc-string";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/SharedWeb/Swagger/GitCommitDocumentFilter.cs
Normal file
50
src/SharedWeb/Swagger/GitCommitDocumentFilter.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace Bit.SharedWeb.Swagger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the Git commit that was used to generate the Swagger document, to help with debugging and reproducibility.
|
||||||
|
/// </summary>
|
||||||
|
public class GitCommitDocumentFilter : IDocumentFilter
|
||||||
|
{
|
||||||
|
|
||||||
|
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(GitCommit))
|
||||||
|
{
|
||||||
|
swaggerDoc.Extensions.Add("x-git-commit", new Microsoft.OpenApi.Any.OpenApiString(GitCommit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GitCommit => _gitCommit.Value;
|
||||||
|
|
||||||
|
private static readonly Lazy<string?> _gitCommit = new(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "git",
|
||||||
|
Arguments = "rev-parse HEAD",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
var result = process.StandardOutput.ReadLine()?.Trim();
|
||||||
|
process.WaitForExit();
|
||||||
|
return result ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
87
src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs
Normal file
87
src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
|
using System.Reflection.Metadata.Ecma335;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace Bit.SharedWeb.Swagger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds source file and line number information to the Swagger operation description.
|
||||||
|
/// This can be useful for locating the source code of the operation in the repository,
|
||||||
|
/// as the generated names are based on the HTTP path, and are hard to search for.
|
||||||
|
/// </summary>
|
||||||
|
public class SourceFileLineOperationFilter : IOperationFilter
|
||||||
|
{
|
||||||
|
private static readonly string _gitCommit = GitCommitDocumentFilter.GitCommit ?? "main";
|
||||||
|
|
||||||
|
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||||
|
{
|
||||||
|
|
||||||
|
var (fileName, lineNumber) = GetSourceFileLine(context.MethodInfo);
|
||||||
|
if (fileName != null && lineNumber > 0)
|
||||||
|
{
|
||||||
|
// Add the information with a link to the source file at the end of the operation description
|
||||||
|
operation.Description +=
|
||||||
|
$"\nThis operation is defined on: [`https://github.com/bitwarden/server/blob/{_gitCommit}/{fileName}#L{lineNumber}`]";
|
||||||
|
|
||||||
|
// Also add the information as extensions, so other tools can use it in the future
|
||||||
|
operation.Extensions.Add("x-source-file", new OpenApiString(fileName));
|
||||||
|
operation.Extensions.Add("x-source-line", new OpenApiInteger(lineNumber));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string? fileName, int lineNumber) GetSourceFileLine(MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
// Get the location of the PDB file associated with the module of the method
|
||||||
|
var pdbPath = Path.ChangeExtension(methodInfo.Module.FullyQualifiedName, ".pdb");
|
||||||
|
if (!File.Exists(pdbPath)) return (null, 0);
|
||||||
|
|
||||||
|
// Open the PDB file and read the metadata
|
||||||
|
using var pdbStream = File.OpenRead(pdbPath);
|
||||||
|
using var metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
|
||||||
|
var metadataReader = metadataReaderProvider.GetMetadataReader();
|
||||||
|
|
||||||
|
// If the method is async, the compiler will generate a state machine,
|
||||||
|
// so we can't look for the original method, but we instead need to look
|
||||||
|
// for the MoveNext method of the state machine.
|
||||||
|
var attr = methodInfo.GetCustomAttribute<AsyncStateMachineAttribute>();
|
||||||
|
if (attr?.StateMachineType != null)
|
||||||
|
{
|
||||||
|
var moveNext = attr.StateMachineType.GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
if (moveNext != null) methodInfo = moveNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we have the method, we can get its sequence points
|
||||||
|
var handle = (MethodDefinitionHandle)MetadataTokens.Handle(methodInfo.MetadataToken);
|
||||||
|
if (handle.IsNil) return (null, 0);
|
||||||
|
var sequencePoints = metadataReader.GetMethodDebugInformation(handle).GetSequencePoints();
|
||||||
|
|
||||||
|
// Iterate through the sequence points and pick the first one that has a valid line number
|
||||||
|
foreach (var sp in sequencePoints)
|
||||||
|
{
|
||||||
|
var docName = metadataReader.GetDocument(sp.Document).Name;
|
||||||
|
if (sp.StartLine != 0 && sp.StartLine != SequencePoint.HiddenLine && !docName.IsNil)
|
||||||
|
{
|
||||||
|
var fileName = metadataReader.GetString(docName);
|
||||||
|
var repoRoot = FindRepoRoot(AppContext.BaseDirectory);
|
||||||
|
var relativeFileName = repoRoot != null ? Path.GetRelativePath(repoRoot, fileName) : fileName;
|
||||||
|
return (relativeFileName, sp.StartLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindRepoRoot(string startPath)
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(startPath);
|
||||||
|
while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, ".git")))
|
||||||
|
dir = dir.Parent;
|
||||||
|
return dir?.FullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
test/SharedWeb.Test/EncryptedStringSchemaFilterTest.cs
Normal file
60
test/SharedWeb.Test/EncryptedStringSchemaFilterTest.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.SharedWeb.Swagger;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
|
||||||
|
namespace SharedWeb.Test;
|
||||||
|
|
||||||
|
public class EncryptedStringSchemaFilterTest
|
||||||
|
{
|
||||||
|
private class TestClass
|
||||||
|
{
|
||||||
|
[EncryptedString]
|
||||||
|
public string SecretKey { get; set; }
|
||||||
|
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
[EncryptedString]
|
||||||
|
public int Wrong { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnnotatedStringSetsFormat()
|
||||||
|
{
|
||||||
|
var schema = new OpenApiSchema
|
||||||
|
{
|
||||||
|
Properties = new Dictionary<string, OpenApiSchema> { { "secretKey", new() } }
|
||||||
|
};
|
||||||
|
var context = new SchemaFilterContext(typeof(TestClass), null, null, null);
|
||||||
|
var filter = new EncryptedStringSchemaFilter();
|
||||||
|
filter.Apply(schema, context);
|
||||||
|
Assert.Equal("x-enc-string", schema.Properties["secretKey"].Format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonAnnotatedStringIsIgnored()
|
||||||
|
{
|
||||||
|
var schema = new OpenApiSchema
|
||||||
|
{
|
||||||
|
Properties = new Dictionary<string, OpenApiSchema> { { "username", new() } }
|
||||||
|
};
|
||||||
|
var context = new SchemaFilterContext(typeof(TestClass), null, null, null);
|
||||||
|
var filter = new EncryptedStringSchemaFilter();
|
||||||
|
filter.Apply(schema, context);
|
||||||
|
Assert.Null(schema.Properties["username"].Format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnnotatedWrongTypeIsIgnored()
|
||||||
|
{
|
||||||
|
var schema = new OpenApiSchema
|
||||||
|
{
|
||||||
|
Properties = new Dictionary<string, OpenApiSchema> { { "wrong", new() } }
|
||||||
|
};
|
||||||
|
var context = new SchemaFilterContext(typeof(TestClass), null, null, null);
|
||||||
|
var filter = new EncryptedStringSchemaFilter();
|
||||||
|
filter.Apply(schema, context);
|
||||||
|
Assert.Null(schema.Properties["wrong"].Format);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
test/SharedWeb.Test/EnumSchemaFilterTest.cs
Normal file
41
test/SharedWeb.Test/EnumSchemaFilterTest.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using Bit.SharedWeb.Swagger;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace SharedWeb.Test;
|
||||||
|
|
||||||
|
public class EnumSchemaFilterTest
|
||||||
|
{
|
||||||
|
private enum TestEnum
|
||||||
|
{
|
||||||
|
First,
|
||||||
|
Second,
|
||||||
|
Third
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetsEnumVarNamesExtension()
|
||||||
|
{
|
||||||
|
var schema = new OpenApiSchema();
|
||||||
|
var context = new SchemaFilterContext(typeof(TestEnum), null, null, null);
|
||||||
|
var filter = new EnumSchemaFilter();
|
||||||
|
filter.Apply(schema, context);
|
||||||
|
|
||||||
|
Assert.True(schema.Extensions.ContainsKey("x-enum-varnames"));
|
||||||
|
var extensions = schema.Extensions["x-enum-varnames"] as OpenApiArray;
|
||||||
|
Assert.NotNull(extensions);
|
||||||
|
Assert.Equal(["First", "Second", "Third"], extensions.Select(x => ((OpenApiString)x).Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoesNotSetExtensionForNonEnum()
|
||||||
|
{
|
||||||
|
var schema = new OpenApiSchema();
|
||||||
|
var context = new SchemaFilterContext(typeof(string), null, null, null);
|
||||||
|
var filter = new EnumSchemaFilter();
|
||||||
|
filter.Apply(schema, context);
|
||||||
|
|
||||||
|
Assert.False(schema.Extensions.ContainsKey("x-enum-varnames"));
|
||||||
|
}
|
||||||
|
}
|
||||||
23
test/SharedWeb.Test/GitCommitDocumentFilterTest.cs
Normal file
23
test/SharedWeb.Test/GitCommitDocumentFilterTest.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Bit.SharedWeb.Swagger;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace SharedWeb.Test;
|
||||||
|
|
||||||
|
public class GitCommitDocumentFilterTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AddsGitCommitExtensionIfAvailable()
|
||||||
|
{
|
||||||
|
var doc = new OpenApiDocument();
|
||||||
|
var context = new DocumentFilterContext(null, null, null);
|
||||||
|
var filter = new GitCommitDocumentFilter();
|
||||||
|
filter.Apply(doc, context);
|
||||||
|
|
||||||
|
Assert.True(doc.Extensions.ContainsKey("x-git-commit"));
|
||||||
|
var ext = doc.Extensions["x-git-commit"] as Microsoft.OpenApi.Any.OpenApiString;
|
||||||
|
Assert.NotNull(ext);
|
||||||
|
Assert.False(string.IsNullOrEmpty(ext.Value));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test/SharedWeb.Test/GlobalUsings.cs
Normal file
1
test/SharedWeb.Test/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
22
test/SharedWeb.Test/SharedWeb.Test.csproj
Normal file
22
test/SharedWeb.Test/SharedWeb.Test.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<RootNamespace>SharedWeb.Test</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio"
|
||||||
|
Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharedWeb\SharedWeb.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
33
test/SharedWeb.Test/SourceFileLineOperationFilterTest.cs
Normal file
33
test/SharedWeb.Test/SourceFileLineOperationFilterTest.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Bit.SharedWeb.Swagger;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace SharedWeb.Test;
|
||||||
|
|
||||||
|
public class SourceFileLineOperationFilterTest
|
||||||
|
{
|
||||||
|
private class DummyController
|
||||||
|
{
|
||||||
|
public void DummyMethod() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddsSourceFileAndLineExtensionsIfAvailable()
|
||||||
|
{
|
||||||
|
var methodInfo = typeof(DummyController).GetMethod(nameof(DummyController.DummyMethod));
|
||||||
|
var operation = new OpenApiOperation();
|
||||||
|
var context = new OperationFilterContext(null, null, null, methodInfo);
|
||||||
|
var filter = new SourceFileLineOperationFilter();
|
||||||
|
filter.Apply(operation, context);
|
||||||
|
|
||||||
|
Assert.True(operation.Extensions.ContainsKey("x-source-file"));
|
||||||
|
Assert.True(operation.Extensions.ContainsKey("x-source-line"));
|
||||||
|
var fileExt = operation.Extensions["x-source-file"] as Microsoft.OpenApi.Any.OpenApiString;
|
||||||
|
var lineExt = operation.Extensions["x-source-line"] as Microsoft.OpenApi.Any.OpenApiInteger;
|
||||||
|
Assert.NotNull(fileExt);
|
||||||
|
Assert.NotNull(lineExt);
|
||||||
|
|
||||||
|
Assert.Equal(11, lineExt.Value);
|
||||||
|
Assert.StartsWith("test/SharedWeb.Test/", fileExt.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user