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