Reduce duplication in swagger spec

This commit is contained in:
Cyberboss
2020-01-12 16:42:19 -05:00
parent 21c0fdfa9b
commit 48bc829bd9
3 changed files with 254 additions and 174 deletions
@@ -0,0 +1,244 @@
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// <see cref="IOperationFilter"/> and <see cref="IDocumentFilter"/> for the server.
/// </summary>
sealed class TgsOpenApiFilters : IOperationFilter, IDocumentFilter
{
/// <summary>
/// The <see cref="OpenApiSecurityScheme"/> name for password authentication.
/// </summary>
public const string PasswordSecuritySchemeId = "Password_Login_Scheme";
/// <summary>
/// The <see cref="OpenApiSecurityScheme"/> name for token authentication.
/// </summary>
public const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
const string InstanceIdParameterId = "Instance_ID_Parameter";
const string ApiVersionParameterId = "Api_Version_Parameter";
const string UserAgentParameterId = "User_Agent_Parameter";
readonly ICollection<OpenApiOperation> operationsToAddInstanceIdReferenceTo;
/// <summary>
/// Initializes a new instance of the <see cref="TgsOpenApiFilters"/> <see langword="class"/>.
/// </summary>
public TgsOpenApiFilters()
{
operationsToAddInstanceIdReferenceTo = new List<OpenApiOperation>();
}
/// <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.Header,
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)
{
swaggerDoc.Components.Headers.Add(ApiHeaders.InstanceIdHeader, new OpenApiHeader
{
Description = "The instance ID being accessed",
Required = true,
Style = ParameterStyle.Simple
});
operationsToAddInstanceIdReferenceTo.Clear();
swaggerDoc.Components.Headers.Add(ApiHeaders.ApiVersionHeader, new OpenApiHeader
{
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}")
});
swaggerDoc.Components.Headers.Add(HeaderNames.UserAgent, new OpenApiHeader
{
Description = "The user agent of the calling client.",
Required = true,
Style = ParameterStyle.Simple,
Example = new OpenApiString("Your-user-agent/1.0.0.0")
});
foreach (var operation in swaggerDoc
.Paths
.SelectMany(path => path.Value.Operations)
.Select(kvp => kvp.Value))
{
operation.Parameters.Add(new OpenApiParameter
{
Reference = new OpenApiReference
{
Type = ReferenceType.Header,
Id = ApiHeaders.ApiVersionHeader
}
});
operation.Parameters.Add(new OpenApiParameter
{
Reference = new OpenApiReference
{
Type = ReferenceType.Header,
Id = HeaderNames.UserAgent
}
});
}
var errorMessageContent = new Dictionary<string, OpenApiMediaType>
{
{
ApiHeaders.ApplicationJson,
new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Reference = new OpenApiReference
{
Id = nameof(ErrorMessage),
Type = ReferenceType.Schema
}
}
}
}
};
void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
{
string responseKey = $"{(int)code}";
swaggerDoc.Components.Responses.Add(responseKey, concrete);
var referenceResponse = new OpenApiResponse
{
Reference = new OpenApiReference
{
Type = ReferenceType.Response,
Id = responseKey
}
};
foreach (var path in swaggerDoc.Paths)
foreach (var operation in path.Value.Operations)
operation.Value.Responses.TryAdd(responseKey, referenceResponse);
}
AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
{
Description = "A badly formatted request was made. See error message for details.",
Content = errorMessageContent,
});
AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
{
Description = "No/invalid token provided."
});
AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
{
Description = "User lacks sufficient permissions for the operation."
});
AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
{
Description = "A data integrity check failed while performing the operation. See error message for details.",
Content = errorMessageContent
});
AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
{
Description = "The server encountered an unhandled error. See error message for details.",
Content = errorMessageContent
});
AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
{
Description = "The server may be starting up or shutting down."
});
}
}
}
@@ -1,168 +0,0 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// <see cref="IOperationFilter"/> for the server.
/// </summary>
sealed class TgsOperationFilter : IOperationFilter
{
/// <summary>
/// The <see cref="OpenApiSecurityScheme"/> name for password authentication.
/// </summary>
public const string PasswordSecuritySchemeId = "Password_Login";
/// <summary>
/// The <see cref="OpenApiSecurityScheme"/> name for token authentication.
/// </summary>
public const string TokenSecuritySchemeId = "Token_Authorization";
/// <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>();
// stub var because debugger conditions are bad
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
{
In = ParameterLocation.Header,
Description = "The instance ID being accessed",
Name = ApiHeaders.InstanceIdHeader,
Required = true,
Style = ParameterStyle.Simple
});
}
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>()
}
}
};
}
operation.Parameters.Add(new OpenApiParameter
{
In = ParameterLocation.Header,
Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
Name = ApiHeaders.ApiVersionHeader,
Required = true,
Style = ParameterStyle.Simple,
Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}")
});
operation.Parameters.Add(new OpenApiParameter
{
In = ParameterLocation.Header,
Description = "The user agent of the calling client.",
Name = "User-Agent",
Required = true,
Style = ParameterStyle.Simple,
Example = new OpenApiString("Your-user-agent/1.0.0.0")
});
var errorMessageContent = new Dictionary<string, OpenApiMediaType>
{
{
ApiHeaders.ApplicationJson,
new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Reference = new OpenApiReference
{
Id = nameof(ErrorMessage),
Type = ReferenceType.Schema
}
}
}
}
};
// Add default common status codes
operation.Responses.TryAdd("400", new OpenApiResponse
{
Description = "A badly formatted request was made. See error message for details.",
Content = errorMessageContent
});
operation.Responses.TryAdd("401", new OpenApiResponse
{
Description = "No/invalid token provided."
});
operation.Responses.TryAdd("403", new OpenApiResponse
{
Description = "User lacks sufficient permissions for the operation."
});
operation.Responses.TryAdd("409", new OpenApiResponse
{
Description = "A data integrity check failed while performing the operation. See error message for details.",
Content = errorMessageContent
});
operation.Responses.TryAdd("500", new OpenApiResponse
{
Description = "The server encountered an unhandled error. See error message for details.",
Content = errorMessageContent
});
operation.Responses.TryAdd("503", new OpenApiResponse
{
Description = "The server may be starting up or shutting down."
});
}
}
}
+10 -6
View File
@@ -256,9 +256,16 @@ namespace Tgstation.Server.Host.Core
Version = "v4"
});
c.OperationFilter<TgsOperationFilter>();
// 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 = Assembly.GetExecutingAssembly().Location;
var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
c.IncludeXmlComments(filePath);
c.AddSecurityDefinition(TgsOperationFilter.PasswordSecuritySchemeId, new OpenApiSecurityScheme
c.OperationFilter<TgsOpenApiFilters>();
c.DocumentFilter<TgsOpenApiFilters>();
c.AddSecurityDefinition(TgsOpenApiFilters.PasswordSecuritySchemeId, new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
@@ -266,7 +273,7 @@ namespace Tgstation.Server.Host.Core
Scheme = ApiHeaders.BasicAuthenticationScheme
});
c.AddSecurityDefinition(TgsOperationFilter.TokenSecuritySchemeId, new OpenApiSecurityScheme
c.AddSecurityDefinition(TgsOpenApiFilters.TokenSecuritySchemeId, new OpenApiSecurityScheme
{
BearerFormat = "JWT",
In = ParameterLocation.Header,
@@ -274,9 +281,6 @@ namespace Tgstation.Server.Host.Core
Name = HeaderNames.Authorization,
Scheme = ApiHeaders.JwtAuthenticationScheme
});
var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Tgstation.Server.Host.xml");
c.IncludeXmlComments(filePath);
});
// enable browser detection