diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 981d207959..4c4807d337 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -233,10 +233,18 @@ namespace Tgstation.Server.Host.Controllers /// The model containing the to update to. /// The for the operation. /// A resulting in the for the operation. + /// Update has been started successfully. + /// The requested version could not be found. /// Upgrade operations are unavailable due to the launch configuration of TGS. + /// A GitHub rate limit was encountered. + /// A GitHub API error occurred. [HttpPost] [TgsAuthorize(AdministrationRights.ChangeVersion)] + [ProducesResponseType(202)] + [ProducesResponseType(410)] [ProducesResponseType(typeof(ErrorMessage), 422)] + [ProducesResponseType(424)] + [ProducesResponseType(typeof(ErrorMessage), 429)] public async Task Update([FromBody] Administration model, CancellationToken cancellationToken) { if (model == null) @@ -258,12 +266,12 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Attempts to restart the server + /// Attempts to restart the server. /// /// A resulting in the of the request /// Restart begun successfully. /// Restart operations are unavailable due to the launch configuration of TGS. - [HttpDelete("{id}")] + [HttpDelete] [TgsAuthorize(AdministrationRights.RestartHost)] [ProducesResponseType(200)] [ProducesResponseType(typeof(ErrorMessage), 422)] diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index a160198a49..ec2573ed4c 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -151,7 +151,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the of the operation. /// Watchdog terminated. - [HttpDelete("{id}")] + [HttpDelete] [TgsAuthorize(DreamDaemonRights.Shutdown)] [ProducesResponseType(200)] public async Task Delete(CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index aba60c5dbd..765388fb4c 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -232,7 +232,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation /// Job to delete the repository created successfully. /// Instance no longer available. - [HttpDelete("{id}")] + [HttpDelete] [TgsAuthorize(RepositoryRights.Delete)] [ProducesResponseType(typeof(Repository), 202)] [ProducesResponseType(410)] diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index ad03584087..40c58d0ff6 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -12,8 +12,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Microsoft.OpenApi.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Serilog; @@ -261,43 +259,12 @@ namespace Tgstation.Server.Host.Core }); if (hostingEnvironment.IsDevelopment()) - services.AddSwaggerGen( - c => - { - c.SwaggerDoc( - "v1", - new OpenApiInfo - { - Title = "TGS API", - Version = "v4" - }); - - // Important to do this before applying our own filters - // Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter - var assemblyLocation = assemblyInformationProvider.Path; - var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml")); - c.IncludeXmlComments(filePath); - - c.OperationFilter(); - c.DocumentFilter(); - - c.AddSecurityDefinition(SwaggerConfiguration.PasswordSecuritySchemeId, new OpenApiSecurityScheme - { - In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, - Name = HeaderNames.Authorization, - Scheme = ApiHeaders.BasicAuthenticationScheme - }); - - c.AddSecurityDefinition(SwaggerConfiguration.TokenSecuritySchemeId, new OpenApiSecurityScheme - { - BearerFormat = "JWT", - In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, - Name = HeaderNames.Authorization, - Scheme = ApiHeaders.JwtAuthenticationScheme - }); - }); + { + string GetDocumentationFilePath(string assemblyLocation) => ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml")); + var assemblyDocumentationPath = GetDocumentationFilePath(assemblyInformationProvider.Path); + var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location); + services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath)); + } // enable browser detection services.AddDetectionCore().AddBrowser(); diff --git a/src/Tgstation.Server.Host/Core/OpenApiEnumVarNamesExtension.cs b/src/Tgstation.Server.Host/Core/OpenApiEnumVarNamesExtension.cs new file mode 100644 index 0000000000..2955147706 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/OpenApiEnumVarNamesExtension.cs @@ -0,0 +1,57 @@ +using Microsoft.OpenApi; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Writers; +using System; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Implements the "x-enum-varnames" OpenAPI 3.0 extension. + /// + sealed class OpenApiEnumVarNamesExtension : IOpenApiExtension + { + /// + /// The of the being described. + /// + readonly Type enumType; + + /// + /// Initializes a new instance of the . + /// + /// The value of , + private OpenApiEnumVarNamesExtension(Type enumType) + { + this.enumType = enumType ?? throw new ArgumentNullException(nameof(enumType)); + } + + /// + /// Applies the extension to a give . + /// + /// The to apply to. + /// The of the being described. + public static void Apply(OpenApiSchema openApiSchema, Type enumType) + { + if (openApiSchema == null) + throw new ArgumentNullException(nameof(openApiSchema)); + + openApiSchema.Extensions.Add("x-enum-varnames", new OpenApiEnumVarNamesExtension(enumType)); + } + + /// + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + if (writer == null) + throw new ArgumentNullException(nameof(writer)); + + if (specVersion != OpenApiSpecVersion.OpenApi3_0) + throw new InvalidOperationException("This extension only applies to OpenAPI 3.0!"); + + writer.WriteStartArray(); + foreach (var enumValue in Enum.GetValues(enumType)) + writer.WriteValue(enumValue.ToString()); + + writer.WriteEndArray(); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index 650e56903a..de3e3d9f02 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -1,4 +1,5 @@ -using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -21,87 +22,12 @@ namespace Tgstation.Server.Host.Core /// /// The name for password authentication. /// - public const string PasswordSecuritySchemeId = "Password_Login_Scheme"; + const string PasswordSecuritySchemeId = "Password_Login_Scheme"; /// /// The name for token authentication. /// - public const string TokenSecuritySchemeId = "Token_Authorization_Scheme"; - - /// - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (operation == null) - throw new ArgumentNullException(nameof(operation)); - if (context == null) - throw new ArgumentNullException(nameof(context)); - - var authAttributes = context - .MethodInfo - .DeclaringType - .GetCustomAttributes(true) - .Union( - context - .MethodInfo - .GetCustomAttributes(true)) - .OfType(); - - if (authAttributes.Any()) - { - var tokenScheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = TokenSecuritySchemeId - } - }; - - operation.Security = new List - { - new OpenApiSecurityRequirement - { - { - tokenScheme, - new List() - } - } - }; - - if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value))) - operation.Parameters.Add(new OpenApiParameter - { - Reference = new OpenApiReference - { - Type = ReferenceType.Parameter, - Id = ApiHeaders.InstanceIdHeader - } - }); - } - else - { - // HomeController.CreateToken - var passwordScheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = PasswordSecuritySchemeId - } - }; - - operation.Security = new List - { - new OpenApiSecurityRequirement - { - { - passwordScheme, - new List() - } - } - }; - } - } + const string TokenSecuritySchemeId = "Token_Authorization_Scheme"; static void AddDefaultResponses(OpenApiDocument document) { @@ -176,6 +102,126 @@ namespace Tgstation.Server.Host.Core }); } + /// + /// Configure the swagger settings. + /// + /// The to use. + /// The path to the XML documentation file for the assembly. + /// The path to the XML documentation file for the assembly. + public static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath) + { + swaggerGenOptions.SwaggerDoc( + "v1", + new OpenApiInfo + { + Title = "TGS API", + Version = "v4" + }); + + // Important to do this before applying our own filters + // Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter + swaggerGenOptions.IncludeXmlComments(assemblyDocumentationPath); + swaggerGenOptions.IncludeXmlComments(apiDocumentationPath); + + swaggerGenOptions.OperationFilter(); + swaggerGenOptions.DocumentFilter(); + swaggerGenOptions.SchemaFilter(); + + swaggerGenOptions.AddSecurityDefinition(PasswordSecuritySchemeId, new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Name = HeaderNames.Authorization, + Scheme = ApiHeaders.BasicAuthenticationScheme + }); + + swaggerGenOptions.AddSecurityDefinition(TokenSecuritySchemeId, new OpenApiSecurityScheme + { + BearerFormat = "JWT", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Name = HeaderNames.Authorization, + Scheme = ApiHeaders.JwtAuthenticationScheme + }); + } + + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (operation == null) + throw new ArgumentNullException(nameof(operation)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + operation.OperationId = $"{context.MethodInfo.DeclaringType.Name}.{context.MethodInfo.Name}"; + + var authAttributes = context + .MethodInfo + .DeclaringType + .GetCustomAttributes(true) + .Union( + context + .MethodInfo + .GetCustomAttributes(true)) + .OfType(); + + if (authAttributes.Any()) + { + var tokenScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = TokenSecuritySchemeId + } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + { + tokenScheme, + new List() + } + } + }; + + if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value))) + operation.Parameters.Add(new OpenApiParameter + { + Reference = new OpenApiReference + { + Type = ReferenceType.Parameter, + Id = ApiHeaders.InstanceIdHeader + } + }); + } + else + { + // HomeController.CreateToken + var passwordScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = PasswordSecuritySchemeId + } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + { + passwordScheme, + new List() + } + } + }; + } + } + /// public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { @@ -190,9 +236,19 @@ namespace Tgstation.Server.Host.Core Name = ApiHeaders.InstanceIdHeader, Description = "The instance ID being accessed", Required = true, - Style = ParameterStyle.Simple + Style = ParameterStyle.Simple, + Schema = new OpenApiSchema + { + Type = "integer" + } }); + var productHeaderSchema = new OpenApiSchema + { + Type = "string", + Format = "productheader" + }; + swaggerDoc.Components.Parameters.Add(ApiHeaders.ApiVersionHeader, new OpenApiParameter { In = ParameterLocation.Header, @@ -200,7 +256,8 @@ namespace Tgstation.Server.Host.Core Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"", Required = true, Style = ParameterStyle.Simple, - Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}") + Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}"), + Schema = productHeaderSchema }); swaggerDoc.Components.Parameters.Add(HeaderNames.UserAgent, new OpenApiParameter @@ -210,7 +267,8 @@ namespace Tgstation.Server.Host.Core Description = "The user agent of the calling client.", Required = true, Style = ParameterStyle.Simple, - Example = new OpenApiString("Your-user-agent/1.0.0.0") + Example = new OpenApiString("Your-user-agent/1.0.0.0"), + Schema = productHeaderSchema }); foreach (var operation in swaggerDoc @@ -247,6 +305,16 @@ namespace Tgstation.Server.Host.Core throw new ArgumentNullException(nameof(schema)); if (context == null) throw new ArgumentNullException(nameof(context)); + + if (!schema.Enum?.Any() ?? false) + return; + + // Could be nullable type, make sure to get the right one + Type enumType = context.Type.IsConstructedGenericType + ? context.Type.GenericTypeArguments.First() + : context.Type; + + OpenApiEnumVarNamesExtension.Apply(schema, enumType); } } }