1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 12:43:14 +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:
Daniel García
2025-08-18 18:40:50 +02:00
committed by GitHub
parent 03327cb082
commit 6971f0a976
20 changed files with 420 additions and 54 deletions

View 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";
}
}
}
}
}

View 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;
}
});
}

View 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;
}
}