Improve OpenAPI conformity

This commit is contained in:
Cyberboss
2020-01-18 23:45:24 -05:00
parent 7ec06871c3
commit 306eb59ed6
6 changed files with 224 additions and 124 deletions
@@ -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)]
+6 -39
View File
@@ -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);
}
}
}