mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-28 15:40:56 +01:00
Reduce duplication in swagger spec
This commit is contained in:
@@ -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."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user