repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
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 Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Add models not directly used by the API, but used for discovery and websockets.
|
||||
/// </summary>
|
||||
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 readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AdditionalModelFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public AdditionalModelFilter(IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
|
||||
|
||||
var webSocketTypes = typeof(WebSocketMessage).Assembly.GetTypes()
|
||||
.Where(t => t.IsSubclassOf(typeof(WebSocketMessage))
|
||||
&& !t.IsGenericType
|
||||
&& t != typeof(WebSocketMessageInfo))
|
||||
.ToList();
|
||||
|
||||
var inboundWebSocketSchemas = new List<OpenApiSchema>();
|
||||
var inboundWebSocketDiscriminators = new Dictionary<string, string>();
|
||||
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
|
||||
{
|
||||
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||
if (messageType is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
inboundWebSocketSchemas.Add(schema);
|
||||
inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
|
||||
}
|
||||
|
||||
var inboundWebSocketMessageSchema = new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Description = "Represents the list of possible inbound websocket types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(InboundWebSocketMessage),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = inboundWebSocketSchemas,
|
||||
Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
PropertyName = nameof(WebSocketMessage.MessageType),
|
||||
Mapping = inboundWebSocketDiscriminators
|
||||
}
|
||||
};
|
||||
|
||||
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
|
||||
|
||||
var outboundWebSocketSchemas = new List<OpenApiSchema>();
|
||||
var outboundWebSocketDiscriminators = new Dictionary<string, string>();
|
||||
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
|
||||
{
|
||||
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||
if (messageType is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
outboundWebSocketSchemas.Add(schema);
|
||||
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
|
||||
}
|
||||
|
||||
// Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us
|
||||
var syncPlayGroupUpdateMessageSchema = new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Description = "Untyped sync play command.",
|
||||
Properties = new Dictionary<string, OpenApiSchema>
|
||||
{
|
||||
{
|
||||
"Data", new OpenApiSchema
|
||||
{
|
||||
AllOf =
|
||||
[
|
||||
new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate<object>) } }
|
||||
],
|
||||
Description = "Group update data",
|
||||
Nullable = false,
|
||||
}
|
||||
},
|
||||
{ "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } },
|
||||
{
|
||||
"MessageType", new OpenApiSchema
|
||||
{
|
||||
Enum = Enum.GetValues<SessionMessageType>().Select(type => new OpenApiString(type.ToString())).ToList<IOpenApiAny>(),
|
||||
AllOf =
|
||||
[
|
||||
new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } }
|
||||
],
|
||||
Description = "The different kinds of messages that are used in the WebSocket api.",
|
||||
Default = new OpenApiString(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 outboundWebSocketMessageSchema = new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Description = "Represents the list of possible outbound websocket types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(OutboundWebSocketMessage),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = outboundWebSocketSchemas,
|
||||
Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
PropertyName = nameof(WebSocketMessage.MessageType),
|
||||
Mapping = outboundWebSocketDiscriminators
|
||||
}
|
||||
};
|
||||
|
||||
context.SchemaRepository.AddDefinition(nameof(OutboundWebSocketMessage), outboundWebSocketMessageSchema);
|
||||
context.SchemaRepository.AddDefinition(
|
||||
nameof(WebSocketMessage),
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Description = "Represents the possible websocket types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(WebSocketMessage),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = new[]
|
||||
{
|
||||
inboundWebSocketMessageSchema,
|
||||
outboundWebSocketMessageSchema
|
||||
}
|
||||
});
|
||||
|
||||
// Manually generate sync play GroupUpdate messages.
|
||||
var groupUpdateTypes = typeof(GroupUpdate<>).Assembly.GetTypes()
|
||||
.Where(t => t.BaseType is not null
|
||||
&& t.BaseType.IsGenericType
|
||||
&& t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>))
|
||||
.ToList();
|
||||
|
||||
var groupUpdateSchemas = new List<OpenApiSchema>();
|
||||
var groupUpdateDiscriminators = new Dictionary<string, string>();
|
||||
foreach (var type in groupUpdateTypes)
|
||||
{
|
||||
var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate<object>.Type))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||
if (groupUpdateType is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
groupUpdateSchemas.Add(schema);
|
||||
groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3;
|
||||
}
|
||||
|
||||
var groupUpdateSchema = new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Description = "Represents the list of possible group update types",
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(GroupUpdate<object>),
|
||||
Type = ReferenceType.Schema
|
||||
},
|
||||
OneOf = groupUpdateSchemas,
|
||||
Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
PropertyName = nameof(GroupUpdate<object>.Type),
|
||||
Mapping = groupUpdateDiscriminators
|
||||
}
|
||||
};
|
||||
|
||||
context.SchemaRepository.Schemas[nameof(GroupUpdate<object>)] = groupUpdateSchema;
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
|
||||
|
||||
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
|
||||
{
|
||||
if (_ignoredConfigurations.IndexOf(configuration.ConfigurationType) != -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using AsyncKeyedLock;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// OpenApi provider with caching.
|
||||
/// </summary>
|
||||
internal sealed class CachingOpenApiProvider : ISwaggerProvider
|
||||
{
|
||||
private const string CacheKey = "openapi.json";
|
||||
|
||||
private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
|
||||
private static readonly AsyncNonKeyedLocker _lock = new(1);
|
||||
private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SwaggerGenerator _swaggerGenerator;
|
||||
private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
|
||||
private readonly ILogger<CachingOpenApiProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="optionsAccessor">The options accessor.</param>
|
||||
/// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
|
||||
/// <param name="schemaGenerator">The schema generator.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CachingOpenApiProvider(
|
||||
IOptions<SwaggerGeneratorOptions> optionsAccessor,
|
||||
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
|
||||
ISchemaGenerator schemaGenerator,
|
||||
IMemoryCache memoryCache,
|
||||
ILogger<CachingOpenApiProvider> logger)
|
||||
{
|
||||
_swaggerGeneratorOptions = optionsAccessor.Value;
|
||||
_swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
|
||||
_memoryCache = memoryCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
|
||||
{
|
||||
return AdjustDocument(openApiDocument, host, basePath);
|
||||
}
|
||||
|
||||
using var acquired = _lock.LockOrNull(_lockTimeout);
|
||||
if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
|
||||
{
|
||||
return AdjustDocument(openApiDocument, host, basePath);
|
||||
}
|
||||
|
||||
if (acquired is null)
|
||||
{
|
||||
throw new InvalidOperationException("OpenApi document is generating");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
openApiDocument = _swaggerGenerator.GetSwagger(documentName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "OpenAPI generation error");
|
||||
throw;
|
||||
}
|
||||
|
||||
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
|
||||
return AdjustDocument(openApiDocument, host, basePath);
|
||||
}
|
||||
|
||||
private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
|
||||
{
|
||||
document.Servers = _swaggerGeneratorOptions.Servers.Count != 0
|
||||
? _swaggerGeneratorOptions.Servers
|
||||
: string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath)
|
||||
? []
|
||||
: [new OpenApiServer { Url = $"{host}{basePath}" }];
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class FileRequestFilter : IOperationFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata)
|
||||
{
|
||||
if (attribute is AcceptsFileAttribute acceptsFileAttribute)
|
||||
{
|
||||
operation.RequestBody = GetRequestBody(acceptsFileAttribute.ContentTypes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenApiRequestBody GetRequestBody(IEnumerable<string> contentTypes)
|
||||
{
|
||||
var body = new OpenApiRequestBody();
|
||||
var mediaType = new OpenApiMediaType
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
};
|
||||
foreach (var contentType in contentTypes)
|
||||
{
|
||||
body.Content.Add(contentType, mediaType);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class FileResponseFilter : IOperationFilter
|
||||
{
|
||||
private const string SuccessCode = "200";
|
||||
private static readonly OpenApiMediaType _openApiMediaType = new OpenApiMediaType
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata)
|
||||
{
|
||||
if (attribute is ProducesFileAttribute producesFileAttribute)
|
||||
{
|
||||
// Get operation response values.
|
||||
var response = operation.Responses
|
||||
.FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
|
||||
|
||||
// Operation doesn't have a response.
|
||||
if (response.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clear existing responses.
|
||||
response.Value.Content.Clear();
|
||||
|
||||
// Add all content-types as file.
|
||||
foreach (var contentType in producesFileAttribute.ContentTypes)
|
||||
{
|
||||
response.Value.Content.Add(contentType, _openApiMediaType);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Schema filter to ensure flags enums are represented correctly in OpenAPI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For flags enums:
|
||||
/// - The enum schema definition is set to type "string" (not integer).
|
||||
/// - Properties using flags enums are transformed to arrays referencing the enum schema.
|
||||
/// </remarks>
|
||||
public class FlagsEnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type);
|
||||
if (type is null || !type.IsEnum)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if enum has [Flags] attribute
|
||||
if (!type.IsDefined(typeof(FlagsAttribute), false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.MemberInfo is null)
|
||||
{
|
||||
// Processing the enum definition itself - ensure it's type "string" not "integer"
|
||||
schema.Type = "string";
|
||||
schema.Format = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Processing a property that uses the flags enum - transform to array
|
||||
// Generate the enum schema to ensure it exists in the repository
|
||||
var enumSchema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Jellyfin.Data.Attributes;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Filter to remove ignored enum values.
|
||||
/// </summary>
|
||||
public class IgnoreEnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false))
|
||||
{
|
||||
var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type);
|
||||
if (type is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enumOpenApiStrings = new List<IOpenApiAny>();
|
||||
|
||||
foreach (var enumName in Enum.GetNames(type))
|
||||
{
|
||||
var member = type.GetMember(enumName)[0];
|
||||
if (!member.GetCustomAttributes<OpenApiIgnoreEnumAttribute>().Any())
|
||||
{
|
||||
enumOpenApiStrings.Add(new OpenApiString(enumName));
|
||||
}
|
||||
}
|
||||
|
||||
schema.Enum = enumOpenApiStrings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Mark parameter as deprecated if it has the <see cref="ParameterObsoleteAttribute"/>.
|
||||
/// </summary>
|
||||
public class ParameterObsoleteFilter : IOperationFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
foreach (var parameterDescription in context.ApiDescription.ParameterDescriptions)
|
||||
{
|
||||
if (parameterDescription
|
||||
.CustomAttributes()
|
||||
.OfType<ParameterObsoleteAttribute>()
|
||||
.Any())
|
||||
{
|
||||
foreach (var parameter in operation.Parameters)
|
||||
{
|
||||
if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal))
|
||||
{
|
||||
parameter.Deprecated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses.TryAdd(
|
||||
"503",
|
||||
new OpenApiResponse
|
||||
{
|
||||
Description = "The server is currently starting or is temporarily not available.",
|
||||
Headers = new Dictionary<string, OpenApiHeader>
|
||||
{
|
||||
{
|
||||
"Retry-After", new OpenApiHeader
|
||||
{
|
||||
AllowEmptyValue = true,
|
||||
Required = false,
|
||||
Description = "A hint for when to retry the operation in full seconds.",
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "integer",
|
||||
Format = "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Message", new OpenApiHeader
|
||||
{
|
||||
AllowEmptyValue = true,
|
||||
Required = false,
|
||||
Description = "A short plain-text reason why the server is not available.",
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Format = "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Content = new Dictionary<string, OpenApiMediaType>()
|
||||
{
|
||||
{ "text/html", new OpenApiMediaType() }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Security requirement operation filter.
|
||||
/// </summary>
|
||||
public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
{
|
||||
private const string DefaultAuthPolicy = "DefaultAuthorization";
|
||||
private static readonly Type _attributeType = typeof(AuthorizeAttribute);
|
||||
|
||||
private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecurityRequirementsOperationFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authorizationPolicyProvider">The authorization policy provider.</param>
|
||||
public SecurityRequirementsOperationFilter(IAuthorizationPolicyProvider authorizationPolicyProvider)
|
||||
{
|
||||
_authorizationPolicyProvider = authorizationPolicyProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var requiredScopes = new List<string>();
|
||||
|
||||
var requiresAuth = false;
|
||||
// Add all method scopes.
|
||||
foreach (var authorizeAttribute in context.MethodInfo.GetCustomAttributes(_attributeType, true).Cast<AuthorizeAttribute>())
|
||||
{
|
||||
requiresAuth = true;
|
||||
var policy = authorizeAttribute.Policy ?? DefaultAuthPolicy;
|
||||
if (!requiredScopes.Contains(policy, StringComparer.Ordinal))
|
||||
{
|
||||
requiredScopes.Add(policy);
|
||||
}
|
||||
}
|
||||
|
||||
// Add controller scopes if any.
|
||||
var controllerAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(_attributeType, true).Cast<AuthorizeAttribute>();
|
||||
if (controllerAttributes is not null)
|
||||
{
|
||||
foreach (var authorizeAttribute in controllerAttributes)
|
||||
{
|
||||
requiresAuth = true;
|
||||
var policy = authorizeAttribute.Policy ?? DefaultAuthPolicy;
|
||||
if (!requiredScopes.Contains(policy, StringComparer.Ordinal))
|
||||
{
|
||||
requiredScopes.Add(policy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiresAuth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
};
|
||||
|
||||
// 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))
|
||||
{
|
||||
foreach (var scope in requiredScopes)
|
||||
{
|
||||
var authorizationPolicy = _authorizationPolicyProvider.GetPolicyAsync(scope).GetAwaiter().GetResult();
|
||||
if (authorizationPolicy is not null
|
||||
&& authorizationPolicy.Requirements.Any(r => r is DefaultAuthorizationRequirement))
|
||||
{
|
||||
requiredScopes.Add(DefaultAuthPolicy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operation.Security = [new OpenApiSecurityRequirement { [scheme] = requiredScopes }];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user