mirror of
https://github.com/bitwarden/server
synced 2025-12-22 19:23:45 +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:
@@ -34,7 +34,7 @@
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -210,7 +210,7 @@ public class Startup
|
||||
config.Conventions.Add(new PublicApiControllersModelConvention());
|
||||
});
|
||||
|
||||
services.AddSwagger(globalSettings);
|
||||
services.AddSwagger(globalSettings, Environment);
|
||||
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Bit.Api.Utilities;
|
||||
|
||||
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 =>
|
||||
{
|
||||
@@ -83,6 +83,14 @@ public static class ServiceCollectionExtensions
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
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");
|
||||
config.IncludeXmlComments(apiFilePath, true);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -64,10 +64,19 @@ public class Startup
|
||||
config.Filters.Add(new ModelStateValidationFilterAttribute());
|
||||
});
|
||||
|
||||
services.AddSwaggerGen(c =>
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
c.SchemaFilter<EnumSchemaFilter>();
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" });
|
||||
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>();
|
||||
}
|
||||
|
||||
config.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" });
|
||||
});
|
||||
|
||||
if (!globalSettings.SelfHosted)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user