mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 08:33:19 +01:00
Improve OpenAPI conformity
This commit is contained in:
@@ -233,10 +233,18 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="model">The model containing the <see cref="Administration.NewVersion"/> to update to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="202">Update has been started successfully.</response>
|
||||
/// <response code="410">The requested version could not be found.</response>
|
||||
/// <response code="422">Upgrade operations are unavailable due to the launch configuration of TGS.</response>
|
||||
/// <response code="424">A GitHub rate limit was encountered.</response>
|
||||
/// <response code="429">A GitHub API error occurred.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion)]
|
||||
[ProducesResponseType(202)]
|
||||
[ProducesResponseType(410)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 422)]
|
||||
[ProducesResponseType(424)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 429)]
|
||||
public async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
@@ -258,12 +266,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to restart the server
|
||||
/// Attempts to restart the server.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request</returns>
|
||||
/// <response code="200">Restart begun successfully.</response>
|
||||
/// <response code="422">Restart operations are unavailable due to the launch configuration of TGS.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(AdministrationRights.RestartHost)]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 422)]
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
|
||||
/// <response code="200">Watchdog terminated.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(DreamDaemonRights.Shutdown)]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken)
|
||||
|
||||
@@ -232,7 +232,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
|
||||
/// <response code="202">Job to delete the repository created successfully.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(RepositoryRights.Delete)]
|
||||
[ProducesResponseType(typeof(Repository), 202)]
|
||||
[ProducesResponseType(410)]
|
||||
|
||||
@@ -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<SwaggerConfiguration>();
|
||||
c.DocumentFilter<SwaggerConfiguration>();
|
||||
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the "x-enum-varnames" OpenAPI 3.0 extension.
|
||||
/// </summary>
|
||||
sealed class OpenApiEnumVarNamesExtension : IOpenApiExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Type"/> of the <see cref="Enum"/> being described.
|
||||
/// </summary>
|
||||
readonly Type enumType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenApiEnumVarNamesExtension"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="enumType">The value of <see cref="enumType"/>,</param>
|
||||
private OpenApiEnumVarNamesExtension(Type enumType)
|
||||
{
|
||||
this.enumType = enumType ?? throw new ArgumentNullException(nameof(enumType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the extension to a give <paramref name="openApiSchema"/>.
|
||||
/// </summary>
|
||||
/// <param name="openApiSchema">The <see cref="OpenApiSchema"/> to apply to.</param>
|
||||
/// <param name="enumType">The <see cref="Type"/> of the <see cref="Enum"/> being described.</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// The <see cref="OpenApiSecurityScheme"/> name for password authentication.
|
||||
/// </summary>
|
||||
public const string PasswordSecuritySchemeId = "Password_Login_Scheme";
|
||||
const string PasswordSecuritySchemeId = "Password_Login_Scheme";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OpenApiSecurityScheme"/> name for token authentication.
|
||||
/// </summary>
|
||||
public const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
|
||||
|
||||
/// <inheritdoc />
|
||||
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<TgsAuthorizeAttribute>();
|
||||
|
||||
if (authAttributes.Any())
|
||||
{
|
||||
var tokenScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = TokenSecuritySchemeId
|
||||
}
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
tokenScheme,
|
||||
new List<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
passwordScheme,
|
||||
new List<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
|
||||
|
||||
static void AddDefaultResponses(OpenApiDocument document)
|
||||
{
|
||||
@@ -176,6 +102,126 @@ namespace Tgstation.Server.Host.Core
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the swagger settings.
|
||||
/// </summary>
|
||||
/// <param name="swaggerGenOptions">The <see cref="SwaggerGenOptions"/> to use.</param>
|
||||
/// <param name="assemblyDocumentationPath">The path to the XML documentation file for the <see cref="Host"/> assembly.</param>
|
||||
/// <param name="apiDocumentationPath">The path to the XML documentation file for the <see cref="Api"/> assembly.</param>
|
||||
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<SwaggerConfiguration>();
|
||||
swaggerGenOptions.DocumentFilter<SwaggerConfiguration>();
|
||||
swaggerGenOptions.SchemaFilter<SwaggerConfiguration>();
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<TgsAuthorizeAttribute>();
|
||||
|
||||
if (authAttributes.Any())
|
||||
{
|
||||
var tokenScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = TokenSecuritySchemeId
|
||||
}
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
tokenScheme,
|
||||
new List<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
passwordScheme,
|
||||
new List<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user