From 94dcaf2ea24e428f2957674ac46b864625782194 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Feb 2026 22:39:49 +0100 Subject: [PATCH] Upgrade Swashbuckle to v10 --- Directory.Packages.props | 4 +- .../ApiApplicationBuilderExtensions.cs | 2 +- .../ApiServiceCollectionExtensions.cs | 21 ++-- .../Filters/AdditionalModelFilter.cs | 101 ++++++++---------- .../Filters/CachingOpenApiProvider.cs | 4 +- Jellyfin.Server/Filters/FileRequestFilter.cs | 5 +- Jellyfin.Server/Filters/FileResponseFilter.cs | 11 +- .../Filters/FlagsEnumSchemaFilter.cs | 23 ++-- .../Filters/IgnoreEnumSchemaFilter.cs | 17 +-- .../Filters/ParameterObsoleteFilter.cs | 12 ++- .../RetryOnTemporarilyUnavailableFilter.cs | 10 +- .../SecurityRequirementsOperationFilter.cs | 15 +-- .../SecuritySchemeReferenceFixupFilter.cs | 56 ++++++++++ 13 files changed, 168 insertions(+), 113 deletions(-) create mode 100644 Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index a520b87e2b..cee12a48ae 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,8 +75,8 @@ - - + + diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 9fd853cf2e..2aadedfa61 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -3,7 +3,7 @@ using Jellyfin.Api.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; namespace Jellyfin.Server.Extensions { diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 9df24fa0d7..c71c193e2e 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Security.Claims; +using System.Text.Json.Nodes; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; @@ -26,7 +26,6 @@ using Jellyfin.Server.Filters; using MediaBrowser.Common.Api; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -34,9 +33,7 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Interfaces; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; @@ -208,7 +205,7 @@ namespace Jellyfin.Server.Extensions { { "x-jellyfin-version", - new OpenApiString(version) + new JsonNodeExtension(JsonValue.Create(version)) } } }); @@ -262,6 +259,7 @@ namespace Jellyfin.Server.Extensions c.OperationFilter(); c.OperationFilter(); c.DocumentFilter(); + c.DocumentFilter(); }) .Replace(ServiceDescriptor.Transient()); } @@ -333,10 +331,10 @@ namespace Jellyfin.Server.Extensions options.MapType>(() => new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, AdditionalProperties = new OpenApiSchema { - Type = "string" + Type = JsonSchemaType.String } }); @@ -344,18 +342,17 @@ namespace Jellyfin.Server.Extensions options.MapType>(() => new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, AdditionalProperties = new OpenApiSchema { - Type = "string", - Nullable = true + Type = JsonSchemaType.String | JsonSchemaType.Null } }); // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. options.MapType(() => new OpenApiSchema { - Type = "string" + Type = JsonSchemaType.String }); } } diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 7407bd2eb7..efa2f4cca5 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -3,18 +3,17 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; +using System.Text.Json.Nodes; using Jellyfin.Extensions; using Jellyfin.Server.Migrations; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages; -using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.ApiClient; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -25,7 +24,7 @@ namespace Jellyfin.Server.Filters public class AdditionalModelFilter : IDocumentFilter { // Array of options that should not be visible in the api spec. - private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions) }; + private static readonly Type[] _ignoredConfigurations = [typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions)]; private readonly IServerConfigurationManager _serverConfigurationManager; /// @@ -48,8 +47,8 @@ namespace Jellyfin.Server.Filters && t != typeof(WebSocketMessageInfo)) .ToList(); - var inboundWebSocketSchemas = new List(); - var inboundWebSocketDiscriminators = new Dictionary(); + var inboundWebSocketSchemas = new List(); + var inboundWebSocketDiscriminators = new Dictionary(); foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t))) { var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute()?.Value; @@ -60,18 +59,16 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); inboundWebSocketSchemas.Add(schema); - inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3; + if (schema is OpenApiSchemaReference schemaRef) + { + inboundWebSocketDiscriminators[messageType.ToString()!] = schemaRef; + } } var inboundWebSocketMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible inbound websocket types", - Reference = new OpenApiReference - { - Id = nameof(InboundWebSocketMessage), - Type = ReferenceType.Schema - }, OneOf = inboundWebSocketSchemas, Discriminator = new OpenApiDiscriminator { @@ -82,8 +79,8 @@ namespace Jellyfin.Server.Filters context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema); - var outboundWebSocketSchemas = new List(); - var outboundWebSocketDiscriminators = new Dictionary(); + var outboundWebSocketSchemas = new List(); + var outboundWebSocketDiscriminators = new Dictionary(); foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t))) { var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute()?.Value; @@ -94,58 +91,55 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); outboundWebSocketSchemas.Add(schema); - outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3); + if (schema is OpenApiSchemaReference schemaRef) + { + outboundWebSocketDiscriminators.Add(messageType.ToString()!, schemaRef); + } } // Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us var syncPlayGroupUpdateMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Untyped sync play command.", - Properties = new Dictionary + Properties = new Dictionary { { "Data", new OpenApiSchema { - AllOf = - [ - new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate) } } - ], + AllOf = new List + { + new OpenApiSchemaReference(nameof(GroupUpdate), null, null) + }, Description = "Group update data", - Nullable = false, } }, - { "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } }, + { "MessageId", new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid", Description = "Gets or sets the message id." } }, { "MessageType", new OpenApiSchema { - Enum = Enum.GetValues().Select(type => new OpenApiString(type.ToString())).ToList(), - AllOf = - [ - new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } } - ], + Enum = Enum.GetValues().Select(type => (JsonNode)JsonValue.Create(type.ToString())!).ToList(), + AllOf = new List + { + new OpenApiSchemaReference(nameof(SessionMessageType), null, null) + }, Description = "The different kinds of messages that are used in the WebSocket api.", - Default = new OpenApiString(nameof(SessionMessageType.SyncPlayGroupUpdate)), + Default = JsonValue.Create(nameof(SessionMessageType.SyncPlayGroupUpdate)), ReadOnly = true } }, }, AdditionalPropertiesAllowed = false, - Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "SyncPlayGroupUpdateMessage" } }; context.SchemaRepository.AddDefinition("SyncPlayGroupUpdateMessage", syncPlayGroupUpdateMessageSchema); - outboundWebSocketSchemas.Add(syncPlayGroupUpdateMessageSchema); - outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayGroupUpdateMessageSchema.Reference.ReferenceV3; + var syncPlayRef = new OpenApiSchemaReference("SyncPlayGroupUpdateMessage", null, null); + outboundWebSocketSchemas.Add(syncPlayRef); + outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayRef; var outboundWebSocketMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible outbound websocket types", - Reference = new OpenApiReference - { - Id = nameof(OutboundWebSocketMessage), - Type = ReferenceType.Schema - }, OneOf = outboundWebSocketSchemas, Discriminator = new OpenApiDiscriminator { @@ -159,17 +153,12 @@ namespace Jellyfin.Server.Filters nameof(WebSocketMessage), new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the possible websocket types", - Reference = new OpenApiReference + OneOf = new List { - Id = nameof(WebSocketMessage), - Type = ReferenceType.Schema - }, - OneOf = new[] - { - inboundWebSocketMessageSchema, - outboundWebSocketMessageSchema + new OpenApiSchemaReference(nameof(InboundWebSocketMessage), null, null), + new OpenApiSchemaReference(nameof(OutboundWebSocketMessage), null, null) } }); @@ -180,8 +169,8 @@ namespace Jellyfin.Server.Filters && t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>)) .ToList(); - var groupUpdateSchemas = new List(); - var groupUpdateDiscriminators = new Dictionary(); + var groupUpdateSchemas = new List(); + var groupUpdateDiscriminators = new Dictionary(); foreach (var type in groupUpdateTypes) { var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate.Type))?.GetCustomAttribute()?.Value; @@ -192,18 +181,16 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); groupUpdateSchemas.Add(schema); - groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3; + if (schema is OpenApiSchemaReference schemaRef) + { + groupUpdateDiscriminators[groupUpdateType.ToString()!] = schemaRef; + } } var groupUpdateSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible group update types", - Reference = new OpenApiReference - { - Id = nameof(GroupUpdate), - Type = ReferenceType.Schema - }, OneOf = groupUpdateSchemas, Discriminator = new OpenApiDiscriminator { diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index 833b684444..fdc49a9840 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -48,7 +48,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider } /// - public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null) + public OpenApiDocument GetSwagger(string documentName, string host, string basePath) { if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null) { diff --git a/Jellyfin.Server/Filters/FileRequestFilter.cs b/Jellyfin.Server/Filters/FileRequestFilter.cs index 86dbf7657e..3d5b1fdf1f 100644 --- a/Jellyfin.Server/Filters/FileRequestFilter.cs +++ b/Jellyfin.Server/Filters/FileRequestFilter.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -28,10 +28,11 @@ namespace Jellyfin.Server.Filters { Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "binary" } }; + body.Content ??= new System.Collections.Generic.Dictionary(); foreach (var contentType in contentTypes) { body.Content.Add(contentType, mediaType); diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs index cd0acadf32..64aea62519 100644 --- a/Jellyfin.Server/Filters/FileResponseFilter.cs +++ b/Jellyfin.Server/Filters/FileResponseFilter.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Filters { Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "binary" } }; @@ -22,6 +22,11 @@ namespace Jellyfin.Server.Filters /// public void Apply(OpenApiOperation operation, OperationFilterContext context) { + if (operation.Responses is null) + { + return; + } + foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata) { if (attribute is ProducesFileAttribute producesFileAttribute) @@ -31,7 +36,7 @@ namespace Jellyfin.Server.Filters .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal)); // Operation doesn't have a response. - if (response.Value is null) + if (response.Value?.Content is null) { continue; } diff --git a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs index 3e0b69d017..0c1f4197ce 100644 --- a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs +++ b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs @@ -1,5 +1,5 @@ using System; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters; public class FlagsEnumSchemaFilter : ISchemaFilter { /// - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) { var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type); if (type is null || !type.IsEnum) @@ -29,11 +29,16 @@ public class FlagsEnumSchemaFilter : ISchemaFilter return; } + if (schema is not OpenApiSchema concreteSchema) + { + return; + } + if (context.MemberInfo is null) { // Processing the enum definition itself - ensure it's type "string" not "integer" - schema.Type = "string"; - schema.Format = null; + concreteSchema.Type = JsonSchemaType.String; + concreteSchema.Format = null; } else { @@ -43,11 +48,11 @@ public class FlagsEnumSchemaFilter : ISchemaFilter // Flags enums should be represented as arrays referencing the enum schema // since multiple values can be combined - schema.Type = "array"; - schema.Format = null; - schema.Enum = null; - schema.AllOf = null; - schema.Items = enumSchema; + concreteSchema.Type = JsonSchemaType.Array; + concreteSchema.Format = null; + concreteSchema.Enum = null; + concreteSchema.AllOf = null; + concreteSchema.Items = enumSchema; } } } diff --git a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs index eb9ad03c21..3dcf29d9c2 100644 --- a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs +++ b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json.Nodes; using Jellyfin.Data.Attributes; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters; public class IgnoreEnumSchemaFilter : ISchemaFilter { /// - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) { if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false)) { @@ -25,18 +25,23 @@ public class IgnoreEnumSchemaFilter : ISchemaFilter return; } - var enumOpenApiStrings = new List(); + if (schema is not OpenApiSchema concreteSchema) + { + return; + } + + var enumOpenApiNodes = new List(); foreach (var enumName in Enum.GetNames(type)) { var member = type.GetMember(enumName)[0]; if (!member.GetCustomAttributes().Any()) { - enumOpenApiStrings.Add(new OpenApiString(enumName)); + enumOpenApiNodes.Add(JsonValue.Create(enumName)!); } } - schema.Enum = enumOpenApiStrings; + concreteSchema.Enum = enumOpenApiNodes; } } } diff --git a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs index 98a8dc0f18..90bca884b1 100644 --- a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs +++ b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -21,11 +21,17 @@ namespace Jellyfin.Server.Filters .OfType() .Any()) { + if (operation.Parameters is null) + { + continue; + } + foreach (var parameter in operation.Parameters) { - if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal)) + if (parameter is OpenApiParameter concreteParam + && string.Equals(concreteParam.Name, parameterDescription.Name, StringComparison.Ordinal)) { - parameter.Deprecated = true; + concreteParam.Deprecated = true; break; } } diff --git a/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs index 8b7268513a..435f55496a 100644 --- a/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs +++ b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -8,12 +8,12 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { - operation.Responses.TryAdd( + operation.Responses?.TryAdd( "503", new OpenApiResponse { Description = "The server is currently starting or is temporarily not available.", - Headers = new Dictionary + Headers = new Dictionary { { "Retry-After", new OpenApiHeader @@ -23,7 +23,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter Description = "A hint for when to retry the operation in full seconds.", Schema = new OpenApiSchema { - Type = "integer", + Type = JsonSchemaType.Integer, Format = "int32" } } @@ -36,7 +36,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter Description = "A short plain-text reason why the server is not available.", Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "text" } } diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs index 8f57572696..5b048be913 100644 --- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs +++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs @@ -5,7 +5,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Extensions; using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -66,17 +66,10 @@ public class SecurityRequirementsOperationFilter : IOperationFilter return; } - operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); - operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + operation.Responses?.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses?.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); - var scheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthenticationSchemes.CustomAuthentication - }, - }; + var scheme = new OpenApiSecuritySchemeReference(AuthenticationSchemes.CustomAuthentication, null, null); // Add DefaultAuthorization scope to any endpoint that has a policy with a requirement that is a subset of DefaultAuthorization. if (!requiredScopes.Contains(DefaultAuthPolicy.AsSpan(), StringComparison.Ordinal)) diff --git a/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs b/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs new file mode 100644 index 0000000000..e4eb5be2b9 --- /dev/null +++ b/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs @@ -0,0 +1,56 @@ +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// +/// Document filter that fixes security scheme references after document generation. +/// +/// +/// In Microsoft.OpenApi v2, requires a resolved +/// Target to serialize correctly. References created without a host document (as in +/// operation filters) serialize as empty objects. This filter re-creates all security scheme +/// references with the document context so they resolve properly during serialization. +/// +internal class SecuritySchemeReferenceFixupFilter : IDocumentFilter +{ + /// + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + swaggerDoc.RegisterComponents(); + + if (swaggerDoc.Paths is null) + { + return; + } + + foreach (var pathItem in swaggerDoc.Paths.Values) + { + if (pathItem.Operations is null) + { + continue; + } + + foreach (var operation in pathItem.Operations.Values) + { + if (operation.Security is null) + { + continue; + } + + for (int i = 0; i < operation.Security.Count; i++) + { + var oldReq = operation.Security[i]; + var newReq = new OpenApiSecurityRequirement(); + foreach (var kvp in oldReq) + { + var fixedRef = new OpenApiSecuritySchemeReference(kvp.Key.Reference.Id!, swaggerDoc); + newReq[fixedRef] = kvp.Value; + } + + operation.Security[i] = newReq; + } + } + } + } +}