tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
SwaggerConfiguration.cs
Go to the documentation of this file.
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using System.Net;
6using System.Net.Mime;
7using System.Reflection;
8
9using Microsoft.Extensions.DependencyInjection;
10using Microsoft.Net.Http.Headers;
11using Microsoft.OpenApi.Any;
12using Microsoft.OpenApi.Models;
13
14using Swashbuckle.AspNetCore.SwaggerGen;
15
22
24{
29 {
33 public const string DocumentName = "tgs_api";
34
38 public const string DocumentationSiteRouteExtension = "documentation";
39
43 const string PasswordSecuritySchemeId = "Password_Login_Scheme";
44
48 const string OAuthSecuritySchemeId = "OAuth_Login_Scheme";
49
53 const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
54
61 public static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
62 {
63 swaggerGenOptions.SwaggerDoc(
65 new OpenApiInfo
66 {
67 Title = "TGS API",
68 Version = ApiHeaders.Version.Semver().ToString(),
69 License = new OpenApiLicense
70 {
71 Name = "AGPL-3.0",
72 Url = new Uri("https://github.com/tgstation/tgstation-server/blob/dev/LICENSE"),
73 },
74 Contact = new OpenApiContact
75 {
76 Name = "/tg/station 13",
77 Url = new Uri("https://github.com/tgstation"),
78 },
79 Description = "A production scale tool for DreamMaker server management",
80 });
81
82 // Important to do this before applying our own filters
83 // Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter
84 swaggerGenOptions.IncludeXmlComments(assemblyDocumentationPath);
85 swaggerGenOptions.IncludeXmlComments(apiDocumentationPath);
86
87 // nullable stuff
88 swaggerGenOptions.UseAllOfToExtendReferenceSchemas();
89
90 swaggerGenOptions.OperationFilter<SwaggerConfiguration>();
91 swaggerGenOptions.DocumentFilter<SwaggerConfiguration>();
92 swaggerGenOptions.SchemaFilter<SwaggerConfiguration>();
93 swaggerGenOptions.RequestBodyFilter<SwaggerConfiguration>();
94
95 swaggerGenOptions.CustomSchemaIds(GenerateSchemaId);
96
97 swaggerGenOptions.AddSecurityDefinition(PasswordSecuritySchemeId, new OpenApiSecurityScheme
98 {
99 In = ParameterLocation.Header,
100 Type = SecuritySchemeType.Http,
101 Name = HeaderNames.Authorization,
103 });
104
105 swaggerGenOptions.AddSecurityDefinition(OAuthSecuritySchemeId, new OpenApiSecurityScheme
106 {
107 In = ParameterLocation.Header,
108 Type = SecuritySchemeType.Http,
109 Name = HeaderNames.Authorization,
111 });
112
113 swaggerGenOptions.AddSecurityDefinition(TokenSecuritySchemeId, new OpenApiSecurityScheme
114 {
115 BearerFormat = "JWT",
116 In = ParameterLocation.Header,
117 Type = SecuritySchemeType.Http,
118 Name = HeaderNames.Authorization,
120 });
121 }
122
127 static void AddDefaultResponses(OpenApiDocument document)
128 {
129 var errorMessageContent = new Dictionary<string, OpenApiMediaType>
130 {
131 {
132 MediaTypeNames.Application.Json,
133 new OpenApiMediaType
134 {
135 Schema = new OpenApiSchema
136 {
137 Reference = new OpenApiReference
138 {
139 Id = nameof(ErrorMessageResponse),
140 Type = ReferenceType.Schema,
141 },
142 },
143 }
144 },
145 };
146
147 void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
148 {
149 string responseKey = $"{(int)code}";
150
151 document.Components.Responses.Add(responseKey, concrete);
152
153 var referenceResponse = new OpenApiResponse
154 {
155 Reference = new OpenApiReference
156 {
157 Type = ReferenceType.Response,
158 Id = responseKey,
159 },
160 };
161
162 foreach (var operation in document.Paths.SelectMany(path => path.Value.Operations))
163 operation.Value.Responses.TryAdd(responseKey, referenceResponse);
164 }
165
166 AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
167 {
168 Description = "A badly formatted request was made. See error message for details.",
169 Content = errorMessageContent,
170 });
171
172 AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
173 {
174 Description = "Invalid Authentication header.",
175 });
176
177 AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
178 {
179 Description = "User lacks sufficient permissions for the operation.",
180 });
181
182 AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
183 {
184 Description = "A data integrity check failed while performing the operation. See error message for details.",
185 Content = errorMessageContent,
186 });
187
188 AddDefaultResponse(HttpStatusCode.NotAcceptable, new OpenApiResponse
189 {
190 Description = $"Invalid Accept header, TGS requires `{HeaderNames.Accept}: {MediaTypeNames.Application.Json}`.",
191 Content = errorMessageContent,
192 });
193
194 AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
195 {
196 Description = ErrorCode.InternalServerError.Describe(),
197 Content = errorMessageContent,
198 });
199
200 AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
201 {
202 Description = "The server may be starting up or shutting down.",
203 });
204
205 AddDefaultResponse(HttpStatusCode.NotImplemented, new OpenApiResponse
206 {
207 Description = ErrorCode.RequiresPosixSystemIdentity.Describe(),
208 Content = errorMessageContent,
209 });
210 }
211
217 static void ApplyAttributesForRootSchema(OpenApiSchema rootSchema, SchemaFilterContext context)
218 {
219 // tune up the descendants
220 rootSchema.Nullable = false;
221 var rootSchemaId = GenerateSchemaId(context.Type);
222 var rootRequestSchema = rootSchemaId.EndsWith("Request", StringComparison.Ordinal);
223 var rootResponseSchema = rootSchemaId.EndsWith("Response", StringComparison.Ordinal);
224 var isPutRequest = rootSchemaId.EndsWith("CreateRequest", StringComparison.Ordinal);
225
226 Tuple<PropertyInfo, string, OpenApiSchema, IDictionary<string, OpenApiSchema>> GetTypeFromKvp(Type currentType, KeyValuePair<string, OpenApiSchema> kvp, IDictionary<string, OpenApiSchema> schemaDictionary)
227 {
228 var propertyInfo = currentType
229 .GetProperties()
230 .Single(x => x.Name.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase));
231
232 return Tuple.Create(
233 propertyInfo,
234 kvp.Key,
235 kvp.Value,
236 schemaDictionary);
237 }
238
239 var subSchemaStack = new Stack<Tuple<PropertyInfo, string, OpenApiSchema, IDictionary<string, OpenApiSchema>>>(
240 rootSchema
241 .Properties
242 .Select(
243 x => GetTypeFromKvp(context.Type, x, rootSchema.Properties))
244 .Where(x => x.Item3.Reference == null));
245
246 while (subSchemaStack.Count > 0)
247 {
248 var tuple = subSchemaStack.Pop();
249 var subSchema = tuple.Item3;
250
251 var subSchemaPropertyInfo = tuple.Item1;
252
253 if (subSchema.Properties != null
254 && !subSchemaPropertyInfo
255 .PropertyType
256 .GetInterfaces()
257 .Any(x => x == typeof(IEnumerable)))
258 foreach (var kvp in subSchema.Properties.Where(x => x.Value.Reference == null))
259 subSchemaStack.Push(GetTypeFromKvp(subSchemaPropertyInfo.PropertyType, kvp, subSchema.Properties));
260
261 var attributes = subSchemaPropertyInfo
262 .GetCustomAttributes();
263 var responsePresence = attributes
264 .OfType<ResponseOptionsAttribute>()
265 .FirstOrDefault()
266 ?.Presence
267 ?? FieldPresence.Required;
268 var requestOptions = attributes
269 .OfType<RequestOptionsAttribute>()
270 .OrderBy(x => x.PutOnly) // Process PUTs last
271 .ToList();
272
273 if (requestOptions.Count == 0 && requestOptions.All(x => x.Presence == FieldPresence.Ignored && !x.PutOnly))
274 subSchema.ReadOnly = true;
275
276 var subSchemaId = tuple.Item2;
277 var subSchemaOwningDictionary = tuple.Item4;
278 if (rootResponseSchema)
279 {
280 subSchema.Nullable = responsePresence == FieldPresence.Optional;
281 if (responsePresence == FieldPresence.Ignored)
282 subSchemaOwningDictionary.Remove(subSchemaId);
283 }
284 else if (rootRequestSchema)
285 {
286 subSchema.Nullable = true;
287 var lastOptionWasIgnored = false;
288 foreach (var requestOption in requestOptions)
289 {
290 var validForThisRequest = !requestOption.PutOnly || isPutRequest;
291 if (!validForThisRequest)
292 continue;
293
294 lastOptionWasIgnored = false;
295 switch (requestOption.Presence)
296 {
297 case FieldPresence.Ignored:
298 lastOptionWasIgnored = true;
299 break;
300 case FieldPresence.Optional:
301 subSchema.Nullable = true;
302 break;
303 case FieldPresence.Required:
304 subSchema.Nullable = false;
305 break;
306 default:
307 throw new InvalidOperationException($"Invalid FieldPresence: {requestOption.Presence}!");
308 }
309 }
310
311 if (lastOptionWasIgnored)
312 subSchemaOwningDictionary.Remove(subSchemaId);
313 }
314 else if (responsePresence == FieldPresence.Required
315 && requestOptions.All(x => x.Presence == FieldPresence.Required && !x.PutOnly))
316 subSchema.Nullable = subSchemaId.Equals(
318 StringComparison.OrdinalIgnoreCase)
319 && rootSchemaId == nameof(TestMergeParameters); // special tactics
320
321 // otherwise, we have to assume it's a shared schema
322 // use what Swagger thinks the nullability is by default
323 }
324 }
325
331 static string GenerateSchemaId(Type type)
332 {
333 if (type == typeof(UserName))
334 return "ShallowUserResponse";
335
336 if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(PaginatedResponse<>))
337 return $"Paginated{type.GenericTypeArguments.First().Name}";
338
339 return type.Name;
340 }
341
343 public void Apply(OpenApiOperation operation, OperationFilterContext context)
344 {
345 ArgumentNullException.ThrowIfNull(operation);
346 ArgumentNullException.ThrowIfNull(context);
347
348 operation.OperationId = $"{context.MethodInfo.DeclaringType!.Name}.{context.MethodInfo.Name}";
349
350 var authAttributes = context
351 .MethodInfo
352 .DeclaringType
353 .GetCustomAttributes(true)
354 .Union(
355 context
356 .MethodInfo
357 .GetCustomAttributes(true))
358 .OfType<TgsAuthorizeAttribute>();
359
360 if (authAttributes.Any())
361 {
362 var tokenScheme = new OpenApiSecurityScheme
363 {
364 Reference = new OpenApiReference
365 {
366 Type = ReferenceType.SecurityScheme,
368 },
369 };
370
371 operation.Security = new List<OpenApiSecurityRequirement>
372 {
373 new()
374 {
375 {
376 tokenScheme,
377 new List<string>()
378 },
379 },
380 };
381
382 if (typeof(InstanceRequiredController).IsAssignableFrom(context.MethodInfo.DeclaringType))
383 operation.Parameters.Insert(0, new OpenApiParameter
384 {
385 Reference = new OpenApiReference
386 {
387 Type = ReferenceType.Parameter,
389 },
390 });
391 else if (typeof(TransferController).IsAssignableFrom(context.MethodInfo.DeclaringType))
392 if (context.MethodInfo.Name == nameof(TransferController.Upload))
393 operation.RequestBody = new OpenApiRequestBody
394 {
395 Content = new Dictionary<string, OpenApiMediaType>
396 {
397 {
398 MediaTypeNames.Application.Octet,
399 new OpenApiMediaType
400 {
401 Schema = new OpenApiSchema
402 {
403 Type = "string",
404 Format = "binary",
405 },
406 }
407 },
408 },
409 };
410 else if (context.MethodInfo.Name == nameof(TransferController.Download))
411 {
412 var twoHundredResponseContents = operation.Responses["200"].Content;
413 var fileContent = twoHundredResponseContents[MediaTypeNames.Application.Json];
414 twoHundredResponseContents.Remove(MediaTypeNames.Application.Json);
415 twoHundredResponseContents.Add(MediaTypeNames.Application.Octet, fileContent);
416 }
417 }
418 else if (context.MethodInfo.Name == nameof(ApiRootController.CreateToken))
419 {
420 var passwordScheme = new OpenApiSecurityScheme
421 {
422 Reference = new OpenApiReference
423 {
424 Type = ReferenceType.SecurityScheme,
426 },
427 };
428
429 var oAuthScheme = new OpenApiSecurityScheme
430 {
431 Reference = new OpenApiReference
432 {
433 Type = ReferenceType.SecurityScheme,
435 },
436 };
437
438 operation.Parameters.Add(new OpenApiParameter
439 {
440 In = ParameterLocation.Header,
442 Description = "The external OAuth service provider.",
443 Style = ParameterStyle.Simple,
444 Example = new OpenApiString("Discord"),
445 Schema = new OpenApiSchema
446 {
447 Type = "string",
448 },
449 });
450
451 operation.Security = new List<OpenApiSecurityRequirement>
452 {
453 new()
454 {
455 {
456 passwordScheme,
457 new List<string>()
458 },
459 {
460 oAuthScheme,
461 new List<string>()
462 },
463 },
464 };
465 }
466 }
467
469 public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
470 {
471 ArgumentNullException.ThrowIfNull(swaggerDoc);
472 ArgumentNullException.ThrowIfNull(context);
473
474 swaggerDoc.ExternalDocs = new OpenApiExternalDocs
475 {
476 Description = "API Usage Documentation",
477 Url = new Uri("https://tgstation.github.io/tgstation-server/api.html"),
478 };
479
480 swaggerDoc.Components.Parameters.Add(ApiHeaders.InstanceIdHeader, new OpenApiParameter
481 {
482 In = ParameterLocation.Header,
483 Name = ApiHeaders.InstanceIdHeader,
484 Description = "The instance ID being accessed",
485 Required = true,
486 Style = ParameterStyle.Simple,
487 Schema = new OpenApiSchema
488 {
489 Type = "integer",
490 },
491 });
492
493 var productHeaderSchema = new OpenApiSchema
494 {
495 Type = "string",
496 Format = "productheader",
497 };
498
499 swaggerDoc.Components.Parameters.Add(ApiHeaders.ApiVersionHeader, new OpenApiParameter
500 {
501 In = ParameterLocation.Header,
502 Name = ApiHeaders.ApiVersionHeader,
503 Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
504 Required = true,
505 Style = ParameterStyle.Simple,
506 Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}"),
507 Schema = productHeaderSchema,
508 });
509
510 swaggerDoc.Components.Parameters.Add(HeaderNames.UserAgent, new OpenApiParameter
511 {
512 In = ParameterLocation.Header,
513 Name = HeaderNames.UserAgent,
514 Description = "The user agent of the calling client.",
515 Required = true,
516 Style = ParameterStyle.Simple,
517 Example = new OpenApiString("Your-user-agent/1.0.0.0"),
518 Schema = productHeaderSchema,
519 });
520
521 var allSchemas = context
522 .SchemaRepository
523 .Schemas;
524 foreach (var path in swaggerDoc.Paths)
525 foreach (var operation in path.Value.Operations.Select(x => x.Value))
526 {
527 operation.Parameters.Insert(0, new OpenApiParameter
528 {
529 Reference = new OpenApiReference
530 {
531 Type = ReferenceType.Parameter,
533 },
534 });
535
536 operation.Parameters.Insert(1, new OpenApiParameter
537 {
538 Reference = new OpenApiReference
539 {
540 Type = ReferenceType.Parameter,
541 Id = HeaderNames.UserAgent,
542 },
543 });
544 }
545
546 AddDefaultResponses(swaggerDoc);
547 }
548
550 public void Apply(OpenApiSchema schema, SchemaFilterContext context)
551 {
552 ArgumentNullException.ThrowIfNull(schema);
553 ArgumentNullException.ThrowIfNull(context);
554
555 // Nothing is required
556 schema.Required.Clear();
557
558 if (context.MemberInfo == null)
559 ApplyAttributesForRootSchema(schema, context);
560
561 if (!schema.Enum?.Any() ?? false)
562 return;
563
564 // Could be nullable type, make sure to get the right one
565 Type firstGenericArgumentOrType = context.Type.IsConstructedGenericType
566 ? context.Type.GenericTypeArguments.First()
567 : context.Type;
568
569 OpenApiEnumVarNamesExtension.Apply(schema, firstGenericArgumentOrType);
570 }
571
573 public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
574 {
575 ArgumentNullException.ThrowIfNull(requestBody);
576 ArgumentNullException.ThrowIfNull(context);
577
578 requestBody.Required = true;
579 }
580 }
581}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
const string OAuthAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:54
const string InstanceIdHeader
The InstanceId header key.
Definition ApiHeaders.cs:34
static readonly Version Version
Get the version of the Api the caller is using.
Definition ApiHeaders.cs:69
const string BasicAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:49
const string ApiVersionHeader
The ApiVersion header key.
Definition ApiHeaders.cs:29
const string BearerAuthenticationScheme
The JWT authentication header scheme.
Definition ApiHeaders.cs:44
const string OAuthProviderHeader
The OAuthProvider header key.
Definition ApiHeaders.cs:39
Indicates the FieldPresence for fields in models.
Represents an error message returned by the server.
Indicates the response FieldPresence of API fields. Changes it from FieldPresence....
virtual ? string TargetCommitSha
The sha of the test merge revision to merge. If not specified, the latest commit from the source will...
Base class for user names.
Definition UserName.cs:7
Root ApiController for the Application.
ValueTask< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders.
ComponentInterfacingController for operations that require an instance.
ValueTask< IActionResult > Download([Required, FromQuery] string ticket, CancellationToken cancellationToken)
Downloads a file with a given ticket .
async ValueTask< IActionResult > Upload([Required, FromQuery] string ticket, CancellationToken cancellationToken)
Uploads a file with a given ticket .
Helper for using the AuthorizeAttribute with the Api.Rights system.
Implements the "x-enum-varnames" OpenAPI 3.0 extension.
static void Apply(OpenApiSchema openApiSchema, Type enumType)
Applies the extension to a give openApiSchema .
Implements various filters for Swashbuckle.
static string GenerateSchemaId(Type type)
Generates the OpenAPI schema ID for a given type .
void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
const string OAuthSecuritySchemeId
The OpenApiSecurityScheme name for OAuth 2.0 authentication.
const string DocumentationSiteRouteExtension
The path to the hosted documentation site.
void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
void Apply(OpenApiSchema schema, SchemaFilterContext context)
static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
Configure the swagger settings.
const string PasswordSecuritySchemeId
The OpenApiSecurityScheme name for password authentication.
const string TokenSecuritySchemeId
The OpenApiSecurityScheme name for token authentication.
const string DocumentName
The name of the swagger document.
static void AddDefaultResponses(OpenApiDocument document)
Add the default error responses to a given document .
void Apply(OpenApiOperation operation, OperationFilterContext context)
static void ApplyAttributesForRootSchema(OpenApiSchema rootSchema, SchemaFilterContext context)
Applies the OpenApiSchema.Nullable, OpenApiSchema.ReadOnly, and OpenApiSchema.WriteOnly to OpenApiSch...
FieldPresence
Indicates whether a request field is Required or Ignored.