From 067ed86cf4eddf2279394da51ff4f7140c7b9e05 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Sun, 29 Dec 2019 11:23:20 -0400 Subject: [PATCH 01/29] Adds parsing of port for MySQL/MariaDB databases --- src/Tgstation.Server.Host/Core/SetupWizard.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Tgstation.Server.Host/Core/SetupWizard.cs b/src/Tgstation.Server.Host/Core/SetupWizard.cs index 44fc7eb3ff..317696056b 100644 --- a/src/Tgstation.Server.Host/Core/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Core/SetupWizard.cs @@ -10,6 +10,7 @@ using System.Data.SqlClient; using System.Globalization; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Configuration; @@ -166,8 +167,18 @@ namespace Tgstation.Server.Host.Core await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false); var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); + var serverPort = 3306U; if (String.IsNullOrWhiteSpace(serverAddress)) serverAddress = null; + else + { + var m = Regex.Match(serverAddress, @"^(?.+):(?[0-9]+)$"); + if (m.Success) + { + serverAddress = m.Groups["server"].Value; + serverPort = uint.Parse(m.Groups["port"].Value, CultureInfo.InvariantCulture); + } + } await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false); @@ -239,6 +250,7 @@ namespace Tgstation.Server.Host.Core var csb = new MySqlConnectionStringBuilder { Server = serverAddress ?? "127.0.0.1", + Port = serverPort, UserID = username, Password = password }; From 121e8e83309251109025162c1a953768c8c1e22b Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Sun, 5 Jan 2020 14:33:54 -0400 Subject: [PATCH 02/29] Looping for server input, improved parsing of port --- src/Tgstation.Server.Host/Core/SetupWizard.cs | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Tgstation.Server.Host/Core/SetupWizard.cs b/src/Tgstation.Server.Host/Core/SetupWizard.cs index 317696056b..f38ea0b844 100644 --- a/src/Tgstation.Server.Host/Core/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Core/SetupWizard.cs @@ -164,21 +164,39 @@ namespace Tgstation.Server.Host.Core } while (true); - await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); - await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false); - var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); - var serverPort = 3306U; - if (String.IsNullOrWhiteSpace(serverAddress)) - serverAddress = null; - else + string serverAddress; + uint? mySQLServerPort = null; + do { - var m = Regex.Match(serverAddress, @"^(?.+):(?[0-9]+)$"); - if (m.Success) + await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); + await console.WriteAsync("Enter the server's address and port [: or ] (blank for local): ", false, cancellationToken).ConfigureAwait(false); + serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); + if (String.IsNullOrWhiteSpace(serverAddress)) { - serverAddress = m.Groups["server"].Value; - serverPort = uint.Parse(m.Groups["port"].Value, CultureInfo.InvariantCulture); + serverAddress = null; + break; } + else if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer) + { + var m = Regex.Match(serverAddress, @"^(?.+):(?.+)$"); + if (m.Success) + { + serverAddress = m.Groups["server"].Value; + if (uint.TryParse(m.Groups["port"].Value, out uint port)) + { + mySQLServerPort = port; + break; + } + else + { + await console.WriteAsync($@"Failed to parse port ""{m.Groups["port"].Value}"", please try again.", true, cancellationToken).ConfigureAwait(false); + } + } + else break; + } + else break; } + while (true); await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false); @@ -250,11 +268,13 @@ namespace Tgstation.Server.Host.Core var csb = new MySqlConnectionStringBuilder { Server = serverAddress ?? "127.0.0.1", - Port = serverPort, UserID = username, Password = password }; + if (mySQLServerPort.HasValue) + csb.Port = mySQLServerPort.Value; + CreateTestConnection(csb.ConnectionString); csb.Database = databaseName; databaseConfiguration.ConnectionString = csb.ConnectionString; From 66d991c7a4a8a7bd4274f593c188d007976bd477 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 20:15:16 -0500 Subject: [PATCH 03/29] Add support for basic authentication - Return WWW-Authenticate header in CreateToken - Support parsing out password from Authentication: basic header - Prevent users with colons from being created --- src/Tgstation.Server.Api/ApiHeaders.cs | 58 ++++++++++++++----- .../Controllers/HomeController.cs | 20 ++++++- .../Controllers/UserController.cs | 21 +++++++ 3 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index b1be91ff6f..6f6bb65a53 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Net.Http.Headers; using System.Reflection; +using System.Text; namespace Tgstation.Server.Api { @@ -23,27 +24,32 @@ namespace Tgstation.Server.Api /// /// The header key /// - const string ApiVersionHeader = "Api"; - - /// - /// The header key - /// - const string UsernameHeader = "Username"; + public const string ApiVersionHeader = "api"; /// /// The header key /// - const string InstanceIdHeader = "Instance"; + public const string InstanceIdHeader = "instance"; /// /// The JWT authentication header scheme /// - const string JwtAuthenticationScheme = "Bearer"; + public const string JwtAuthenticationScheme = "bearer"; /// - /// The password authentication header scheme + /// The JWT authentication header scheme /// - const string PasswordAuthenticationScheme = "Password"; + public const string BasicAuthenticationScheme = "basic"; + + /// + /// The header key + /// + const string UsernameHeader = "username"; + + /// + /// The basic authentication header scheme + /// + const string PasswordAuthenticationScheme = "password"; /// /// The current @@ -180,7 +186,9 @@ namespace Tgstation.Server.Api InstanceId = instanceId; } - switch (scheme) +#pragma warning disable CA1308 // Normalize strings to uppercase + switch (scheme.ToLowerInvariant()) +#pragma warning restore CA1308 // Normalize strings to uppercase { case JwtAuthenticationScheme: Token = parameter; @@ -197,6 +205,25 @@ namespace Tgstation.Server.Api if (fail) throw new InvalidOperationException("Missing Username header!"); break; + case BasicAuthenticationScheme: + string joinedString; + try + { + var base64Bytes = Convert.FromBase64String(parameter); + joinedString = Encoding.UTF8.GetString(base64Bytes); + } + catch + { + throw new InvalidOperationException("Invalid basic Authorization header!"); + } + + var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + if (basicAuthSplits.Length < 2) + throw new InvalidOperationException("Invalid basic Authorization header!"); + + Username = basicAuthSplits.First(); + Password = String.Concat(basicAuthSplits.Skip(1)); + break; default: throw new InvalidOperationException("Invalid authentication scheme!"); } @@ -241,14 +268,13 @@ namespace Tgstation.Server.Api if (IsTokenAuthentication) headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token); else - { - headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password); - headers.Add(UsernameHeader, Username); - } + headers.Authorization = new AuthenticationHeaderValue( + BasicAuthenticationScheme, + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))); headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent)); headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString()); - instanceId = instanceId ?? InstanceId; + instanceId ??= InstanceId; if (instanceId.HasValue) headers.Add(InstanceIdHeader, instanceId.ToString()); } diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index c45b51dac7..249f82f589 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using System; using System.Linq; using System.Net.Mime; @@ -118,7 +121,22 @@ namespace Tgstation.Server.Host.Controllers public async Task CreateToken(CancellationToken cancellationToken) { if (ApiHeaders == null) - return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" }); + { + // Get the exact error + var errorMessage = "Missing API headers!"; + try + { + var _ = new ApiHeaders(Request.GetTypedHeaders()); + } + catch (InvalidOperationException ex) + { + errorMessage = ex.Message; + } + + Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\"")); + + return BadRequest(new Api.Models.ErrorMessage { Message = errorMessage }); + } if (ApiHeaders.IsTokenAuthentication) return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" }); diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index e41f4f7380..d0fb649305 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -61,6 +61,18 @@ namespace Tgstation.Server.Host.Controllers generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } + /// + /// Check if a given has a valid specified. + /// + /// The to check. + /// if is valid, a otherwise. + BadRequestObjectResult CheckValidName(UserUpdate model) + { + if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture)) + return BadRequest(new ErrorMessage { Message = "Username must not contain colons!" }); + return null; + } + /// [TgsAuthorize(AdministrationRights.WriteUsers)] public override async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken) @@ -78,6 +90,10 @@ namespace Tgstation.Server.Host.Controllers if (!(model.Name == null ^ model.SystemIdentifier == null)) return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" }); + var fail = CheckValidName(model); + if (fail != null) + return fail; + var dbUser = new Models.User { AdministrationRights = model.AdministrationRights ?? AdministrationRights.None, @@ -154,6 +170,11 @@ namespace Tgstation.Server.Host.Controllers originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights; originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights; originalUser.Enabled = model.Enabled ?? originalUser.Enabled; + + var fail = CheckValidName(model); + if (fail != null) + return fail; + originalUser.Name = model.Name ?? originalUser.Name; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); From f2ec20484e4d180f01bce998a44ab3c08e4075a2 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 20:17:32 -0500 Subject: [PATCH 04/29] Provide the relevant RightsType in TgsAuthorizeAttributes --- .../Controllers/TgsAuthorizeAttribute.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs index 0d52ac6ae1..4501a9783f 100644 --- a/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs +++ b/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs @@ -11,6 +11,11 @@ namespace Tgstation.Server.Host.Controllers [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] sealed class TgsAuthorizeAttribute : AuthorizeAttribute { + /// + /// Gets the associated with the if any. + /// + public RightsType? RightsType { get; } + /// /// Construct a /// @@ -23,6 +28,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(AdministrationRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.Administration; } /// @@ -32,6 +38,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(InstanceManagerRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.InstanceManager; } /// @@ -41,6 +48,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(RepositoryRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.Repository; } /// @@ -50,6 +58,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(ByondRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.Byond; } /// @@ -59,6 +68,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(DreamMakerRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.DreamMaker; } /// @@ -68,6 +78,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(DreamDaemonRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.DreamDaemon; } /// @@ -77,6 +88,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(ChatBotRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.ChatBots; } /// @@ -86,6 +98,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(ConfigurationRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.Configuration; } /// @@ -95,6 +108,7 @@ namespace Tgstation.Server.Host.Controllers public TgsAuthorizeAttribute(InstanceUserRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); + RightsType = Api.Rights.RightsType.InstanceUser; } } } From 9ca39c9ad0f839a74087f762b1ba57f03408b80d Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 20:17:56 -0500 Subject: [PATCH 05/29] Make RightsHelper use its own helper --- src/Tgstation.Server.Api/Rights/RightsHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs index 6df923331e..52947aaa6d 100644 --- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs +++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs @@ -60,7 +60,7 @@ namespace Tgstation.Server.Api.Rights /// A representing the claim role name public static string RoleName(RightsType rightsType, Enum right) { - var enumType = TypeMap[rightsType]; + var enumType = RightToType(rightsType); return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right)); } From b53562a66735bc66233bb5d6c2d07e34a4f6f3fb Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 20:29:14 -0500 Subject: [PATCH 06/29] Remove developer exception page in production - Replace it with an ErrorMessage payload - Adjust the client to account for this --- src/Tgstation.Server.Client/ApiClient.cs | 6 +++- .../ServerErrorException.cs | 17 +++------ src/Tgstation.Server.Host/Core/Application.cs | 5 ++- .../Core/ApplicationBuilderExtensions.cs | 35 +++++++++++++++++++ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 3d01a67871..b6ae18e747 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -112,7 +112,11 @@ namespace Tgstation.Server.Client throw new MethodNotSupportedException(); case HttpStatusCode.InternalServerError: // response json is html - throw new ServerErrorException(json); + throw new ServerErrorException(errorMessage ?? new ErrorMessage + { + Message = "An internal server error occurred!", + SeverApiVersion = null + }, response.StatusCode); case (HttpStatusCode)429: // rate limited response.Headers.TryGetValues("Retry-After", out var values); diff --git a/src/Tgstation.Server.Client/ServerErrorException.cs b/src/Tgstation.Server.Client/ServerErrorException.cs index 940f3513a7..24794c8027 100644 --- a/src/Tgstation.Server.Client/ServerErrorException.cs +++ b/src/Tgstation.Server.Client/ServerErrorException.cs @@ -9,27 +9,18 @@ namespace Tgstation.Server.Client /// public sealed class ServerErrorException : ClientException { - /// - /// The raw HTML of the error - /// - public string Html { get; } - /// /// Construct an /// public ServerErrorException() { } /// - /// Construct an with + /// Construct an with a given /// - /// The raw HTML response of the - public ServerErrorException(string html) : base(new ErrorMessage + /// The for the + /// The for the + public ServerErrorException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode) { - Message = "An internal server error occurred!", - SeverApiVersion = null - }, HttpStatusCode.InternalServerError) - { - Html = html; } /// diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 4f5438ca5f..61e70fe05d 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -351,9 +351,12 @@ namespace Tgstation.Server.Host.Core ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart()); // setup the HTTP request pipeline + // Final point where we wrap exceptions in a 500 (ErrorMessage) response + applicationBuilder.UseServerErrorHandling(); // should anything after this throw an exception, catch it and display a detailed html page - applicationBuilder.UseDeveloperExceptionPage(); // it is not worth it to limit this, you should only ever get it if you're an authorized user + if(hostingEnvironment.IsDevelopment()) + applicationBuilder.UseDeveloperExceptionPage(); // it is not worth it to limit this, you should only ever get it if you're an authorized user // suppress OperationCancelledExceptions, they are just aborted HTTP requests applicationBuilder.UseCancelledRequestSuppression(); diff --git a/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs index 971d018f73..a9290b8c01 100644 --- a/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Globalization; +using System.Net; using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Core @@ -69,5 +70,39 @@ namespace Tgstation.Server.Host.Core } }); } + + /// + /// Suppress all in flight exceptions with error 500. + /// + /// The to configure + public static void UseServerErrorHandling(this IApplicationBuilder applicationBuilder) + { + if (applicationBuilder == null) + throw new ArgumentNullException(nameof(applicationBuilder)); + applicationBuilder.Use(async (context, next) => + { + var logger = GetLogger(context); + try + { + await next().ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogError("Failed request: {0}", e); + await new ObjectResult( + new ErrorMessage + { + Message = $"A unhandled exception has occurred: {e}" + }) + { + StatusCode = (int)HttpStatusCode.InternalServerError + } + .ExecuteResultAsync(new ActionContext + { + HttpContext = context + }).ConfigureAwait(false); + } + }); + } } } From 858ae383a0aa04cd1f988489682a377c9b35f7d5 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 20:44:17 -0500 Subject: [PATCH 07/29] Readd ApiController attribute - Change data annotation validation workaround to hit at the cause. --- .../Controllers/ApiController.cs | 23 +++-------------- src/Tgstation.Server.Host/Core/Application.cs | 25 ++++++++++++------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 9565072343..c5804466aa 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -18,6 +18,7 @@ namespace Tgstation.Server.Host.Controllers /// A for API functions /// [Produces(ApiHeaders.ApplicationJson)] + [ApiController] public abstract class ApiController : Controller { /// @@ -129,25 +130,9 @@ namespace Tgstation.Server.Host.Controllers if (ModelState?.IsValid == false) { - var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList(); - - // HACK - // do some fuckery to remove RequiredAttribute errors - for (var I = 0; I < errorMessages.Count; ++I) - { - var message = errorMessages[I]; - if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal)) - { - errorMessages.RemoveAt(I); - --I; - } - } - - if (errorMessages.Count > 0) - { - await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false); - return; - } + var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage); + await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false); + return; } if (ApiHeaders != null) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 61e70fe05d..e1d67a99ff 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -19,6 +19,7 @@ using Serilog.Formatting.Display; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using Tgstation.Server.Host.Components; @@ -221,15 +222,21 @@ namespace Tgstation.Server.Host.Core JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // add mvc, configure the json serializer settings - services.AddMvc().AddJsonOptions(options => - { - options.AllowInputFormatterExceptionMessages = true; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - options.SerializerSettings.CheckAdditionalContent = true; - options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error; - options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - options.SerializerSettings.Converters = new[] { new VersionConverter() }; - }); + services + .AddMvc(options => + { + var dataAnnotationValidator = options.ModelValidatorProviders.Single(validator => validator.GetType().Name == "DataAnnotationsModelValidatorProvider"); + options.ModelValidatorProviders.Remove(dataAnnotationValidator); + }) + .AddJsonOptions(options => + { + options.AllowInputFormatterExceptionMessages = true; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.CheckAdditionalContent = true; + options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error; + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.Converters = new[] { new VersionConverter() }; + }); // enable browser detection services.AddDetectionCore().AddBrowser(); From 4da75aa4ff452397db9ae73cfba37ba2ffd08f62 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 21:05:44 -0500 Subject: [PATCH 08/29] Add ProducesResponseType for all controller actions - Excluding common status codes which will be documented elsewhere --- .../Controllers/AdministrationController.cs | 6 +++++ .../Controllers/ByondController.cs | 5 ++++ .../Controllers/ChatController.cs | 7 +++++ .../Controllers/ConfigurationController.cs | 27 +++++++++++++++++++ .../Controllers/DreamDaemonController.cs | 8 ++++++ .../Controllers/DreamMakerController.cs | 9 +++++++ .../Controllers/HomeController.cs | 4 +++ .../Controllers/InstanceController.cs | 13 +++++++-- .../Controllers/InstanceUserController.cs | 10 +++++++ .../Controllers/JobController.cs | 8 ++++++ .../Controllers/RepositoryController.cs | 9 ++++++- .../Controllers/UserController.cs | 9 +++++++ 12 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 61198121f6..0cdf62a0a2 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -154,6 +154,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(typeof(Administration), 200)] + [ProducesResponseType(424)] + [ProducesResponseType(429)] public override async Task Read(CancellationToken cancellationToken) { try @@ -198,6 +201,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(AdministrationRights.ChangeVersion)] + [ProducesResponseType(typeof(ErrorMessage), 422)] public override async Task Update([FromBody] Administration model, CancellationToken cancellationToken) { if (model == null) @@ -224,6 +228,8 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the request [HttpDelete] [TgsAuthorize(AdministrationRights.RestartHost)] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ErrorMessage), 422)] public async Task Delete() { try diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 10229aaef1..717fd136bb 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; @@ -47,6 +48,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ByondRights.ReadActive)] + [ProducesResponseType(typeof(Api.Models.Byond), 200)] public override Task Read(CancellationToken cancellationToken) => Task.FromResult( Json(new Api.Models.Byond { @@ -55,6 +57,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ByondRights.ListInstalled)] + [ProducesResponseType(typeof(IEnumerable), 200)] public override Task List(CancellationToken cancellationToken) => Task.FromResult( Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond { @@ -63,6 +66,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ByondRights.ChangeVersion)] + [ProducesResponseType(typeof(Api.Models.Byond), 200)] + [ProducesResponseType(typeof(Api.Models.Byond), 202)] public override async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) { if (model == null) diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 26ac8bc834..695fb0cd03 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -59,6 +59,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ChatBotRights.Create)] + [ProducesResponseType(typeof(Api.Models.ChatBot), 201)] public override async Task Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken) { if (model == null) @@ -131,6 +132,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ChatBotRights.Delete)] + [ProducesResponseType(200)] public override async Task Delete(long id, CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); @@ -141,6 +143,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ChatBotRights.Read)] + [ProducesResponseType(typeof(IEnumerable), 200)] public override async Task List(CancellationToken cancellationToken) { var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels); @@ -158,6 +161,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ChatBotRights.Read)] + [ProducesResponseType(typeof(Api.Models.ChatBot), 200)] + [ProducesResponseType(410)] public override async Task GetId(long id, CancellationToken cancellationToken) { var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels); @@ -177,6 +182,8 @@ namespace Tgstation.Server.Host.Controllers /// #pragma warning disable CA1506 // TODO: Decomplexify [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(Api.Models.ChatBot), 200)] public override async Task Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken) { if (model == null) diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 49bbb78e67..b2849e30b7 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Threading; @@ -65,6 +66,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ConfigurationRights.Write)] + [ProducesResponseType(typeof(ConfigurationFile), 200)] + [ProducesResponseType(typeof(ConfigurationFile), 201)] + [ProducesResponseType(501)] public override async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken) { if (model == null) @@ -108,6 +112,9 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the for the operation [HttpGet(Routes.File + "/{*filePath}")] [TgsAuthorize(ConfigurationRights.Read)] + [ProducesResponseType(typeof(ConfigurationFile), 200)] + [ProducesResponseType(410)] + [ProducesResponseType(501)] public async Task File(string filePath, CancellationToken cancellationToken) { if (ForbidDueToModeConflicts(filePath, out var systemIdentity)) @@ -143,6 +150,9 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the for the operation [HttpGet("List/{*directoryPath}")] [TgsAuthorize(ConfigurationRights.List)] + [ProducesResponseType(typeof(IReadOnlyList), 200)] + [ProducesResponseType(410)] + [ProducesResponseType(501)] public async Task Directory(string directoryPath, CancellationToken cancellationToken) { if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity)) @@ -168,10 +178,17 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(ConfigurationRights.List)] + [ProducesResponseType(typeof(IReadOnlyList), 200)] + [ProducesResponseType(410)] + [ProducesResponseType(501)] public override Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken); /// [TgsAuthorize(ConfigurationRights.Write)] + [ProducesResponseType(typeof(ConfigurationFile), 200)] + [ProducesResponseType(typeof(ConfigurationFile), 201)] + [ProducesResponseType(410)] + [ProducesResponseType(501)] public override async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken) { if (model == null) @@ -185,6 +202,14 @@ namespace Tgstation.Server.Host.Controllers model.IsDirectory = true; return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model); } + catch (IOException e) + { + Logger.LogInformation("IOException while creating directory {0}: {1}", model.Path, e); + return Conflict(new ErrorMessage + { + Message = e.Message + }); + } catch (NotImplementedException) { return StatusCode((int)HttpStatusCode.NotImplemented); @@ -203,6 +228,8 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation [HttpDelete] [TgsAuthorize(ConfigurationRights.Delete)] + [ProducesResponseType(200)] + [ProducesResponseType(501)] public async Task Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken) { if (directory == null) diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 3a33d10457..06994df6b6 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -51,6 +51,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamDaemonRights.Start)] + [ProducesResponseType(typeof(Api.Models.Job), 202)] + [ProducesResponseType(410)] public override async Task Create([FromBody] DreamDaemon model, CancellationToken cancellationToken) { // alias for launching DD @@ -73,6 +75,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)] + [ProducesResponseType(typeof(DreamDaemon), 200)] + [ProducesResponseType(410)] public override Task Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken); /// @@ -134,6 +138,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation [HttpDelete] [TgsAuthorize(DreamDaemonRights.Shutdown)] + [ProducesResponseType(200)] public async Task Delete(CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); @@ -144,6 +149,8 @@ namespace Tgstation.Server.Host.Controllers /// #pragma warning disable CA1506 // TODO: Decomplexify [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)] + [ProducesResponseType(typeof(DreamDaemon), 200)] + [ProducesResponseType(410)] public override async Task Update([FromBody] DreamDaemon model, CancellationToken cancellationToken) { if (model == null) @@ -219,6 +226,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the request [HttpPatch] [TgsAuthorize(DreamDaemonRights.Restart)] + [ProducesResponseType(typeof(Api.Models.Job), 202)] public async Task Restart(CancellationToken cancellationToken) { var job = new Models.Job diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index f9d0a57451..af71b78151 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; @@ -49,6 +50,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamMakerRights.Read)] + [ProducesResponseType(typeof(DreamMaker), 200)] public override async Task Read(CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); @@ -58,6 +60,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamMakerRights.CompileJobs)] + [ProducesResponseType(typeof(Api.Models.CompileJob), 200)] + [ProducesResponseType(404)] public override async Task GetId(long id, CancellationToken cancellationToken) { var compileJob = await DatabaseContext.CompileJobs @@ -73,6 +77,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamMakerRights.CompileJobs)] + [ProducesResponseType(typeof(List), 200)] public override async Task List(CancellationToken cancellationToken) { var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob @@ -84,6 +89,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamMakerRights.Compile)] + [ProducesResponseType(typeof(Api.Models.Job), 202)] public override async Task Create([FromBody] DreamMaker model, CancellationToken cancellationToken) { var job = new Models.Job @@ -100,6 +106,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)] + [ProducesResponseType(typeof(DreamMaker), 200)] + [ProducesResponseType(200)] + [ProducesResponseType(410)] public override async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken) { if (model.ApiValidationPort == 0) diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 249f82f589..d5e61a062b 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -92,6 +92,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize] [AllowAnonymous] [HttpGet] + [ProducesResponseType(typeof(Api.Models.ServerInformation), 200)] public IActionResult Home() { if (AuthenticationContext != null) @@ -117,6 +118,9 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation /// A resulting in the of the operation [HttpPost] + [ProducesResponseType(typeof(Api.Models.Token), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] #pragma warning disable CA1506 // TODO: Decomplexify public async Task CreateToken(CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 22419b3be2..c02dd7d936 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -110,6 +110,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceManagerRights.Create)] + [ProducesResponseType(typeof(Api.Models.Instance), 200)] + [ProducesResponseType(typeof(Api.Models.Instance), 201)] public override async Task Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { if (model == null) @@ -130,7 +132,7 @@ namespace Tgstation.Server.Host.Controllers }, out var normalizedLocalPath); if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal)) - return Conflict("Instances cannot be created in the installation directory!"); + return Conflict(new ErrorMessage { Message = "Instances cannot be created in the installation directory!" }); var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); bool attached = false; @@ -216,6 +218,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceManagerRights.Delete)] + [ProducesResponseType(200)] + [ProducesResponseType(410)] public override async Task Delete(long id, CancellationToken cancellationToken) { var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id) @@ -250,7 +254,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)] - #pragma warning disable CA1502 // TODO: Decomplexify + [ProducesResponseType(typeof(Api.Models.Instance), 200)] + [ProducesResponseType(410)] +#pragma warning disable CA1502 // TODO: Decomplexify public override async Task Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id); @@ -396,6 +402,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)] + [ProducesResponseType(typeof(IEnumerable), 200)] public override async Task List(CancellationToken cancellationToken) { IQueryable query = DatabaseContext.Instances; @@ -420,6 +427,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)] + [ProducesResponseType(typeof(Api.Models.Instance), 200)] + [ProducesResponseType(410)] public override async Task GetId(long id, CancellationToken cancellationToken) { var query = DatabaseContext.Instances.Where(x => x.Id == id); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 67a4d0189d..2217e8f0e9 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; @@ -48,6 +49,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceUserRights.CreateUsers)] + [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)] public override async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { var test = StandardModelChecks(model); @@ -75,6 +77,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceUserRights.WriteUsers)] + [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] + [ProducesResponseType(410)] #pragma warning disable CA1506 // TODO: Decomplexify public override async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { @@ -104,10 +108,13 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] + [ProducesResponseType(404)] public override Task Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound()); /// [TgsAuthorize(InstanceUserRights.ReadUsers)] + [ProducesResponseType(typeof(IEnumerable), 200)] public override async Task List(CancellationToken cancellationToken) { var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false); @@ -116,6 +123,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceUserRights.ReadUsers)] + [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] + [ProducesResponseType(410)] public override async Task GetId(long id, CancellationToken cancellationToken) { // this functions as userId @@ -127,6 +136,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(InstanceUserRights.WriteUsers)] + [ProducesResponseType(200)] public override async Task Delete(long id, CancellationToken cancellationToken) { await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index e5c08bbc99..c305597c4b 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; @@ -38,6 +39,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(typeof(IEnumerable), 200)] public override async Task Read(CancellationToken cancellationToken) { var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false); @@ -46,6 +48,7 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(typeof(List), 200)] public override async Task List(CancellationToken cancellationToken) { // you KNOW this will need pagination eventually right? @@ -58,6 +61,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(202)] + [ProducesResponseType(404)] + [ProducesResponseType(410)] public override async Task Delete(long id, CancellationToken cancellationToken) { // don't care if an instance post or not at this point @@ -77,6 +83,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(404)] + [ProducesResponseType(typeof(Api.Models.Job), 200)] public override async Task GetId(long id, CancellationToken cancellationToken) { var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 69d0d6417b..f08fa51e9d 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -128,6 +128,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(RepositoryRights.SetOrigin)] + [ProducesResponseType(typeof(Repository), 201)] + [ProducesResponseType(410)] public override async Task Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) @@ -219,6 +221,8 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation /// A resulting in the of the operation [TgsAuthorize(RepositoryRights.Delete)] + [ProducesResponseType(typeof(Repository), 202)] + [ProducesResponseType(410)] public async Task Delete(CancellationToken cancellationToken) { var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -247,6 +251,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(RepositoryRights.Read)] + [ProducesResponseType(typeof(Repository), 200)] + [ProducesResponseType(typeof(Repository), 201)] + [ProducesResponseType(410)] public override async Task Read(CancellationToken cancellationToken) { var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -273,7 +280,7 @@ namespace Tgstation.Server.Host.Controllers { if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false)) { - // user may have fucked with the repo without telling us, do what we can + // user may have fucked with the repo manually, do what we can await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return StatusCode((int)HttpStatusCode.Created, api); } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index d0fb649305..ebe9aaf2b3 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -75,6 +75,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(AdministrationRights.WriteUsers)] + [ProducesResponseType(typeof(Api.Models.User), 201)] + [ProducesResponseType(410)] + [ProducesResponseType(501)] public override async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) @@ -139,6 +142,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] + [ProducesResponseType(typeof(Api.Models.User), 200)] + [ProducesResponseType(404)] public override async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) @@ -187,10 +192,12 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(typeof(Api.Models.User), 200)] public override Task Read(CancellationToken cancellationToken) => Task.FromResult(Json(AuthenticationContext.User.ToApi(true))); /// [TgsAuthorize(AdministrationRights.ReadUsers)] + [ProducesResponseType(typeof(IEnumerable), 200)] public override async Task List(CancellationToken cancellationToken) { var users = await DatabaseContext.Users @@ -201,6 +208,8 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize] + [ProducesResponseType(typeof(Api.Models.User), 200)] + [ProducesResponseType(404)] public override async Task GetId(long id, CancellationToken cancellationToken) { if (id == AuthenticationContext.User.Id) From f1fe846ce6b32ea58b1a53ede900736bc9215b45 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 21:32:10 -0500 Subject: [PATCH 09/29] Removes the mistake that was ModelController - Controllers inherit ApiController - Add now missing Http verb attributes - Added response comments for methods with documentation - Renamed API List function to ListRoute --- src/Tgstation.Server.Api/Routes.cs | 7 +- .../Components/ByondClient.cs | 2 +- .../Components/ChatBotsClient.cs | 2 +- .../Components/ConfigurationClient.cs | 2 +- .../Components/DreamMakerClient.cs | 2 +- .../Components/InstanceUserClient.cs | 2 +- .../Components/JobsClient.cs | 2 +- .../InstanceManagerClient.cs | 2 +- src/Tgstation.Server.Client/UsersClient.cs | 2 +- .../Controllers/AdministrationController.cs | 14 ++-- .../Controllers/ByondController.cs | 16 ++-- .../Controllers/ChatController.cs | 28 +++---- .../Controllers/ConfigurationController.cs | 26 ++++--- .../Controllers/DreamDaemonController.cs | 26 ++++--- .../Controllers/DreamMakerController.cs | 26 +++---- .../Controllers/HomeController.cs | 13 +++- .../Controllers/InstanceController.cs | 26 +++---- .../Controllers/InstanceUserController.cs | 30 ++++---- .../Controllers/JobController.cs | 22 +++--- .../Controllers/ModelController.cs | 77 ------------------- .../Controllers/RepositoryController.cs | 26 ++++--- .../Controllers/UserController.cs | 26 +++---- 22 files changed, 165 insertions(+), 214 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Controllers/ModelController.cs diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index 2ba4b4b19b..6a2da6c8f0 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -78,6 +78,11 @@ namespace Tgstation.Server.Api /// public const string Jobs = Root + nameof(Models.Job); + /// + /// The postfix for list operations + /// + public const string List = "List"; + /// /// Apply an postfix to a /// @@ -91,6 +96,6 @@ namespace Tgstation.Server.Api /// /// The route /// The with /List appended - public static string List(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/List", route); + public static string ListRoute(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/{1}", route, List); } } diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index f3675dc265..23d56dc64a 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -35,7 +35,7 @@ namespace Tgstation.Server.Client.Components public Task ActiveVersion(CancellationToken cancellationToken) => apiClient.Read(Routes.Byond, instance.Id, cancellationToken); /// - public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Byond), instance.Id, cancellationToken); + public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken); /// public Task SetActiveVersion(Byond byond, CancellationToken cancellationToken) => apiClient.Update(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs index f7890f1f22..5a05e8c930 100644 --- a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs +++ b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Chat), instance.Id, cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken); /// public Task Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index 54dc6acf39..4063e73963 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -52,7 +52,7 @@ namespace Tgstation.Server.Client.Components public Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create(Routes.Configuration, directory, instance.Id, cancellationToken); /// - public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken); + public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken); /// public Task Read(ConfigurationFile file, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs index 942d252812..d08c587194 100644 --- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components public Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken); /// - public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.DreamMaker), instance.Id, cancellationToken); + public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken); /// public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs index 440340b6c4..cc9757f00b 100644 --- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs +++ b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs @@ -44,7 +44,7 @@ namespace Tgstation.Server.Client.Components public Task Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.InstanceUser), instance.Id, cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken); /// public Task GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs index d6c421e65d..d673abfc41 100644 --- a/src/Tgstation.Server.Client/Components/JobsClient.cs +++ b/src/Tgstation.Server.Client/Components/JobsClient.cs @@ -35,7 +35,7 @@ namespace Tgstation.Server.Client.Components public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Jobs), instance.Id, cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken); /// public Task> ListActive(CancellationToken cancellationToken) => apiClient.Read>(Routes.Jobs, instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/InstanceManagerClient.cs b/src/Tgstation.Server.Client/InstanceManagerClient.cs index 62d99de98d..aa5458f0e6 100644 --- a/src/Tgstation.Server.Client/InstanceManagerClient.cs +++ b/src/Tgstation.Server.Client/InstanceManagerClient.cs @@ -39,7 +39,7 @@ namespace Tgstation.Server.Client public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.InstanceManager), cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstanceManager), cancellationToken); /// public Task Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken); diff --git a/src/Tgstation.Server.Client/UsersClient.cs b/src/Tgstation.Server.Client/UsersClient.cs index 1e6c5fd45e..9d4aa53de6 100644 --- a/src/Tgstation.Server.Client/UsersClient.cs +++ b/src/Tgstation.Server.Client/UsersClient.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Client public Task GetId(User user, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.User), cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.User), cancellationToken); /// public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.User, cancellationToken); diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 0cdf62a0a2..0914dc5d78 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -22,10 +22,10 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// for + /// for purposes /// [Route(Routes.Administration)] - public sealed class AdministrationController : ModelController + public sealed class AdministrationController : ApiController { const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!"; @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The containing value of /// The containing value of - public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger logger, IOptions updatesConfigurationOptions, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false) + public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger logger, IOptions updatesConfigurationOptions, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true) { this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); @@ -152,12 +152,12 @@ namespace Tgstation.Server.Host.Controllers IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken); - /// + [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(Administration), 200)] [ProducesResponseType(424)] [ProducesResponseType(429)] - public override async Task Read(CancellationToken cancellationToken) + public async Task Read(CancellationToken cancellationToken) { try { @@ -199,10 +199,10 @@ namespace Tgstation.Server.Host.Controllers } } - /// + [HttpPost] [TgsAuthorize(AdministrationRights.ChangeVersion)] [ProducesResponseType(typeof(ErrorMessage), 422)] - public override async Task Update([FromBody] Administration model, CancellationToken cancellationToken) + public async Task Update([FromBody] Administration model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 717fd136bb..7b7e11bc36 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Controllers /// Controller for managing s /// [Route(Routes.Byond)] - public sealed class ByondController : ModelController + public sealed class ByondController : ApiController { /// /// The for the @@ -40,35 +40,35 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The for the - public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } - /// + [HttpGet] [TgsAuthorize(ByondRights.ReadActive)] [ProducesResponseType(typeof(Api.Models.Byond), 200)] - public override Task Read(CancellationToken cancellationToken) => Task.FromResult( + public Task Read(CancellationToken cancellationToken) => Task.FromResult( Json(new Api.Models.Byond { Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion })); - /// + [HttpGet(Routes.List)] [TgsAuthorize(ByondRights.ListInstalled)] [ProducesResponseType(typeof(IEnumerable), 200)] - public override Task List(CancellationToken cancellationToken) => Task.FromResult( + public Task List(CancellationToken cancellationToken) => Task.FromResult( Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond { Version = x }))); - /// + [HttpPost] [TgsAuthorize(ByondRights.ChangeVersion)] [ProducesResponseType(typeof(Api.Models.Byond), 200)] [ProducesResponseType(typeof(Api.Models.Byond), 202)] - public override async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) + public async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 695fb0cd03..a83ed01dda 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -20,10 +20,10 @@ using Z.EntityFramework.Plus; namespace Tgstation.Server.Host.Controllers { /// - /// for managing s + /// for managing s /// [Route(Routes.Chat)] - public sealed class ChatController : ModelController + public sealed class ChatController : ApiController { /// /// The for the @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The value of /// The for the - public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } @@ -57,10 +57,10 @@ namespace Tgstation.Server.Host.Controllers Tag = api.Tag }; - /// + [HttpPut] [TgsAuthorize(ChatBotRights.Create)] [ProducesResponseType(typeof(Api.Models.ChatBot), 201)] - public override async Task Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken) + public async Task Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -130,10 +130,10 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi()); } - /// + [HttpDelete] [TgsAuthorize(ChatBotRights.Delete)] [ProducesResponseType(200)] - public override async Task Delete(long id, CancellationToken cancellationToken) + public async Task Delete(long id, CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false); @@ -141,10 +141,10 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } - /// + [HttpGet(Routes.List)] [TgsAuthorize(ChatBotRights.Read)] [ProducesResponseType(typeof(IEnumerable), 200)] - public override async Task List(CancellationToken cancellationToken) + public async Task List(CancellationToken cancellationToken) { var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels); @@ -159,11 +159,11 @@ namespace Tgstation.Server.Host.Controllers return Json(results.Select(x => x.ToApi())); } - /// + [HttpGet("{id}")] [TgsAuthorize(ChatBotRights.Read)] [ProducesResponseType(typeof(Api.Models.ChatBot), 200)] [ProducesResponseType(410)] - public override async Task GetId(long id, CancellationToken cancellationToken) + public async Task GetId(long id, CancellationToken cancellationToken) { var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels); @@ -179,12 +179,12 @@ namespace Tgstation.Server.Host.Controllers return Json(results.ToApi()); } - /// - #pragma warning disable CA1506 // TODO: Decomplexify + [HttpGet] [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)] [ProducesResponseType(200)] [ProducesResponseType(typeof(Api.Models.ChatBot), 200)] - public override async Task Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken) + #pragma warning disable CA1506 // TODO: Decomplexify + public async Task Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index b2849e30b7..9144c9677a 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -17,10 +17,10 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// The for s + /// The for s /// [Route(Routes.Configuration)] - public sealed class ConfigurationController : ModelController + public sealed class ConfigurationController : ApiController { /// /// The for the @@ -40,7 +40,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The for the - public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -64,12 +64,12 @@ namespace Tgstation.Server.Host.Controllers return false; } - /// + [HttpPost] [TgsAuthorize(ConfigurationRights.Write)] [ProducesResponseType(typeof(ConfigurationFile), 200)] [ProducesResponseType(typeof(ConfigurationFile), 201)] [ProducesResponseType(501)] - public override async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken) + public async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -110,6 +110,8 @@ namespace Tgstation.Server.Host.Controllers /// The path of the file to get /// The for the operation /// A resulting in the for the operation + /// File not found on disk. + /// POSIX system impersonation requested but not implemented. [HttpGet(Routes.File + "/{*filePath}")] [TgsAuthorize(ConfigurationRights.Read)] [ProducesResponseType(typeof(ConfigurationFile), 200)] @@ -148,7 +150,9 @@ namespace Tgstation.Server.Host.Controllers /// The path of the directory to get /// The for the operation /// A resulting in the for the operation - [HttpGet("List/{*directoryPath}")] + /// Directory not found on disk. + /// POSIX system impersonation requested but not implemented. + [HttpGet(Routes.List + "/{*directoryPath}")] [TgsAuthorize(ConfigurationRights.List)] [ProducesResponseType(typeof(IReadOnlyList), 200)] [ProducesResponseType(410)] @@ -176,20 +180,20 @@ namespace Tgstation.Server.Host.Controllers } } - /// + [HttpGet(Routes.List)] [TgsAuthorize(ConfigurationRights.List)] [ProducesResponseType(typeof(IReadOnlyList), 200)] [ProducesResponseType(410)] [ProducesResponseType(501)] - public override Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken); + public Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken); - /// + [HttpPut] [TgsAuthorize(ConfigurationRights.Write)] [ProducesResponseType(typeof(ConfigurationFile), 200)] [ProducesResponseType(typeof(ConfigurationFile), 201)] [ProducesResponseType(410)] [ProducesResponseType(501)] - public override async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken) + public async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -226,6 +230,8 @@ namespace Tgstation.Server.Host.Controllers /// A representing the path to the directory to delete /// The for the operation /// A resulting in the of the operation + /// Empty directory deleted successfully. + /// POSIX system impersonation requested but not implemented. [HttpDelete] [TgsAuthorize(ConfigurationRights.Delete)] [ProducesResponseType(200)] diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 06994df6b6..ca86960519 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -20,10 +20,10 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// for managing the + /// for managing the /// [Route(Routes.DreamDaemon)] - public sealed class DreamDaemonController : ModelController + public sealed class DreamDaemonController : ApiController { /// /// The for the @@ -43,17 +43,17 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The for the - public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } - /// + [HttpPut] [TgsAuthorize(DreamDaemonRights.Start)] [ProducesResponseType(typeof(Api.Models.Job), 202)] [ProducesResponseType(410)] - public override async Task Create([FromBody] DreamDaemon model, CancellationToken cancellationToken) + public async Task Create([FromBody] DreamDaemon model, CancellationToken cancellationToken) { // alias for launching DD var instance = instanceManager.GetInstance(Instance); @@ -73,11 +73,11 @@ namespace Tgstation.Server.Host.Controllers return Accepted(job.ToApi()); } - /// + [HttpGet] [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)] [ProducesResponseType(typeof(DreamDaemon), 200)] [ProducesResponseType(410)] - public override Task Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken); + public Task Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken); /// /// Implementation of @@ -132,10 +132,11 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Stops DreamDaemon if it's running + /// Stops the Watchdog if it's running /// /// The for the operation /// A resulting in the of the operation + /// Watchdog terminated. [HttpDelete] [TgsAuthorize(DreamDaemonRights.Shutdown)] [ProducesResponseType(200)] @@ -146,12 +147,12 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } - /// - #pragma warning disable CA1506 // TODO: Decomplexify + [HttpPost] [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)] [ProducesResponseType(typeof(DreamDaemon), 200)] [ProducesResponseType(410)] - public override async Task Update([FromBody] DreamDaemon model, CancellationToken cancellationToken) + #pragma warning disable CA1506 // TODO: Decomplexify + public async Task Update([FromBody] DreamDaemon model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -220,10 +221,11 @@ namespace Tgstation.Server.Host.Controllers #pragma warning restore CA1506 /// - /// Handle a HTTP PATCH to the + /// Creates a to restart the Watchdog. It will start if it wasn't already running. /// /// The for the operation /// A resulting in the of the request + /// Job started successfully. [HttpPatch] [TgsAuthorize(DreamDaemonRights.Restart)] [ProducesResponseType(typeof(Api.Models.Job), 202)] diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index af71b78151..1d45c7132b 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -18,11 +18,11 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// Controller for managing the compiler + /// for managing the deployment system. /// [Route(Routes.DreamMaker)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class DreamMakerController : ModelController + public sealed class DreamMakerController : ApiController { /// /// The for the @@ -42,27 +42,27 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The for the - public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } - /// + [HttpGet] [TgsAuthorize(DreamMakerRights.Read)] [ProducesResponseType(typeof(DreamMaker), 200)] - public override async Task Read(CancellationToken cancellationToken) + public async Task Read(CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); return Json(dreamMakerSettings.ToApi()); } - /// + [HttpGet("{id}")] [TgsAuthorize(DreamMakerRights.CompileJobs)] [ProducesResponseType(typeof(Api.Models.CompileJob), 200)] [ProducesResponseType(404)] - public override async Task GetId(long id, CancellationToken cancellationToken) + public async Task GetId(long id, CancellationToken cancellationToken) { var compileJob = await DatabaseContext.CompileJobs .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id) @@ -75,10 +75,10 @@ namespace Tgstation.Server.Host.Controllers return Json(compileJob.ToApi()); } - /// + [HttpGet(Routes.List)] [TgsAuthorize(DreamMakerRights.CompileJobs)] [ProducesResponseType(typeof(List), 200)] - public override async Task List(CancellationToken cancellationToken) + public async Task List(CancellationToken cancellationToken) { var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob { @@ -87,10 +87,10 @@ namespace Tgstation.Server.Host.Controllers return Json(compileJobs); } - /// + [HttpPut] [TgsAuthorize(DreamMakerRights.Compile)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public override async Task Create([FromBody] DreamMaker model, CancellationToken cancellationToken) + public async Task Create([FromBody] DreamMaker model, CancellationToken cancellationToken) { var job = new Models.Job { @@ -104,12 +104,12 @@ namespace Tgstation.Server.Host.Controllers return Accepted(job.ToApi()); } - /// + [HttpPost] [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)] [ProducesResponseType(typeof(DreamMaker), 200)] [ProducesResponseType(200)] [ProducesResponseType(410)] - public override async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken) + public async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken) { if (model.ApiValidationPort == 0) return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" }); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index d5e61a062b..b936049c8c 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -88,10 +88,13 @@ namespace Tgstation.Server.Host.Controllers /// /// Main page of the /// - /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise + /// + /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise. + /// + /// retrieved successfully. + [HttpGet] [TgsAuthorize] [AllowAnonymous] - [HttpGet] [ProducesResponseType(typeof(Api.Models.ServerInformation), 200)] public IActionResult Home() { @@ -117,6 +120,9 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation /// A resulting in the of the operation + /// User logged in and generated successfully. + /// User authentication failed. + /// User authenticated but is disabled by an administrator. [HttpPost] [ProducesResponseType(typeof(Api.Models.Token), 200)] [ProducesResponseType(401)] @@ -204,7 +210,10 @@ namespace Tgstation.Server.Host.Controllers // Now that the bookeeping is done, tell them to fuck off if necessary if (!user.Enabled.Value) + { + Logger.LogTrace("Not logging in disabled user {0}.", user.Id); return Forbid(); + } var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false); if (systemIdentity != null) diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index c02dd7d936..6383c81368 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -23,11 +23,11 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// Controller for managing s + /// for managing s /// [Route(Routes.InstanceManager)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class InstanceController : ModelController + public sealed class InstanceController : ApiController { /// /// File name to allow attaching instances @@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The for the - public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, false) + public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, false, true) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); @@ -108,11 +108,11 @@ namespace Tgstation.Server.Host.Controllers UserId = AuthenticationContext.User.Id }; - /// + [HttpPut] [TgsAuthorize(InstanceManagerRights.Create)] [ProducesResponseType(typeof(Api.Models.Instance), 200)] [ProducesResponseType(typeof(Api.Models.Instance), 201)] - public override async Task Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) + public async Task Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -216,11 +216,11 @@ namespace Tgstation.Server.Host.Controllers return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api); } - /// + [HttpDelete] [TgsAuthorize(InstanceManagerRights.Delete)] [ProducesResponseType(200)] [ProducesResponseType(410)] - public override async Task Delete(long id, CancellationToken cancellationToken) + public async Task Delete(long id, CancellationToken cancellationToken) { var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id) .Include(x => x.WatchdogReattachInformation) @@ -252,12 +252,12 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } - /// + [HttpPost] [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)] [ProducesResponseType(typeof(Api.Models.Instance), 200)] [ProducesResponseType(410)] #pragma warning disable CA1502 // TODO: Decomplexify - public override async Task Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) + public async Task Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id); @@ -400,10 +400,10 @@ namespace Tgstation.Server.Host.Controllers } #pragma warning restore CA1502 - /// + [HttpGet(Routes.List)] [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)] [ProducesResponseType(typeof(IEnumerable), 200)] - public override async Task List(CancellationToken cancellationToken) + public async Task List(CancellationToken cancellationToken) { IQueryable query = DatabaseContext.Instances; if (!AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List)) @@ -425,11 +425,11 @@ namespace Tgstation.Server.Host.Controllers return Json(apis); } - /// + [HttpGet("{id}")] [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)] [ProducesResponseType(typeof(Api.Models.Instance), 200)] [ProducesResponseType(410)] - public override async Task GetId(long id, CancellationToken cancellationToken) + public async Task GetId(long id, CancellationToken cancellationToken) { var query = DatabaseContext.Instances.Where(x => x.Id == id); var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 2217e8f0e9..6e51490755 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -17,10 +17,10 @@ using Z.EntityFramework.Plus; namespace Tgstation.Server.Host.Controllers { /// - /// For managing s + /// for managing s. /// [Route(Routes.InstanceUser)] - public sealed class InstanceUserController : ModelController + public sealed class InstanceUserController : ApiController { /// /// Construct a @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The for the - public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) // false instance requirement, we handle this ourself + public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { } /// @@ -47,10 +47,10 @@ namespace Tgstation.Server.Host.Controllers return null; } - /// + [HttpPut] [TgsAuthorize(InstanceUserRights.CreateUsers)] [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)] - public override async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + public async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { var test = StandardModelChecks(model); if (test != null) @@ -75,12 +75,12 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi()); } - /// + [HttpPost] [TgsAuthorize(InstanceUserRights.WriteUsers)] [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] [ProducesResponseType(410)] #pragma warning disable CA1506 // TODO: Decomplexify - public override async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + public async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { var test = StandardModelChecks(model); if (test != null) @@ -106,26 +106,26 @@ namespace Tgstation.Server.Host.Controllers } #pragma warning restore CA1506 - /// + [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] [ProducesResponseType(404)] - public override Task Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound()); + public Task Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound()); - /// + [HttpGet(Routes.List)] [TgsAuthorize(InstanceUserRights.ReadUsers)] [ProducesResponseType(typeof(IEnumerable), 200)] - public override async Task List(CancellationToken cancellationToken) + public async Task List(CancellationToken cancellationToken) { var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false); return Json(users.Select(x => x.ToApi())); } - /// + [HttpGet("{id}")] [TgsAuthorize(InstanceUserRights.ReadUsers)] [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] [ProducesResponseType(410)] - public override async Task GetId(long id, CancellationToken cancellationToken) + public async Task GetId(long id, CancellationToken cancellationToken) { // this functions as userId var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -134,10 +134,10 @@ namespace Tgstation.Server.Host.Controllers return Json(user.ToApi()); } - /// + [HttpDelete] [TgsAuthorize(InstanceUserRights.WriteUsers)] [ProducesResponseType(200)] - public override async Task Delete(long id, CancellationToken cancellationToken) + public async Task Delete(long id, CancellationToken cancellationToken) { await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false); return Ok(); diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index c305597c4b..156e294e45 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -15,10 +15,10 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// for s + /// for s /// [Route(Routes.Jobs)] - public sealed class JobController : ModelController + public sealed class JobController : ApiController { /// /// The for the @@ -32,24 +32,24 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The value of /// The for the - public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } - /// + [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(IEnumerable), 200)] - public override async Task Read(CancellationToken cancellationToken) + public async Task Read(CancellationToken cancellationToken) { var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false); return Json(result.Select(x => x.ToApi())); } - /// + [HttpGet(Routes.List)] [TgsAuthorize] [ProducesResponseType(typeof(List), 200)] - public override async Task List(CancellationToken cancellationToken) + public async Task List(CancellationToken cancellationToken) { // you KNOW this will need pagination eventually right? var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.Job @@ -59,12 +59,12 @@ namespace Tgstation.Server.Host.Controllers return Json(jobs); } - /// + [HttpDelete] [TgsAuthorize] [ProducesResponseType(202)] [ProducesResponseType(404)] [ProducesResponseType(410)] - public override async Task Delete(long id, CancellationToken cancellationToken) + public async Task Delete(long id, CancellationToken cancellationToken) { // don't care if an instance post or not at this point var job = await DatabaseContext.Jobs.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -81,11 +81,11 @@ namespace Tgstation.Server.Host.Controllers return cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone); } - /// + [HttpGet("{id}")] [TgsAuthorize] [ProducesResponseType(404)] [ProducesResponseType(typeof(Api.Models.Job), 200)] - public override async Task GetId(long id, CancellationToken cancellationToken) + public async Task GetId(long id, CancellationToken cancellationToken) { var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (job == default(Job)) diff --git a/src/Tgstation.Server.Host/Controllers/ModelController.cs b/src/Tgstation.Server.Host/Controllers/ModelController.cs deleted file mode 100644 index 3434992040..0000000000 --- a/src/Tgstation.Server.Host/Controllers/ModelController.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Threading; -using System.Threading.Tasks; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; - -namespace Tgstation.Server.Host.Controllers -{ - /// - /// An representing a - /// - /// The model being represented - public abstract class ModelController : ApiController where TModel : class - { - /// - /// Construct a - /// - /// The for the - /// The for the - /// The for the - /// If the requires an - public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance, true) { } - - /// - /// Attempt to create a - /// - /// The being created - /// The for the operation - /// A resulting in the of the operation - [HttpPut] - public virtual Task Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); - - /// - /// Attempt to read a - /// - /// The for the operation - /// A resulting in the of the operation - [HttpGet] - public virtual Task Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); - - /// - /// Attempt to get a specific a - /// - /// The ID of the model to get - /// The for the operation - /// A resulting in the of the operation - [HttpGet("{id}")] - public virtual Task GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); - - /// - /// Attempt to update a - /// - /// The being updated - /// The for the operation - /// A resulting in the of the operation - [HttpPost] - public virtual Task Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); - - /// - /// Attempt to delete a model with a particular - /// - /// The ID of the model to delete - /// The for the operation - /// A resulting in the of the operation - [HttpDelete("{id}")] - public virtual Task Delete(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); - - /// - /// Attempt to list entries of the - /// - /// The for the operation - /// A resulting in the of the operation - [HttpGet("List")] - public virtual Task List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); - } -} diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index f08fa51e9d..fde31f048c 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -23,11 +23,11 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// Controller for managing the s + /// for managing the s /// [Route(Routes.Repository)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class RepositoryController : ModelController + public sealed class RepositoryController : ApiController { /// /// The for the @@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The for the /// The containing value of - public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true) + public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true) { this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); @@ -126,11 +126,11 @@ namespace Tgstation.Server.Host.Controllers return needsDbUpdate; } - /// + [HttpPut] [TgsAuthorize(RepositoryRights.SetOrigin)] [ProducesResponseType(typeof(Repository), 201)] [ProducesResponseType(410)] - public override async Task Create([FromBody] Repository model, CancellationToken cancellationToken) + public async Task Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -216,10 +216,13 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Delete the + /// Delete the . /// /// The for the operation /// A resulting in the of the operation + /// Job to delete the repository created successfully. + /// The repository is not present. + [HttpDelete] [TgsAuthorize(RepositoryRights.Delete)] [ProducesResponseType(typeof(Repository), 202)] [ProducesResponseType(410)] @@ -249,12 +252,12 @@ namespace Tgstation.Server.Host.Controllers return Accepted(api); } - /// + [HttpGet] [TgsAuthorize(RepositoryRights.Read)] [ProducesResponseType(typeof(Repository), 200)] [ProducesResponseType(typeof(Repository), 201)] [ProducesResponseType(410)] - public override async Task Read(CancellationToken cancellationToken) + public async Task Read(CancellationToken cancellationToken) { var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -289,11 +292,14 @@ namespace Tgstation.Server.Host.Controllers } } - /// + [HttpPost] [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)] + [ProducesResponseType(typeof(Repository), 200)] + [ProducesResponseType(typeof(Repository), 202)] + [ProducesResponseType(410)] #pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1505 - public override async Task Update([FromBody]Repository model, CancellationToken cancellationToken) + public async Task Update([FromBody]Repository model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index ebe9aaf2b3..ed3cf8d495 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -19,10 +19,10 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { /// - /// For managing s + /// for managing s. /// [Route(Routes.User)] - public sealed class UserController : ModelController + public sealed class UserController : ApiController { /// /// The for the @@ -53,7 +53,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The containing the value of - public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false) + public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); @@ -73,12 +73,12 @@ namespace Tgstation.Server.Host.Controllers return null; } - /// + [HttpPut] [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(typeof(Api.Models.User), 201)] [ProducesResponseType(410)] [ProducesResponseType(501)] - public override async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken) + public async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -140,11 +140,11 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true)); } - /// + [HttpPost] [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] [ProducesResponseType(typeof(Api.Models.User), 200)] [ProducesResponseType(404)] - public override async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken) + public async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -190,15 +190,15 @@ namespace Tgstation.Server.Host.Controllers }); } - /// + [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(Api.Models.User), 200)] - public override Task Read(CancellationToken cancellationToken) => Task.FromResult(Json(AuthenticationContext.User.ToApi(true))); + public Task Read(CancellationToken cancellationToken) => Task.FromResult(Json(AuthenticationContext.User.ToApi(true))); - /// + [HttpGet(Routes.List)] [TgsAuthorize(AdministrationRights.ReadUsers)] [ProducesResponseType(typeof(IEnumerable), 200)] - public override async Task List(CancellationToken cancellationToken) + public async Task List(CancellationToken cancellationToken) { var users = await DatabaseContext.Users .Include(x => x.CreatedBy) @@ -206,11 +206,11 @@ namespace Tgstation.Server.Host.Controllers return Json(users.Select(x => x.ToApi(true))); } - /// + [HttpGet("{id}")] [TgsAuthorize] [ProducesResponseType(typeof(Api.Models.User), 200)] [ProducesResponseType(404)] - public override async Task GetId(long id, CancellationToken cancellationToken) + public async Task GetId(long id, CancellationToken cancellationToken) { if (id == AuthenticationContext.User.Id) return await Read(cancellationToken).ConfigureAwait(false); From 9ece4dc252ef1bff5b15eee503c2ca3ca461bb73 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 20:38:23 -0500 Subject: [PATCH 10/29] Deduplicate download code for IByondInstallers --- .../Components/Byond/ByondInstallerBase.cs | 64 +++++++++++++++++++ .../Components/Byond/PosixByondInstaller.cs | 60 ++++++----------- .../Components/Byond/WindowsByondInstaller.cs | 57 ++++++----------- .../Byond/TestPosixByondInstaller.cs | 18 +++--- 4 files changed, 109 insertions(+), 90 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs new file mode 100644 index 0000000000..7d7c98bf9c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Byond +{ + /// + abstract class ByondInstallerBase : IByondInstaller + { + /// + public abstract string DreamDaemonName { get; } + + /// + public abstract string DreamMakerName { get; } + + /// + /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}. + /// + protected abstract string ByondRevisionsURLTemplate { get; } + + /// + /// Gets the for the . + /// + protected IIOManager IOManager { get; } + + /// + /// Gets the for the . + /// + protected ILogger Logger { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + protected ByondInstallerBase(IIOManager ioManager, ILogger logger) + { + IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public abstract Task CleanCache(CancellationToken cancellationToken); + + /// + public abstract Task InstallByond(string path, Version version, CancellationToken cancellationToken); + + /// + public Task DownloadVersion(Version version, CancellationToken cancellationToken) + { + if (version == null) + throw new ArgumentNullException(nameof(version)); + + var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor); + + Logger.LogTrace("Downloading from: {0}", url); + + return IOManager.DownloadFile(new Uri(url), cancellationToken); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs index 6d741bb952..8b42a813ee 100644 --- a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs @@ -11,13 +11,8 @@ namespace Tgstation.Server.Host.Components.Byond /// /// for Posix systems /// - sealed class PosixByondInstaller : IByondInstaller + sealed class PosixByondInstaller : ByondInstallerBase { - /// - /// The URL format string for getting BYOND linux version {0}.{1} zipfile - /// - const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip"; - /// /// Path to the BYOND cache /// @@ -28,45 +23,37 @@ namespace Tgstation.Server.Host.Components.Byond const string ShellScriptExtension = ".sh"; /// - public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension; + public override string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension; /// - public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension; + public override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension; - /// - /// The for the - /// - readonly IIOManager ioManager; + /// + protected override string ByondRevisionsURLTemplate => "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip"; /// /// The for the /// readonly IPostWriteHandler postWriteHandler; - /// - /// The for the - /// - readonly ILogger logger; - /// /// Construct a /// - /// The value of /// The value of - /// The value of - public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger logger) + /// The for the . + /// The for the . + public PosixByondInstaller(IPostWriteHandler postWriteHandler, IIOManager ioManager, ILogger logger) + : base(ioManager, logger) { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// - public async Task CleanCache(CancellationToken cancellationToken) + public override async Task CleanCache(CancellationToken cancellationToken) { try { - await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false); + await IOManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -74,23 +61,12 @@ namespace Tgstation.Server.Host.Components.Byond } catch (Exception e) { - logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); + Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); } } /// - public async Task DownloadVersion(Version version, CancellationToken cancellationToken) - { - if (version == null) - throw new ArgumentNullException(nameof(version)); - - var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor); - - return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false); - } - - /// - public Task InstallByond(string path, Version version, CancellationToken cancellationToken) + public override Task InstallByond(string path, Version version, CancellationToken cancellationToken) { if (path == null) throw new ArgumentNullException(nameof(path)); @@ -106,16 +82,16 @@ namespace Tgstation.Server.Host.Components.Byond async Task WriteAndMakeExecutable(string fullPath, string script) { - await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false); + await IOManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false); postWriteHandler.HandleWrite(fullPath); } - var basePath = ioManager.ConcatPath(path, ByondManager.BinPath); + var basePath = IOManager.ConcatPath(path, ByondManager.BinPath); - var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript)); + var task = Task.WhenAll(WriteAndMakeExecutable(IOManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(IOManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript)); - postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName)); - postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName)); + postWriteHandler.HandleWrite(IOManager.ConcatPath(basePath, DreamDaemonExecutableName)); + postWriteHandler.HandleWrite(IOManager.ConcatPath(basePath, DreamMakerExecutableName)); return task; } diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index 801b0468fc..4b48d6f0bf 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -12,13 +12,8 @@ namespace Tgstation.Server.Host.Components.Byond /// /// for windows systems /// - sealed class WindowsByondInstaller : IByondInstaller, IDisposable + sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable { - /// - /// The URL format string for getting BYOND windows version {0}.{1} zipfile - /// - const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip"; - /// /// Directory to byond installation configuration /// @@ -40,26 +35,19 @@ namespace Tgstation.Server.Host.Components.Byond const string ByondDXDir = "byond/directx"; /// - public string DreamDaemonName => "dreamdaemon.exe"; + public override string DreamDaemonName => "dreamdaemon.exe"; /// - public string DreamMakerName => "dm.exe"; + public override string DreamMakerName => "dm.exe"; - /// - /// The for the - /// - readonly IIOManager ioManager; + /// + protected override string ByondRevisionsURLTemplate => "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip"; /// /// The for the /// readonly IProcessExecutor processExecutor; - /// - /// The for the - /// - readonly ILogger logger; - /// /// The for the /// @@ -73,14 +61,13 @@ namespace Tgstation.Server.Host.Components.Byond /// /// Construct a /// - /// The value of /// The value of - /// The value of - public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger logger) + /// The for the . + /// The for the . + public WindowsByondInstaller(IProcessExecutor processExecutor, IIOManager ioManager, ILogger logger) + : base(ioManager, logger) { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); semaphore = new SemaphoreSlim(1); installedDirectX = false; @@ -90,11 +77,11 @@ namespace Tgstation.Server.Host.Components.Byond public void Dispose() => semaphore.Dispose(); /// - public async Task CleanCache(CancellationToken cancellationToken) + public override async Task CleanCache(CancellationToken cancellationToken) { try { - await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false); + await IOManager.DeleteDirectory(IOManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false); } catch(OperationCanceledException) { @@ -102,26 +89,18 @@ namespace Tgstation.Server.Host.Components.Byond } catch (Exception e) { - logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); + Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); } } /// - public Task DownloadVersion(Version version, CancellationToken cancellationToken) - { - var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor); - - return ioManager.DownloadFile(new Uri(url), cancellationToken); - } - - /// - public async Task InstallByond(string path, Version version, CancellationToken cancellationToken) + public override async Task InstallByond(string path, Version version, CancellationToken cancellationToken) { async Task SetNoPromptTrusted() { - var configPath = ioManager.ConcatPath(path, ByondConfigDir); - await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false); - await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false); + var configPath = IOManager.ConcatPath(path, ByondConfigDir); + await IOManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false); + await IOManager.WriteAllBytes(IOManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false); } var setNoPromptTrustedModeTask = SetNoPromptTrusted(); @@ -135,13 +114,13 @@ namespace Tgstation.Server.Host.Components.Byond { // ^check again because race conditions // always install it, it's pretty fast and will do better redundancy checking than us - var rbdx = ioManager.ConcatPath(path, ByondDXDir); + var rbdx = IOManager.ConcatPath(path, ByondDXDir); // noShellExecute because we aren't doing runas shennanigans IProcess directXInstaller; try { - directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true); + directXInstaller = processExecutor.LaunchProcess(IOManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true); } catch (Exception e) { diff --git a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs index e2fb2cff0f..b6337b1ebd 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs @@ -14,22 +14,22 @@ namespace Tgstation.Server.Host.Components.Byond.Tests public void TestConstruction() { Assert.ThrowsException(() => new PosixByondInstaller(null, null, null)); - var mockIOManager = new Mock(); - Assert.ThrowsException(() => new PosixByondInstaller(mockIOManager.Object, null, null)); var mockPostWriteHandler = new Mock(); - Assert.ThrowsException(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null)); + Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null)); + var mockIOManager = new Mock(); + Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null)); var mockLogger = new Mock>(); - new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object); + new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object); } [TestMethod] public async Task TestCacheClean() { - var mockIOManager = new Mock(); var mockPostWriteHandler = new Mock(); + var mockIOManager = new Mock(); var mockLogger = new Mock>(); - var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object); + var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object); const string ByondCachePath = "~/.byond/cache"; @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests await installer.CleanCache(default); - mockIOManager.Verify(); + mockPostWriteHandler.Verify(); mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable(); @@ -60,7 +60,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests var mockIOManager = new Mock(); var mockPostWriteHandler = new Mock(); var mockLogger = new Mock>(); - var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object); + var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object); await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, default)).ConfigureAwait(false); @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests var mockIOManager = new Mock(); var mockPostWriteHandler = new Mock(); var mockLogger = new Mock>(); - var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object); + var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object); const string FakePath = "fake"; await Assert.ThrowsExceptionAsync(() => installer.InstallByond(null, null, default)).ConfigureAwait(false); From b0c4b8cdc84a41cd5aaba46d25280f577dfa203c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 21:48:06 -0500 Subject: [PATCH 11/29] Add AspNetCore compatibility version --- src/Tgstation.Server.Host/Core/Application.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index e1d67a99ff..53fe98e15a 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -228,6 +229,7 @@ namespace Tgstation.Server.Host.Core var dataAnnotationValidator = options.ModelValidatorProviders.Single(validator => validator.GetType().Name == "DataAnnotationsModelValidatorProvider"); options.ModelValidatorProviders.Remove(dataAnnotationValidator); }) + .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .AddJsonOptions(options => { options.AllowInputFormatterExceptionMessages = true; From 7a62bcca8c341716a2a04b61a2e3db799141efd6 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 21:51:44 -0500 Subject: [PATCH 12/29] Integrate swagger UI in development mode --- .../Controllers/TgsOperationFilter.cs | 170 ++++++++++++++++++ src/Tgstation.Server.Host/Core/Application.cs | 48 ++++- .../Tgstation.Server.Host.csproj | 1 + 3 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs diff --git a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs b/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs new file mode 100644 index 0000000000..88df5f5862 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs @@ -0,0 +1,170 @@ +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 +{ + /// + /// for the server. + /// + sealed class TgsOperationFilter : IOperationFilter + { + /// + /// The name for password authentication. + /// + public const string PasswordSecuritySchemeId = "Password_Login"; + + /// + /// The name for token authentication. + /// + public const string TokenSecuritySchemeId = "Token_Authorization"; + + /// + 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(); + + // 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 + { + new OpenApiSecurityRequirement + { + { + tokenScheme, + new List() + } + } + }; + + 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 + { + new OpenApiSecurityRequirement + { + { + passwordScheme, + new List() + } + } + }; + + operation.Tags = new List { new OpenApiTag { Name = "_Login" } }; + } + + 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 + { + { + 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." + }); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 53fe98e15a..c137d1f748 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -12,6 +12,8 @@ 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; @@ -23,12 +25,14 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Tgstation.Server.Api; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -36,7 +40,7 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Core { /// - #pragma warning disable CA1506 +#pragma warning disable CA1506 sealed class Application : IApplication { /// @@ -240,6 +244,38 @@ namespace Tgstation.Server.Host.Core options.SerializerSettings.Converters = new[] { new VersionConverter() }; }); + if (hostingEnvironment.IsDevelopment()) + services.AddSwaggerGen( + c => + { + c.SwaggerDoc( + "v1", + new OpenApiInfo + { + Title = "TGS API", + Version = "v4" + }); + + c.OperationFilter(); + + c.AddSecurityDefinition(TgsOperationFilter.PasswordSecuritySchemeId, new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Name = HeaderNames.Authorization, + Scheme = ApiHeaders.BasicAuthenticationScheme + }); + + c.AddSecurityDefinition(TgsOperationFilter.TokenSecuritySchemeId, new OpenApiSecurityScheme + { + BearerFormat = "JWT", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Name = HeaderNames.Authorization, + Scheme = ApiHeaders.JwtAuthenticationScheme + }); + }); + // enable browser detection services.AddDetectionCore().AddBrowser(); @@ -356,7 +392,7 @@ namespace Tgstation.Server.Host.Core logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath); // attempt to restart the server if the configuration changes - if(serverControl.WatchdogPresent) + if (serverControl.WatchdogPresent) ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart()); // setup the HTTP request pipeline @@ -364,12 +400,18 @@ namespace Tgstation.Server.Host.Core applicationBuilder.UseServerErrorHandling(); // should anything after this throw an exception, catch it and display a detailed html page - if(hostingEnvironment.IsDevelopment()) + if (hostingEnvironment.IsDevelopment()) applicationBuilder.UseDeveloperExceptionPage(); // it is not worth it to limit this, you should only ever get it if you're an authorized user // suppress OperationCancelledExceptions, they are just aborted HTTP requests applicationBuilder.UseCancelledRequestSuppression(); + if (hostingEnvironment.IsDevelopment()) + { + applicationBuilder.UseSwagger(); + applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4")); + } + // Set up CORS based on configuration if necessary Action corsBuilder = null; if (controlPanelConfiguration.AllowAnyOrigin) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 47f1ede45b..79a66fa8d5 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -83,6 +83,7 @@ all runtime; build; native; contentfiles; analyzers + From 88e0481e9bbb0c9b8143386b74c1fb81c851b157 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 23:27:38 -0500 Subject: [PATCH 13/29] Enable XML documetation responses for Swagger --- src/Tgstation.Server.Api/Models/EntityId.cs | 13 +++++ .../Models/Internal/CompileJob.cs | 7 +-- .../Models/Internal/Job.cs | 7 +-- .../Controllers/AdministrationController.cs | 27 ++++++++-- .../Controllers/ByondController.cs | 22 +++++++- .../Controllers/ChatController.cs | 41 +++++++++++++-- .../Controllers/ConfigurationController.cs | 28 +++++++++- .../Controllers/DreamDaemonController.cs | 33 +++++++++--- .../Controllers/DreamMakerController.cs | 40 +++++++++++++-- .../Controllers/InstanceController.cs | 51 +++++++++++++++++-- .../Controllers/InstanceUserController.cs | 49 ++++++++++++++++-- .../Controllers/JobController.cs | 41 ++++++++++++--- .../Controllers/RepositoryController.cs | 29 ++++++++++- .../Controllers/TgsOperationFilter.cs | 2 - .../Controllers/UserController.cs | 40 ++++++++++++++- src/Tgstation.Server.Host/Core/Application.cs | 3 ++ 16 files changed, 380 insertions(+), 53 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/EntityId.cs diff --git a/src/Tgstation.Server.Api/Models/EntityId.cs b/src/Tgstation.Server.Api/Models/EntityId.cs new file mode 100644 index 0000000000..879b3b2ec8 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/EntityId.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// Base of s. + /// + public class EntityId + { + /// + /// The ID of the entity. + /// + public long Id { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index c659c49737..204657b883 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -6,13 +6,8 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Represents a run of /// - public class CompileJob + public class CompileJob : EntityId { - /// - /// The ID of the job - /// - public long Id { get; set; } - /// /// The .dme file used for compilation /// diff --git a/src/Tgstation.Server.Api/Models/Internal/Job.cs b/src/Tgstation.Server.Api/Models/Internal/Job.cs index e64bb68115..945d0d6aaa 100644 --- a/src/Tgstation.Server.Api/Models/Internal/Job.cs +++ b/src/Tgstation.Server.Api/Models/Internal/Job.cs @@ -7,13 +7,8 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Represents a long running job /// - public class Job + public class Job : EntityId { - /// - /// The ID - /// - public long Id { get; set; } - /// /// English description of the /// diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 0914dc5d78..def822cfb8 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -152,12 +152,19 @@ namespace Tgstation.Server.Host.Controllers IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken); + /// + /// Get server information. + /// + /// A resulting in the for the operation. + /// Retrieved data successfully. + /// The GitHub API rate limit was hit. See response header Retry-After. + /// A GitHub API error occurred. See error message for details. [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(Administration), 200)] [ProducesResponseType(424)] - [ProducesResponseType(429)] - public async Task Read(CancellationToken cancellationToken) + [ProducesResponseType(typeof(ErrorMessage), 429)] + public async Task Read() { try { @@ -195,10 +202,20 @@ namespace Tgstation.Server.Host.Controllers catch (ApiException e) { Logger.LogWarning(OctokitException, e); - return StatusCode((int)HttpStatusCode.FailedDependency); + return StatusCode((int)HttpStatusCode.FailedDependency, new ErrorMessage + { + Message = e.Message + }); } } + /// + /// Attempt to perform a server upgrade. + /// + /// The model containing the to update to. + /// The for the operation. + /// A resulting in the for the operation. + /// Upgrade operations are unavailable due to the launch configuration of TGS. [HttpPost] [TgsAuthorize(AdministrationRights.ChangeVersion)] [ProducesResponseType(typeof(ErrorMessage), 422)] @@ -226,7 +243,9 @@ namespace Tgstation.Server.Host.Controllers /// Attempts to restart the server /// /// A resulting in the of the request - [HttpDelete] + /// Restart begun successfully. + /// Restart operations are unavailable due to the launch configuration of TGS. + [HttpDelete("{id}")] [TgsAuthorize(AdministrationRights.RestartHost)] [ProducesResponseType(200)] [ProducesResponseType(typeof(ErrorMessage), 422)] diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 7b7e11bc36..030d06e4ba 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -46,24 +46,42 @@ namespace Tgstation.Server.Host.Controllers this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } + /// + /// Gets the active version. + /// + /// A resulting in the for the operation. + /// Retrieved version information successfully. [HttpGet] [TgsAuthorize(ByondRights.ReadActive)] [ProducesResponseType(typeof(Api.Models.Byond), 200)] - public Task Read(CancellationToken cancellationToken) => Task.FromResult( + public Task Read() => Task.FromResult( Json(new Api.Models.Byond { Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion })); + /// + /// Lists installed versions. + /// + /// A resulting in the for the operation. + /// Retrieved version information successfully. [HttpGet(Routes.List)] [TgsAuthorize(ByondRights.ListInstalled)] [ProducesResponseType(typeof(IEnumerable), 200)] - public Task List(CancellationToken cancellationToken) => Task.FromResult( + public Task List() => Task.FromResult( Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond { Version = x }))); + /// + /// Changes the active BYOND version to the one specified in a given . + /// + /// The to switch to. + /// The for the operation. + /// A resulting in the for the operation. + /// Switched active version successfully. + /// Created to install and switch active version successfully. [HttpPost] [TgsAuthorize(ByondRights.ChangeVersion)] [ProducesResponseType(typeof(Api.Models.Byond), 200)] diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index a83ed01dda..8eeae5e184 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -57,6 +57,13 @@ namespace Tgstation.Server.Host.Controllers Tag = api.Tag }; + /// + /// Create a new chat bot . + /// + /// The to create. + /// The for the operation. + /// A resulting in the for the operation. + /// Created chat bot successfully. [HttpPut] [TgsAuthorize(ChatBotRights.Create)] [ProducesResponseType(typeof(Api.Models.ChatBot), 201)] @@ -86,7 +93,7 @@ namespace Tgstation.Server.Host.Controllers if (!model.ValidateProviderChannelTypes()) return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" }); - model.Enabled = model.Enabled ?? false; + model.Enabled ??= false; // try to update das db first var dbModel = new Models.ChatBot @@ -130,7 +137,14 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi()); } - [HttpDelete] + /// + /// Delete a . + /// + /// The to delete. + /// The for the operation. + /// A resulting in the for the operation. + /// Chat bot deleted or does not exist. + [HttpDelete("{id}")] [TgsAuthorize(ChatBotRights.Delete)] [ProducesResponseType(200)] public async Task Delete(long id, CancellationToken cancellationToken) @@ -141,6 +155,12 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } + /// + /// List s. + /// + /// The for the operation. + /// A resulting in the for the operation. + /// Listed chat bots successfully. [HttpGet(Routes.List)] [TgsAuthorize(ChatBotRights.Read)] [ProducesResponseType(typeof(IEnumerable), 200)] @@ -159,6 +179,14 @@ namespace Tgstation.Server.Host.Controllers return Json(results.Select(x => x.ToApi())); } + /// + /// Get a specific . + /// + /// The to retrieve. + /// The for the operation. + /// A resulting in the for the operation. + /// Retrieved successfully. + /// Chat bot does not exist. [HttpGet("{id}")] [TgsAuthorize(ChatBotRights.Read)] [ProducesResponseType(typeof(Api.Models.ChatBot), 200)] @@ -179,7 +207,14 @@ namespace Tgstation.Server.Host.Controllers return Json(results.ToApi()); } - [HttpGet] + /// + /// Updates a chat bot . + /// + /// The update to apply. + /// The for the operation. + /// A resulting in the for the operation. + /// Update applied successfully. may or may not be returned based on user permissions. + [HttpPost] [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)] [ProducesResponseType(200)] [ProducesResponseType(typeof(Api.Models.ChatBot), 200)] diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 9144c9677a..3aecdb7587 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -64,6 +64,15 @@ namespace Tgstation.Server.Host.Controllers return false; } + /// + /// Write to a configuration file. + /// + /// The representing the file. + /// The for the operation. + /// A resulting in the for the operation. + /// File updated successfully. + /// File created successfully. + /// POSIX system impersonation requested but not implemented. [HttpPost] [TgsAuthorize(ConfigurationRights.Write)] [ProducesResponseType(typeof(ConfigurationFile), 200)] @@ -180,6 +189,13 @@ namespace Tgstation.Server.Host.Controllers } } + /// + /// Get the contents of the root configuration directory. + /// + /// The for the operation. + /// A resulting in the for the operation. + /// Directory not found on disk. + /// POSIX system impersonation requested but not implemented. [HttpGet(Routes.List)] [TgsAuthorize(ConfigurationRights.List)] [ProducesResponseType(typeof(IReadOnlyList), 200)] @@ -187,11 +203,19 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(501)] public Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken); + /// + /// Create a configuration directory. + /// + /// The representing the directory. + /// The for the operation. + /// A resulting in the for the operation. + /// Directory already exists. + /// Directory created successfully. + /// POSIX system impersonation requested but not implemented. [HttpPut] [TgsAuthorize(ConfigurationRights.Write)] [ProducesResponseType(typeof(ConfigurationFile), 200)] [ProducesResponseType(typeof(ConfigurationFile), 201)] - [ProducesResponseType(410)] [ProducesResponseType(501)] public async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken) { @@ -232,7 +256,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation /// Empty directory deleted successfully. /// POSIX system impersonation requested but not implemented. - [HttpDelete] + [HttpDelete("{id}")] [TgsAuthorize(ConfigurationRights.Delete)] [ProducesResponseType(200)] [ProducesResponseType(501)] diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index ca86960519..d9d8324e25 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -49,11 +49,18 @@ namespace Tgstation.Server.Host.Controllers this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } + /// + /// Launches the watchdog. + /// + /// The for the operation. + /// A resulting in the of the operation. + /// to launch the watchdog started successfully. + /// Watchdog already running. [HttpPut] [TgsAuthorize(DreamDaemonRights.Start)] [ProducesResponseType(typeof(Api.Models.Job), 202)] [ProducesResponseType(410)] - public async Task Create([FromBody] DreamDaemon model, CancellationToken cancellationToken) + public async Task Create(CancellationToken cancellationToken) { // alias for launching DD var instance = instanceManager.GetInstance(Instance); @@ -73,6 +80,12 @@ namespace Tgstation.Server.Host.Controllers return Accepted(job.ToApi()); } + /// + /// Get the watchdog status. + /// + /// The for the operation. + /// A resulting in the of the operation. + /// Read information successfully. [HttpGet] [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)] [ProducesResponseType(typeof(DreamDaemon), 200)] @@ -132,12 +145,12 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Stops the Watchdog if it's running + /// Stops the Watchdog if it's running. /// - /// The for the operation - /// A resulting in the of the operation + /// The for the operation. + /// A resulting in the of the operation. /// Watchdog terminated. - [HttpDelete] + [HttpDelete("{id}")] [TgsAuthorize(DreamDaemonRights.Shutdown)] [ProducesResponseType(200)] public async Task Delete(CancellationToken cancellationToken) @@ -147,6 +160,14 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } + /// + /// Update watchdog settings to be applied at next server reboot. + /// + /// The updated settings. + /// The for the operation. + /// A resulting in the of the operation. + /// Settings applied successfully. + /// Instance no longer available. [HttpPost] [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)] [ProducesResponseType(typeof(DreamDaemon), 200)] @@ -225,7 +246,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation /// A resulting in the of the request - /// Job started successfully. + /// Restart started successfully. [HttpPatch] [TgsAuthorize(DreamDaemonRights.Restart)] [ProducesResponseType(typeof(Api.Models.Job), 202)] diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 1d45c7132b..897c93f809 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -48,6 +48,12 @@ namespace Tgstation.Server.Host.Controllers this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } + /// + /// Read current status. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Read status successfully. [HttpGet] [TgsAuthorize(DreamMakerRights.Read)] [ProducesResponseType(typeof(DreamMaker), 200)] @@ -58,6 +64,14 @@ namespace Tgstation.Server.Host.Controllers return Json(dreamMakerSettings.ToApi()); } + /// + /// Get a specified by a given . + /// + /// The . + /// The for the operation. + /// A resulting in the of the request. + /// retrieved successfully. + /// Specified compile job does not exist in this instance. [HttpGet("{id}")] [TgsAuthorize(DreamMakerRights.CompileJobs)] [ProducesResponseType(typeof(Api.Models.CompileJob), 200)] @@ -75,22 +89,34 @@ namespace Tgstation.Server.Host.Controllers return Json(compileJob.ToApi()); } + /// + /// List all s for the instance. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(DreamMakerRights.CompileJobs)] - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), 200)] public async Task List(CancellationToken cancellationToken) { - var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob + var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new EntityId { Id = x.Id }).ToListAsync(cancellationToken).ConfigureAwait(false); return Json(compileJobs); } + /// + /// Begin deploying repository code. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Created deployment successfully. [HttpPut] [TgsAuthorize(DreamMakerRights.Compile)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public async Task Create([FromBody] DreamMaker model, CancellationToken cancellationToken) + public async Task Create(CancellationToken cancellationToken) { var job = new Models.Job { @@ -104,6 +130,14 @@ namespace Tgstation.Server.Host.Controllers return Accepted(job.ToApi()); } + /// + /// Update deployment settings. + /// + /// The updated settings. + /// The for the operation. + /// A resulting in the of the request. + /// Changes applied successfully. The updated settings will be returned based on user permissions. + /// Instance no longer available. [HttpPost] [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)] [ProducesResponseType(typeof(DreamMaker), 200)] diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 6383c81368..e7c6a5ee5a 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -108,6 +108,14 @@ namespace Tgstation.Server.Host.Controllers UserId = AuthenticationContext.User.Id }; + /// + /// Create or attach an . + /// + /// The settings. + /// The for the operation. + /// A resulting in the of the request. + /// Instance attached successfully. + /// Instance created successfully. [HttpPut] [TgsAuthorize(InstanceManagerRights.Create)] [ProducesResponseType(typeof(Api.Models.Instance), 200)] @@ -216,7 +224,15 @@ namespace Tgstation.Server.Host.Controllers return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api); } - [HttpDelete] + /// + /// Detach an with the given . + /// + /// The to detach. + /// The for the operation. + /// A resulting in the of the request. + /// Instance detatched successfully. + /// Instance not available. + [HttpDelete("{id}")] [TgsAuthorize(InstanceManagerRights.Delete)] [ProducesResponseType(200)] [ProducesResponseType(410)] @@ -252,6 +268,14 @@ namespace Tgstation.Server.Host.Controllers return Ok(); } + /// + /// Modify an 's settings. + /// + /// The updated settings. + /// The for the operation. + /// A resulting in the of the request. + /// Instance updated successfully. + /// Instance updated successfully and relocation job created. [HttpPost] [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)] [ProducesResponseType(typeof(Api.Models.Instance), 200)] @@ -259,6 +283,9 @@ namespace Tgstation.Server.Host.Controllers #pragma warning disable CA1502 // TODO: Decomplexify public async Task Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { + if (model == null) + throw new ArgumentNullException(nameof(model)); + var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id); var moveJob = await instanceQuery @@ -378,7 +405,9 @@ namespace Tgstation.Server.Host.Controllers { Id = originalModel.Id }; - if (originalModelPath != null) + + var moving = originalModelPath != null; + if (moving) { var job = new Models.Job { @@ -396,10 +425,16 @@ namespace Tgstation.Server.Host.Controllers if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false); - return Json(api); + return moving ? (IActionResult)Accepted(api) : Json(api); } - #pragma warning restore CA1502 +#pragma warning restore CA1502 + /// + /// List s. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)] [ProducesResponseType(typeof(IEnumerable), 200)] @@ -425,6 +460,14 @@ namespace Tgstation.Server.Host.Controllers return Json(apis); } + /// + /// Get a specific . + /// + /// The to retrieve. + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved successfully. + /// Instance not available. [HttpGet("{id}")] [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)] [ProducesResponseType(typeof(Api.Models.Instance), 200)] diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 6e51490755..dc998b9788 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -47,6 +47,13 @@ namespace Tgstation.Server.Host.Controllers return null; } + /// + /// Create am . + /// + /// The to create. + /// The for the operation. + /// A resulting in the of the request. + /// created successfully. [HttpPut] [TgsAuthorize(InstanceUserRights.CreateUsers)] [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)] @@ -75,6 +82,14 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi()); } + /// + /// Update the permissions for an . + /// + /// The updated . + /// The for the operation. + /// A resulting in the of the request. + /// updated successfully. + /// Instance user unavailable. [HttpPost] [TgsAuthorize(InstanceUserRights.WriteUsers)] [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] @@ -104,14 +119,23 @@ namespace Tgstation.Server.Host.Controllers UserId = originalUser.UserId }); } - #pragma warning restore CA1506 - +#pragma warning restore CA1506 + /// + /// Read the active . + /// + /// The of the request. + /// retrieved successfully. [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] - [ProducesResponseType(404)] - public Task Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound()); + public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi()); + /// + /// Lists s for the instance. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(InstanceUserRights.ReadUsers)] [ProducesResponseType(typeof(IEnumerable), 200)] @@ -121,6 +145,14 @@ namespace Tgstation.Server.Host.Controllers return Json(users.Select(x => x.ToApi())); } + /// + /// Gets a specific . + /// + /// The . + /// The for the operation. + /// A resulting in the of the request. + /// Retrieve successfully. + /// Instance user unavailable. [HttpGet("{id}")] [TgsAuthorize(InstanceUserRights.ReadUsers)] [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] @@ -134,7 +166,14 @@ namespace Tgstation.Server.Host.Controllers return Json(user.ToApi()); } - [HttpDelete] + /// + /// Delete an . + /// + /// The to delete. + /// The for the operation. + /// A resulting in the of the request. + /// deleted or no longer exists. + [HttpDelete("{id}")] [TgsAuthorize(InstanceUserRights.WriteUsers)] [ProducesResponseType(200)] public async Task Delete(long id, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 156e294e45..5dddb4bd01 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -37,6 +37,12 @@ namespace Tgstation.Server.Host.Controllers this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } + /// + /// Get active s for the instance. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved active s successfully. [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(IEnumerable), 200)] @@ -46,20 +52,35 @@ namespace Tgstation.Server.Host.Controllers return Json(result.Select(x => x.ToApi())); } + /// + /// List all s for the instance in reverse creation order. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize] - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), 200)] public async Task List(CancellationToken cancellationToken) { // you KNOW this will need pagination eventually right? - var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.Job + var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.EntityId { Id = x.Id }).ToListAsync(cancellationToken).ConfigureAwait(false); return Json(jobs); } - [HttpDelete] + /// + /// Cancel a running . + /// + /// The of the to cancel. + /// The for the operation. + /// A resulting in the of the request. + /// cancellation requested successfully. + /// does not exist in this instance. + /// already cancelled or completed. + [HttpDelete("{id}")] [TgsAuthorize] [ProducesResponseType(202)] [ProducesResponseType(404)] @@ -67,7 +88,7 @@ namespace Tgstation.Server.Host.Controllers public async Task Delete(long id, CancellationToken cancellationToken) { // don't care if an instance post or not at this point - var job = await DatabaseContext.Jobs.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (job == default(Job)) return NotFound(); @@ -81,13 +102,21 @@ namespace Tgstation.Server.Host.Controllers return cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone); } + /// + /// Get a specific . + /// + /// The of the to retrieve. + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved successfully. + /// does not exist in this instance. [HttpGet("{id}")] [TgsAuthorize] - [ProducesResponseType(404)] [ProducesResponseType(typeof(Api.Models.Job), 200)] + [ProducesResponseType(404)] public async Task GetId(long id, CancellationToken cancellationToken) { - var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (job == default(Job)) return NotFound(); var api = job.ToApi(); diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index fde31f048c..c6fd18a741 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -126,6 +126,14 @@ namespace Tgstation.Server.Host.Controllers return needsDbUpdate; } + /// + /// Begin cloning the repository if it doesn't exist. + /// + /// Initial settings. + /// The for the operation. + /// A resulting in the of the request. + /// The was created successfully and the to clone it has begun. + /// Instance no longer available. [HttpPut] [TgsAuthorize(RepositoryRights.SetOrigin)] [ProducesResponseType(typeof(Repository), 201)] @@ -221,8 +229,8 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation /// A resulting in the of the operation /// Job to delete the repository created successfully. - /// The repository is not present. - [HttpDelete] + /// Instance no longer available. + [HttpDelete("{id}")] [TgsAuthorize(RepositoryRights.Delete)] [ProducesResponseType(typeof(Repository), 202)] [ProducesResponseType(410)] @@ -252,6 +260,14 @@ namespace Tgstation.Server.Host.Controllers return Accepted(api); } + /// + /// Get status. + /// + /// The for the operation. + /// A resulting in the of the operation. + /// Retrieved the settings successfully. + /// Retrieved the settings successfully, though they did not previously exist. + /// Instance no longer available. [HttpGet] [TgsAuthorize(RepositoryRights.Read)] [ProducesResponseType(typeof(Repository), 200)] @@ -292,6 +308,15 @@ namespace Tgstation.Server.Host.Controllers } } + /// + /// Perform updats to the . + /// + /// The updated . + /// The for the operation. + /// A resulting in the of the operation. + /// Updated the settings successfully. + /// Updated the settings successfully and a was created to make the requested git changes. + /// Instance no longer available. [HttpPost] [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)] [ProducesResponseType(typeof(Repository), 200)] diff --git a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs b/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs index 88df5f5862..49f4fba1ce 100644 --- a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs +++ b/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs @@ -90,8 +90,6 @@ namespace Tgstation.Server.Host.Controllers } } }; - - operation.Tags = new List { new OpenApiTag { Name = "_Login" } }; } operation.Parameters.Add(new OpenApiParameter diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index ed3cf8d495..0f8b2cc0ff 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -73,6 +73,15 @@ namespace Tgstation.Server.Host.Controllers return null; } + /// + /// Create a . + /// + /// The to create. + /// The for the operation. + /// A resulting in the of the operation. + /// created successfully. + /// The requested could not be loaded. + /// A system user was requested but this is not implemented on POSIX. [HttpPut] [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(typeof(Api.Models.User), 201)] @@ -140,6 +149,14 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true)); } + /// + /// Update a . + /// + /// The to update. + /// The for the operation. + /// A resulting in the of the operation. + /// updated successfully. + /// Requested does not exist. [HttpPost] [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] [ProducesResponseType(typeof(Api.Models.User), 200)] @@ -190,11 +207,22 @@ namespace Tgstation.Server.Host.Controllers }); } + /// + /// Get information about the current . + /// + /// The of the operation. + /// The was retrieved successfully. [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(Api.Models.User), 200)] - public Task Read(CancellationToken cancellationToken) => Task.FromResult(Json(AuthenticationContext.User.ToApi(true))); + public IActionResult Read() => Json(AuthenticationContext.User.ToApi(true)); + /// + /// List all s in the server. + /// + /// The for the operation. + /// A resulting in the of the operation. + /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(AdministrationRights.ReadUsers)] [ProducesResponseType(typeof(IEnumerable), 200)] @@ -206,6 +234,14 @@ namespace Tgstation.Server.Host.Controllers return Json(users.Select(x => x.ToApi(true))); } + /// + /// Get a specific . + /// + /// The to retrieve. + /// The for the operation. + /// A resulting in the of the operation. + /// The was retrieved successfully. + /// The does not exist. [HttpGet("{id}")] [TgsAuthorize] [ProducesResponseType(typeof(Api.Models.User), 200)] @@ -213,7 +249,7 @@ namespace Tgstation.Server.Host.Controllers public async Task GetId(long id, CancellationToken cancellationToken) { if (id == AuthenticationContext.User.Id) - return await Read(cancellationToken).ConfigureAwait(false); + return Read(); if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) return Forbid(); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index c137d1f748..63ef876d7c 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -274,6 +274,9 @@ 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 From 8d2275e5e13b69e30509b080149d2eff972b142c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 9 Jan 2020 23:56:45 -0500 Subject: [PATCH 14/29] Add swagger spec to gh-pages --- build/BuildDox.ps1 | 3 ++ .../Tgstation.Server.Tests/IntegrationTest.cs | 4 +- tests/Tgstation.Server.Tests/TestingServer.cs | 54 ++++++++++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/build/BuildDox.ps1 b/build/BuildDox.ps1 index e41968ebb8..a88025e3d7 100644 --- a/build/BuildDox.ps1 +++ b/build/BuildDox.ps1 @@ -25,6 +25,9 @@ if($publish_dox){ git config user.email "tgstation-server@tgstation13.org" echo '# THIS BRANCH IS AUTO GENERATED BY APPVEYOR CI' > README.md + # Add in the swagger specification + mv C:/swagger.json "$doxdir/swagger.json" + # Need to create a .nojekyll file to allow filenames starting with an underscore # to be seen on the gh-pages site. Therefore creating an empty .nojekyll file. echo "" > .nojekyll diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index d5b1fe13ef..355d25b031 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -29,7 +29,7 @@ namespace Tgstation.Server.Tests try { var updatePath = Path.Combine(updatePathRoot, Guid.NewGuid().ToString()); - var server = new TestingServer(updatePath); + var server = new TestingServer(clientFactory, updatePath); using (var serverCts = new CancellationTokenSource()) { var cancellationToken = serverCts.Token; @@ -98,7 +98,7 @@ namespace Tgstation.Server.Tests [TestCategory("SkipWhenLiveUnitTesting")] public async Task TestStandardOperation() { - var server = new TestingServer(null); + var server = new TestingServer(clientFactory, null); using (var serverCts = new CancellationTokenSource()) { var cancellationToken = serverCts.Token; diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index f75fbbea6e..17f8ba84b7 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Net; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Client; using Tgstation.Server.Host; using Tgstation.Server.Host.Configuration; @@ -19,8 +22,14 @@ namespace Tgstation.Server.Tests readonly IServer realServer; - public TestingServer(string updatePath) + readonly IServerClientFactory serverClientFactory; + + readonly bool dumpOpenAPISpecpath; + + public TestingServer(IServerClientFactory serverClientFactory, string updatePath) { + this.serverClientFactory = serverClientFactory; + Directory = Path.GetTempFileName(); File.Delete(Directory); System.IO.Directory.CreateDirectory(Directory); @@ -31,6 +40,7 @@ namespace Tgstation.Server.Tests var databaseType = Environment.GetEnvironmentVariable("TGS4_TEST_DATABASE_TYPE"); var connectionString = Environment.GetEnvironmentVariable("TGS4_TEST_CONNECTION_STRING"); var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS4_TEST_GITHUB_TOKEN"); + var dumpOpenAPISpecPathEnvVar = Environment.GetEnvironmentVariable("TGS4_TEST_DUMP_API_SPEC"); if (String.IsNullOrEmpty(databaseType)) Assert.Inconclusive("No database type configured in env var TGS4_TEST_DATABASE_TYPE!"); @@ -40,6 +50,8 @@ namespace Tgstation.Server.Tests if (String.IsNullOrEmpty(gitHubAccessToken)) Console.WriteLine("WARNING: No GitHub access token configured, test may fail due to rate limits!"); + + dumpOpenAPISpecpath = !String.IsNullOrEmpty(dumpOpenAPISpecPathEnvVar); var args = new List() { @@ -53,6 +65,9 @@ namespace Tgstation.Server.Tests if (!String.IsNullOrEmpty(gitHubAccessToken)) args.Add(String.Format(CultureInfo.InvariantCulture, "General:GitHubAccessToken={0}", gitHubAccessToken)); + if (dumpOpenAPISpecpath) + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + realServer = new ServerFactory().CreateServer(args.ToArray(), updatePath); } @@ -61,6 +76,41 @@ namespace Tgstation.Server.Tests System.IO.Directory.Delete(Directory, true); } - public Task RunAsync(CancellationToken cancellationToken) => realServer.RunAsync(cancellationToken); + public async Task RunAsync(CancellationToken cancellationToken) + { + Task runTask = realServer.RunAsync(cancellationToken); + + if (dumpOpenAPISpecpath) + { + var giveUpAt = DateTimeOffset.Now.AddSeconds(60); + do + { + try + { + var client = await serverClientFactory.CreateServerClient(Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false); + break; + } + catch (ServiceUnavailableException) + { + //migrating, to be expected + if (DateTimeOffset.Now > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + } while (true); + + // Dump swagger to disk + // This is purely for CI + var webRequest = WebRequest.Create(Url.ToString() + "swagger/v1/swagger.json"); + using (var response = webRequest.GetResponse()) + using (var content = response.GetResponseStream()) + using (var output = new FileStream(@"C:\swagger.json", FileMode.Create)) + { + await content.CopyToAsync(output); + } + } + + await runTask; + } } } From e381b2e1d31be5c3ab36cb023ebf402c2c372563 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 10 Jan 2020 00:01:10 -0500 Subject: [PATCH 15/29] Update API documentation page --- docs/API.dox | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/API.dox b/docs/API.dox index 15416efd79..532597365d 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -3,6 +3,14 @@ @tableofcontents +@section api_swag OpenAPI Spec + +TGS4 has a, from code, generated OpenAPI 3.0 specification. It is much more authorative than these documents. + +The most up to date version should be found here: https://raw.githubusercontent.com/tgstation/tgstation-server/gh-pages/swagger.json + +You can use the API explorer SwaggerUI to interact with it: https://petstore.swagger.io + @section api_intro Introduction The TGS4 API is designed to be a fully realized RESTful service. Once hosted, follow the specified protocol for developing new clients or one off requests that provide full control over the server From 793a6c11530f5650e142c9006e81b8a8aec93b10 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 19:19:40 -0500 Subject: [PATCH 16/29] Remove C#8 feature --- src/Tgstation.Server.Api/ApiHeaders.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 6f6bb65a53..ff31d48e6d 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -274,7 +274,7 @@ namespace Tgstation.Server.Api headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent)); headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString()); - instanceId ??= InstanceId; + instanceId = instanceId ?? InstanceId; if (instanceId.HasValue) headers.Add(InstanceIdHeader, instanceId.ToString()); } From b367a115cea4a4f9060ce3748e516f140ddb7a0e Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 19:21:38 -0500 Subject: [PATCH 17/29] Change DreamMaker and Job listing clients to use EntityId --- .../Components/DreamMakerClient.cs | 2 +- .../Components/IDreamMakerClient.cs | 6 +++--- src/Tgstation.Server.Client/Components/IJobsClient.cs | 9 ++++----- src/Tgstation.Server.Client/Components/JobsClient.cs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs index d08c587194..496872d60f 100644 --- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components public Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken); /// - public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken); + public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken); /// public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs index 011475d5a5..87a3d0fb54 100644 --- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs @@ -33,11 +33,11 @@ namespace Tgstation.Server.Client.Components Task Compile(CancellationToken cancellationToken); /// - /// Gets the s of all s for the instance + /// Gets the s of all s for the instance /// /// The for the operation - /// A resulting in a of s with only the field populated - Task> GetJobIds(CancellationToken cancellationToken); + /// A resulting in a of s. + Task> GetJobIds(CancellationToken cancellationToken); /// /// Get a diff --git a/src/Tgstation.Server.Client/Components/IJobsClient.cs b/src/Tgstation.Server.Client/Components/IJobsClient.cs index 8ce64a95e0..f53b0dd612 100644 --- a/src/Tgstation.Server.Client/Components/IJobsClient.cs +++ b/src/Tgstation.Server.Client/Components/IJobsClient.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -12,11 +11,11 @@ namespace Tgstation.Server.Client.Components public interface IJobsClient { /// - /// List the s in the + /// List the s in the /// /// The for the operation - /// A resulting in a of the s in the - Task> List(CancellationToken cancellationToken); + /// A resulting in a of the s in the + Task> List(CancellationToken cancellationToken); /// /// List the active s in the diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs index d673abfc41..00c043ae0e 100644 --- a/src/Tgstation.Server.Client/Components/JobsClient.cs +++ b/src/Tgstation.Server.Client/Components/JobsClient.cs @@ -35,7 +35,7 @@ namespace Tgstation.Server.Client.Components public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken); /// public Task> ListActive(CancellationToken cancellationToken) => apiClient.Read>(Routes.Jobs, instance.Id, cancellationToken); From 683da5808f6967e449e1290747ce52c2234a513d Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 19:24:04 -0500 Subject: [PATCH 18/29] Minor cleanups in DreamMakerController --- .../Controllers/DreamMakerController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 897c93f809..1a9b4e6bcd 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Get a specified by a given . /// - /// The . + /// The . /// The for the operation. /// A resulting in the of the request. /// retrieved successfully. @@ -145,6 +145,9 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(410)] public async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken) { + if (model == null) + throw new ArgumentNullException(nameof(model)); + if (model.ApiValidationPort == 0) return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" }); From 9f58c94140d9237281eb4462a75ca5e2d2c6c793 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 19:47:09 -0500 Subject: [PATCH 19/29] Code cleanups --- src/Tgstation.Server.Host/Controllers/ChatController.cs | 2 +- src/Tgstation.Server.Host/Controllers/UserController.cs | 3 +-- src/Tgstation.Server.Host/Core/JobManager.cs | 2 +- src/Tgstation.Server.Host/Models/CompileJob.cs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 8eeae5e184..bbc3ba1f97 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Controllers if (!model.ValidateProviderChannelTypes()) return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" }); - model.Enabled ??= false; + model.Enabled = model.Enabled ?? false; // try to update das db first var dbModel = new Models.ChatBot diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 0f8b2cc0ff..b1d1ec5197 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net; using System.Threading; @@ -136,7 +135,7 @@ namespace Tgstation.Server.Host.Controllers else { if (model.Password.Length < generalConfiguration.MinimumPasswordLength) - return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) }); + return BadRequest(new ErrorMessage { Message = $"Password must be at least {generalConfiguration.MinimumPasswordLength} characters long!" }); cryptographySuite.SetUserPassword(dbUser, model.Password, true); } diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs index dbbc1eba6d..42b9bba454 100644 --- a/src/Tgstation.Server.Host/Core/JobManager.cs +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Core readonly ILogger logger; /// - /// of to running s + /// of s to running s /// readonly Dictionary jobs; diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 3fadc5b182..c2398e017c 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Models public Job Job { get; set; } /// - /// The of + /// The of /// public long JobId { get; set; } From ca20c375296ba88e9c719b7d189df21219fed276 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 21:26:41 -0500 Subject: [PATCH 20/29] Fix HttpDelete route --- .../Controllers/ConfigurationController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 3aecdb7587..363be52b3a 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -256,7 +256,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation /// Empty directory deleted successfully. /// POSIX system impersonation requested but not implemented. - [HttpDelete("{id}")] + [HttpDelete] [TgsAuthorize(ConfigurationRights.Delete)] [ProducesResponseType(200)] [ProducesResponseType(501)] From 4910c1feb81b13951e6b263aeee94a532250ec2d Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 21:26:54 -0500 Subject: [PATCH 21/29] Reduce code complexity --- src/Tgstation.Server.Host/Controllers/ByondController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 030d06e4ba..52872441b6 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -113,7 +112,7 @@ namespace Tgstation.Server.Host.Controllers // run the install through the job manager var job = new Models.Job { - Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion), + Description = $"Install BYOND version {installingVersion}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, From 0c01f4e17383b3948deb07e0a23753afac11f989 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 22:11:12 -0500 Subject: [PATCH 22/29] Attempting to reduce the complexity of UserController.Update --- .../Controllers/UserController.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index b1d1ec5197..edfa147ff3 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -165,7 +165,8 @@ namespace Tgstation.Server.Host.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); - var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers); + var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); + var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) @@ -200,10 +201,14 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - return Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User - { - Id = originalUser.Id - }); + return Json( + model.Id == originalUser.Id + || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers) + ? originalUser.ToApi(true) + : new Api.Models.User + { + Id = originalUser.Id + }); } /// From 21c0fdfa9bc2f451939d51da6f33f25d87154384 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 11 Jan 2020 22:50:01 -0500 Subject: [PATCH 23/29] Further UserController.Update improvements --- .../Controllers/UserController.cs | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index edfa147ff3..8988c62cbb 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -72,6 +72,20 @@ namespace Tgstation.Server.Host.Controllers return null; } + /// + /// Attempt to change the password of a given . + /// + /// The to update. + /// The new password. + /// on success, if is too short. + BadRequestObjectResult TrySetPassword(Models.User dbUser, string newPassword) + { + if (newPassword.Length < generalConfiguration.MinimumPasswordLength) + return BadRequest(new ErrorMessage { Message = $"Password must be at least {generalConfiguration.MinimumPasswordLength} characters long!" }); + cryptographySuite.SetUserPassword(dbUser, newPassword, true); + return null; + } + /// /// Create a . /// @@ -134,9 +148,9 @@ namespace Tgstation.Server.Host.Controllers } else { - if (model.Password.Length < generalConfiguration.MinimumPasswordLength) - return BadRequest(new ErrorMessage { Message = $"Password must be at least {generalConfiguration.MinimumPasswordLength} characters long!" }); - cryptographySuite.SetUserPassword(dbUser, model.Password, true); + var result = TrySetPassword(dbUser, model.Password); + if (result != null) + return result; } dbUser.CanonicalName = dbUser.Name.ToUpperInvariant(); @@ -168,23 +182,28 @@ namespace Tgstation.Server.Host.Controllers var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); - var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id) - .Include(x => x.CreatedBy) - .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var originalUser = passwordEditOnly + ? AuthenticationContext.User + : await DatabaseContext.Users.Where(x => x.Id == model.Id) + .Include(x => x.CreatedBy) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (originalUser == default) return NotFound(); - if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null)) + // Ensure they are only trying to edit password (system identity change will trigger a bad request) + if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.Name != null)) return Forbid(); + if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) + return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" }); + if (model.Password != null) { - if (originalUser.PasswordHash == null) - return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" }); - cryptographySuite.SetUserPassword(originalUser, model.Password, false); + var result = TrySetPassword(originalUser, model.Password); + if (result != null) + return result; } - else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) - return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" }); if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName) return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" }); @@ -201,6 +220,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + // return id only if not a self update or and cannot read users return Json( model.Id == originalUser.Id || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers) From f7ba5ee94b8e8a488789542b48379295ca7d3285 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 12 Jan 2020 13:46:11 -0500 Subject: [PATCH 24/29] Add support for console cancellation during setup wizard --- src/Tgstation.Server.Host/Core/SetupWizard.cs | 73 ++++++++++++------- src/Tgstation.Server.Host/IO/Console.cs | 41 ++++++++++- src/Tgstation.Server.Host/IO/IConsole.cs | 5 ++ 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/src/Tgstation.Server.Host/Core/SetupWizard.cs b/src/Tgstation.Server.Host/Core/SetupWizard.cs index bc3a0759b8..dff6446387 100644 --- a/src/Tgstation.Server.Host/Core/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Core/SetupWizard.cs @@ -638,41 +638,60 @@ namespace Tgstation.Server.Host.Core } var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName); - var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false); - bool shouldRunBasedOnAutodetect; - if (exists) + async Task HandleSetupCancel() { - var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false); - var contents = Encoding.UTF8.GetString(bytes); - var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}"; - logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty); - shouldRunBasedOnAutodetect = existingConfigIsEmpty; - } - else - { - shouldRunBasedOnAutodetect = true; - logger.LogTrace("No configuration json detected"); + await console.WriteAsync(String.Empty, true, default).ConfigureAwait(false); + await console.WriteAsync("Aborting setup!", true, default).ConfigureAwait(false); } - if (!shouldRunBasedOnAutodetect) - { - if (forceRun) + // Link passed cancellationToken with cancel key press + Task finalTask = Task.CompletedTask; + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress)) + using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel())) + try { - logger.LogTrace("Asking user to bypass due to force run request..."); - await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false); + var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false); - forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false); + bool shouldRunBasedOnAutodetect; + if (exists) + { + var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false); + var contents = Encoding.UTF8.GetString(bytes); + var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}"; + logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty); + shouldRunBasedOnAutodetect = existingConfigIsEmpty; + } + else + { + shouldRunBasedOnAutodetect = true; + logger.LogTrace("No configuration json detected"); + } + + if (!shouldRunBasedOnAutodetect) + { + if (forceRun) + { + logger.LogTrace("Asking user to bypass due to force run request..."); + await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false); + + forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false); + } + + if (!forceRun) + return false; + } + + // flush the logs to prevent console conflicts + await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + + await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false); + } + finally + { + await finalTask.ConfigureAwait(false); } - if (!forceRun) - return false; - } - - // flush the logs to prevent console conflicts - await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - - await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false); return true; } } diff --git a/src/Tgstation.Server.Host/IO/Console.cs b/src/Tgstation.Server.Host/IO/Console.cs index 9f1fceba80..0d90d54c25 100644 --- a/src/Tgstation.Server.Host/IO/Console.cs +++ b/src/Tgstation.Server.Host/IO/Console.cs @@ -6,11 +6,50 @@ using System.Threading.Tasks; namespace Tgstation.Server.Host.IO { /// - sealed class Console : IConsole + sealed class Console : IConsole, IDisposable { /// public bool Available => Environment.UserInteractive; + /// + public CancellationToken CancelKeyPress => cancelKeyCts.Token; + + /// + /// The for . + /// + readonly CancellationTokenSource cancelKeyCts; + + /// + /// If the was disposed; + /// + bool disposed; + + /// + /// Initializes a new instance of the . + /// + public Console() + { + cancelKeyCts = new CancellationTokenSource(); + System.Console.CancelKeyPress += (sender, e) => + { + lock (cancelKeyCts) + { + if (!disposed) + cancelKeyCts.Cancel(); + } + }; + } + + /// + public void Dispose() + { + lock (cancelKeyCts) + { + cancelKeyCts.Dispose(); + disposed = true; + } + } + void CheckAvailable() { if (!Available) diff --git a/src/Tgstation.Server.Host/IO/IConsole.cs b/src/Tgstation.Server.Host/IO/IConsole.cs index 119b19d5dc..405ffb425b 100644 --- a/src/Tgstation.Server.Host/IO/IConsole.cs +++ b/src/Tgstation.Server.Host/IO/IConsole.cs @@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.IO /// bool Available { get; } + /// + /// Gets a that triggers if Crtl+C or an equivalent is pressed. + /// + CancellationToken CancelKeyPress { get; } + /// /// Write some to the /// From 0a0343cd641b11afbe9d5a806b21cd4a73d8b05c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 12 Jan 2020 14:32:30 -0500 Subject: [PATCH 25/29] Fix get endpoint for DreamMakeClient and Jobs Clients to accept entity IDs --- src/Tgstation.Server.Client/Components/DreamMakerClient.cs | 2 +- src/Tgstation.Server.Client/Components/IDreamMakerClient.cs | 4 ++-- src/Tgstation.Server.Client/Components/IJobsClient.cs | 4 ++-- src/Tgstation.Server.Client/Components/JobsClient.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs index 496872d60f..dafa049834 100644 --- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs @@ -35,7 +35,7 @@ namespace Tgstation.Server.Client.Components public Task Compile(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamMaker, instance.Id, cancellationToken); /// - public Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken); + public Task GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken); /// public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs index 87a3d0fb54..174634bdb4 100644 --- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs @@ -42,9 +42,9 @@ namespace Tgstation.Server.Client.Components /// /// Get a /// - /// The to get + /// The to get /// The for the operation /// A resulting in the - Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken); + Task GetCompileJob(EntityId compileJob, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/Components/IJobsClient.cs b/src/Tgstation.Server.Client/Components/IJobsClient.cs index f53b0dd612..ae879f485e 100644 --- a/src/Tgstation.Server.Client/Components/IJobsClient.cs +++ b/src/Tgstation.Server.Client/Components/IJobsClient.cs @@ -27,10 +27,10 @@ namespace Tgstation.Server.Client.Components /// /// Get a /// - /// The to get + /// The 's to get /// The for the operation /// A resulting in the - Task GetId(Job job, CancellationToken cancellationToken); + Task GetId(EntityId job, CancellationToken cancellationToken); /// /// Cancels a diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs index 00c043ae0e..228a2c4a9b 100644 --- a/src/Tgstation.Server.Client/Components/JobsClient.cs +++ b/src/Tgstation.Server.Client/Components/JobsClient.cs @@ -41,6 +41,6 @@ namespace Tgstation.Server.Client.Components public Task> ListActive(CancellationToken cancellationToken) => apiClient.Read>(Routes.Jobs, instance.Id, cancellationToken); /// - public Task GetId(Job job, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken); + public Task GetId(EntityId job, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken); } } \ No newline at end of file From 48bc829bd99b3fff92d45febead84b7302537df6 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 12 Jan 2020 16:42:19 -0500 Subject: [PATCH 26/29] Reduce duplication in swagger spec --- .../Controllers/TgsOpenApiFilters.cs | 244 ++++++++++++++++++ .../Controllers/TgsOperationFilter.cs | 168 ------------ src/Tgstation.Server.Host/Core/Application.cs | 16 +- 3 files changed, 254 insertions(+), 174 deletions(-) create mode 100644 src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs delete mode 100644 src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs diff --git a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs new file mode 100644 index 0000000000..f158398efe --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs @@ -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 +{ + /// + /// and for the server. + /// + sealed class TgsOpenApiFilters : IOperationFilter, IDocumentFilter + { + /// + /// The name for password authentication. + /// + public const string PasswordSecuritySchemeId = "Password_Login_Scheme"; + + /// + /// The name for token authentication. + /// + 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 operationsToAddInstanceIdReferenceTo; + + /// + /// Initializes a new instance of the . + /// + public TgsOpenApiFilters() + { + operationsToAddInstanceIdReferenceTo = new List(); + } + + /// + 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(); + + if (authAttributes.Any()) + { + var tokenScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = TokenSecuritySchemeId + } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + { + tokenScheme, + new List() + } + } + }; + + 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 + { + new OpenApiSecurityRequirement + { + { + passwordScheme, + new List() + } + } + }; + } + } + + /// + 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 + { + { + 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." + }); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs b/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs deleted file mode 100644 index 49f4fba1ce..0000000000 --- a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs +++ /dev/null @@ -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 -{ - /// - /// for the server. - /// - sealed class TgsOperationFilter : IOperationFilter - { - /// - /// The name for password authentication. - /// - public const string PasswordSecuritySchemeId = "Password_Login"; - - /// - /// The name for token authentication. - /// - public const string TokenSecuritySchemeId = "Token_Authorization"; - - /// - 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(); - - // 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 - { - new OpenApiSecurityRequirement - { - { - tokenScheme, - new List() - } - } - }; - - 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 - { - new OpenApiSecurityRequirement - { - { - passwordScheme, - new List() - } - } - }; - } - - 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 - { - { - 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." - }); - } - } -} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 63ef876d7c..9939ea6778 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -256,9 +256,16 @@ namespace Tgstation.Server.Host.Core Version = "v4" }); - c.OperationFilter(); + // 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(); + c.DocumentFilter(); + + 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 From cd53ca9db44d6863cbada5e12b2381838bd6d300 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 12 Jan 2020 17:29:49 -0500 Subject: [PATCH 27/29] General code cleanup pass --- .../Controllers/TgsOpenApiFilters.cs | 1 - src/Tgstation.Server.Host/Core/Application.cs | 17 ++++++-- .../Core/AssemblyInformationProvider.cs | 24 +++++++++++ .../Core/IAssemblyInformationProvider.cs | 20 +++++++++ .../DesignTimeDbContextFactoryHelpers.cs | 8 ++-- src/Tgstation.Server.Host/Program.cs | 2 +- .../Security/TokenFactory.cs | 9 ++-- src/Tgstation.Server.Host/Server.cs | 9 +++- src/Tgstation.Server.Host/ServerFactory.cs | 42 +++++++++++++++++-- .../Core/TestApplication.cs | 17 +++++--- .../TestServerFactory.cs | 17 +++++++- tests/Tgstation.Server.Tests/TestingServer.cs | 2 +- 12 files changed, 143 insertions(+), 25 deletions(-) create mode 100644 src/Tgstation.Server.Host/Core/AssemblyInformationProvider.cs create mode 100644 src/Tgstation.Server.Host/Core/IAssemblyInformationProvider.cs diff --git a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs index f158398efe..b9c0e0f8d9 100644 --- a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs +++ b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs @@ -4,7 +4,6 @@ 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; diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 9939ea6778..796d929743 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -23,7 +23,6 @@ using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; -using System.Reflection; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Host.Components; @@ -57,6 +56,11 @@ namespace Tgstation.Server.Host.Core /// readonly IConfiguration configuration; + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + /// /// The for the /// @@ -76,15 +80,20 @@ namespace Tgstation.Server.Host.Core /// Construct an /// /// The value of + /// The for the . /// The value of - public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment) + public Application( + IConfiguration configuration, + IAssemblyInformationProvider assemblyInformationProvider, + Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment) { this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); startupTcs = new TaskCompletionSource(); - Version = Assembly.GetExecutingAssembly().GetName().Version; + Version = assemblyInformationProvider.Name.Version; VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version); } @@ -258,7 +267,7 @@ namespace Tgstation.Server.Host.Core // 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 assemblyLocation = assemblyInformationProvider.Path; var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml")); c.IncludeXmlComments(filePath); diff --git a/src/Tgstation.Server.Host/Core/AssemblyInformationProvider.cs b/src/Tgstation.Server.Host/Core/AssemblyInformationProvider.cs new file mode 100644 index 0000000000..41fe7919ca --- /dev/null +++ b/src/Tgstation.Server.Host/Core/AssemblyInformationProvider.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class AssemblyInformationProvider : IAssemblyInformationProvider + { + /// + public string Path { get; } + + /// + public AssemblyName Name { get; } + + /// + /// Initializes a new instance of the . + /// + public AssemblyInformationProvider() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + Path = assembly.Location; + Name = assembly.GetName(); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/IAssemblyInformationProvider.cs b/src/Tgstation.Server.Host/Core/IAssemblyInformationProvider.cs new file mode 100644 index 0000000000..8112deb276 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IAssemblyInformationProvider.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace Tgstation.Server.Host.Core +{ + /// + /// For retrieving the 's location. + /// + interface IAssemblyInformationProvider + { + /// + /// Gets the path to the executing assembly. + /// + string Path { get; } + + /// + /// Gets the . + /// + AssemblyName Name { get; } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs index cb4e730d1e..2de82a18bf 100644 --- a/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -using System.IO; -using System.Reflection; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Models.Migrations { @@ -28,7 +28,9 @@ namespace Tgstation.Server.Host.Models.Migrations public static IOptions GetDbContextOptions() { var builder = new ConfigurationBuilder(); - builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + var assemblyInfoProvider = new AssemblyInformationProvider(); + var ioManager = new DefaultIOManager(); + builder.SetBasePath(ioManager.GetDirectoryName(assemblyInfoProvider.Path)); builder.AddJsonFile(RootJson); builder.AddJsonFile(DevJson); var configuration = builder.Build(); diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs index 83895df62b..0e81f1d1f9 100644 --- a/src/Tgstation.Server.Host/Program.cs +++ b/src/Tgstation.Server.Host/Program.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Host /// The to use /// #pragma warning disable SA1401 // Fields must be private - internal static IServerFactory ServerFactory = new ServerFactory(); + internal static IServerFactory ServerFactory = Host.ServerFactory.CreateDefault(); #pragma warning restore SA1401 // Fields must be private /// diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 2974f6d3b1..3023189b84 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -2,7 +2,6 @@ using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; -using System.Reflection; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; @@ -42,7 +41,11 @@ namespace Tgstation.Server.Host.Security /// /// The value of /// The used for generating the - public TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite) + /// The used to generate the issuer name. + public TokenFactory( + IAsyncDelayer asyncDelayer, + ICryptographySuite cryptographySuite, + IAssemblyInformationProvider assemblyInformationProvider) { ValidationParameters = new TokenValidationParameters { @@ -50,7 +53,7 @@ namespace Tgstation.Server.Host.Security IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)), ValidateIssuer = true, - ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name, + ValidIssuer = assemblyInformationProvider.Name.Name, ValidateLifetime = true, ValidateAudience = true, diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 45a683a91e..7dcf93d719 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -30,6 +30,11 @@ namespace Tgstation.Server.Host /// readonly IWebHostBuilder webHostBuilder; + /// + /// The for the . + /// + readonly IIOManager ioManager; + /// /// The s to run when the restarts /// @@ -69,10 +74,12 @@ namespace Tgstation.Server.Host /// Construct a /// /// The value of + /// The value of . /// The value of - public Server(IWebHostBuilder webHostBuilder, string updatePath) + public Server(IWebHostBuilder webHostBuilder, IIOManager ioManager, string updatePath) { this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.updatePath = updatePath; webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton(this)); diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index d06f20fa93..57c46c1ae2 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -1,29 +1,65 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using System; using System.IO; -using System.Reflection; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host { /// public sealed class ServerFactory : IServerFactory { + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// Create the default . + /// + /// A new with the default settings. + public static IServerFactory CreateDefault() + => new ServerFactory( + new AssemblyInformationProvider(), + new DefaultIOManager()); + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager) + { + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + } + /// public IServer CreateServer(string[] args, string updatePath) { var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args))) .ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory())) + .ConfigureServices(serviceCollection => + { + serviceCollection.AddSingleton(ioManager); + serviceCollection.AddSingleton(assemblyInformationProvider); + }) .UseStartup() .SuppressStatusMessages(true) .UseShutdownTimeout(TimeSpan.FromMinutes(1)); if(updatePath != null) - webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + webHost.UseContentRoot(Path.GetDirectoryName(assemblyInformationProvider.Path)); - return new Server(webHost, updatePath); + return new Server(webHost, ioManager, updatePath); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index 94eba126e5..d19f8c3aa8 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -23,13 +23,18 @@ namespace Tgstation.Server.Host.Core.Tests [TestMethod] public void TestMethodThrows() { - Assert.ThrowsException(() => new Application(null, null)); + Assert.ThrowsException(() => new Application(null, null, null)); var mockConfiguration = new Mock(); - Assert.ThrowsException(() => new Application(mockConfiguration.Object, null)); + Assert.ThrowsException(() => new Application(mockConfiguration.Object, null, null)); + + var mockAssemblyInfo = new Mock(); + mockAssemblyInfo.SetupGet(x => x.Name).Returns(typeof(Application).Assembly.GetName()); + + Assert.ThrowsException(() => new Application(mockConfiguration.Object, mockAssemblyInfo.Object, null)); var mockHostingEnvironment = new Mock(); - var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object); + var app = new Application(mockConfiguration.Object, mockAssemblyInfo.Object, mockHostingEnvironment.Object); Assert.ThrowsException(() => app.ConfigureServices(null)); Assert.ThrowsException(() => app.Configure(null, null, null, null, null)); @@ -69,11 +74,11 @@ namespace Tgstation.Server.Host.Core.Tests public void TestConfigureServicesThrowsWhenSetupWizardConfigurationDemands() { var mockConfiguration = new Mock(); - Assert.ThrowsException(() => new Application(mockConfiguration.Object, null)); - + var mockAssemblyInfo = new Mock(); + mockAssemblyInfo.SetupGet(x => x.Name).Returns(typeof(Application).Assembly.GetName()); var mockHostingEnvironment = new Mock(); - var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object); + var app = new Application(mockConfiguration.Object, mockAssemblyInfo.Object, mockHostingEnvironment.Object); var mockOptions = new Mock>(); mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration diff --git a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs index 1334ae87c9..eede3ff41c 100644 --- a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs @@ -1,5 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using System; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Tests { @@ -9,10 +12,20 @@ namespace Tgstation.Server.Host.Tests [TestClass] public sealed class TestServerFactory { + [TestMethod] + public void TestContructor() + { + Assert.ThrowsException(() => new ServerFactory(null, null)); + IAssemblyInformationProvider assemblyInformationProvider = Mock.Of(); + Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, null)); + IIOManager ioManager = Mock.Of(); + new ServerFactory(assemblyInformationProvider, ioManager); + } + [TestMethod] public void TestWorksWithoutUpdatePath() { - var factory = new ServerFactory(); + var factory = ServerFactory.CreateDefault(); Assert.ThrowsException(() => factory.CreateServer(null, null)); factory.CreateServer(Array.Empty(), null); @@ -21,7 +34,7 @@ namespace Tgstation.Server.Host.Tests [TestMethod] public void TestWorksWithUpdatePath() { - var factory = new ServerFactory(); + var factory = ServerFactory.CreateDefault(); const string Path = "/test"; Assert.ThrowsException(() => factory.CreateServer(null, null)); diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 17f8ba84b7..29ece7cbca 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -68,7 +68,7 @@ namespace Tgstation.Server.Tests if (dumpOpenAPISpecpath) Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); - realServer = new ServerFactory().CreateServer(args.ToArray(), updatePath); + realServer = ServerFactory.CreateDefault().CreateServer(args.ToArray(), updatePath); } public void Dispose() From 6e44037d5daac8c226bceb34c08834ba8a38ff3b Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 12 Jan 2020 18:07:53 -0500 Subject: [PATCH 28/29] Fix the build --- .../Controllers/TgsOpenApiFilters.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs index b9c0e0f8d9..b902770af5 100644 --- a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs +++ b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs @@ -27,20 +27,6 @@ namespace Tgstation.Server.Host.Controllers /// 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 operationsToAddInstanceIdReferenceTo; - - /// - /// Initializes a new instance of the . - /// - public TgsOpenApiFilters() - { - operationsToAddInstanceIdReferenceTo = new List(); - } - /// public void Apply(OpenApiOperation operation, OperationFilterContext context) { @@ -126,8 +112,6 @@ namespace Tgstation.Server.Host.Controllers 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]\"", From a35a2bed3f8027ceb365c1ba6e6319f8bcd6f324 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 12 Jan 2020 18:30:38 -0500 Subject: [PATCH 29/29] Fix installation directory instance detection. --- .../Controllers/InstanceController.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 22419b3be2..74c4d9b349 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -130,7 +130,14 @@ namespace Tgstation.Server.Host.Controllers }, out var normalizedLocalPath); if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal)) - return Conflict("Instances cannot be created in the installation directory!"); + { + bool sameLength = rawPath.Length == normalizedLocalPath.Length; + char dirSeparatorChar = rawPath.ToCharArray()[normalizedLocalPath.Length]; + if(sameLength + || dirSeparatorChar == Path.DirectorySeparatorChar + || dirSeparatorChar == Path.AltDirectorySeparatorChar) + return Conflict(new ErrorMessage { Message = "Instances cannot be created in the installation directory!" }); + } var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); bool attached = false;