From 79700c7d0e9821dad7edbb515ff0b991abca9860 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 22 Nov 2020 21:11:00 -0500 Subject: [PATCH 001/154] Adds Oauth framework --- build/Version.props | 8 +- src/Tgstation.Server.Api/ApiHeaders.cs | 60 +- src/Tgstation.Server.Api/HeaderTypes.cs | 13 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 + .../Models/OAuthConnection.cs | 21 + .../Models/OAuthProvider.cs | 13 + src/Tgstation.Server.Api/Models/User.cs | 11 +- .../Rights/AdministrationRights.cs | 5 + .../.config/dotnet-tools.json | 2 +- .../Configuration/GitHubOAuthConfiguration.cs | 18 + .../Configuration/SecurityConfiguration.cs | 21 +- .../Controllers/AdministrationController.cs | 10 - .../Controllers/ApiController.cs | 21 +- .../Controllers/HomeController.cs | 119 ++- .../Controllers/InstanceController.cs | 14 +- .../Controllers/RepositoryController.cs | 4 +- .../Controllers/UserController.cs | 83 +- src/Tgstation.Server.Host/Core/Application.cs | 42 +- .../Core/SwaggerConfiguration.cs | 2 +- .../Database/DatabaseContext.cs | 61 +- .../Database/IDatabaseContext.cs | 31 +- ...22231219_MSAddOAuthConnections.Designer.cs | 820 ++++++++++++++++++ .../20201122231219_MSAddOAuthConnections.cs | 60 ++ ...22231327_MYAddOAuthConnections.Designer.cs | 809 +++++++++++++++++ .../20201122231327_MYAddOAuthConnections.cs | 61 ++ ...22231443_PGAddOAuthConnections.Designer.cs | 817 +++++++++++++++++ .../20201122231443_PGAddOAuthConnections.cs | 61 ++ ...22231546_SLAddOAuthConnections.Designer.cs | 808 +++++++++++++++++ .../20201122231546_SLAddOAuthConnections.cs | 60 ++ .../MySqlDatabaseContextModelSnapshot.cs | 35 +- ...PostgresSqlDatabaseContextModelSnapshot.cs | 36 +- .../SqlServerDatabaseContextModelSnapshot.cs | 36 +- .../SqliteDatabaseContextModelSnapshot.cs | 35 +- .../Models/OAuthConnection.cs | 26 + src/Tgstation.Server.Host/Models/User.cs | 7 +- .../Security/ITokenFactory.cs | 5 +- .../Security/OAuth/GitHubOAuthValidator.cs | 95 ++ .../Security/OAuth/IOAuthProviders.cs | 17 + .../Security/OAuth/IOAuthValidator.cs | 25 + .../Security/OAuth/OAuthProviders.cs | 51 ++ .../Security/TokenFactory.cs | 6 +- .../Setup/IPostSetupServices.cs | 7 +- .../Setup/PostSetupServices.cs | 11 + .../Setup/SetupApplication.cs | 1 + .../Tgstation.Server.Host.csproj | 32 +- src/Tgstation.Server.Host/appsettings.json | 3 +- .../TestApiHeaders.cs | 4 +- 47 files changed, 4330 insertions(+), 163 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/OAuthConnection.cs create mode 100644 src/Tgstation.Server.Api/Models/OAuthProvider.cs create mode 100644 src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs create mode 100644 src/Tgstation.Server.Host/Models/OAuthConnection.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs diff --git a/build/Version.props b/build/Version.props index 220cd98461..6898cf6b0c 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,10 +3,10 @@ - 4.6.0 - 2.1.1 - 7.4.0 - 8.4.0 + 4.7.0 + 2.2.0 + 8.0.0 + 9.0.0 5.2.8 1.1.0 1.1.1 diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index cecfa88a5a..5bc550d4d7 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Net.Mime; using System.Reflection; using System.Text; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Api { @@ -17,25 +18,35 @@ namespace Tgstation.Server.Api public sealed class ApiHeaders { /// - /// The header key + /// The header key. /// public const string ApiVersionHeader = "Api"; /// - /// The header key + /// The header key. /// public const string InstanceIdHeader = "Instance"; + /// + /// The header key. + /// + public const string OAuthProviderHeader = "OAuthProvider"; + /// /// The JWT authentication header scheme /// - public const string JwtAuthenticationScheme = "bearer"; + public const string BearerAuthenticationScheme = "bearer"; /// /// The JWT authentication header scheme /// public const string BasicAuthenticationScheme = "basic"; + /// + /// The JWT authentication header scheme + /// + public const string OAuthAuthenticationScheme = "oauth"; + /// /// The current /// @@ -47,7 +58,7 @@ namespace Tgstation.Server.Api public static readonly Version Version = AssemblyName.Version.Semver(); /// - /// The instance being accessed + /// The instance being accessed /// public long? InstanceId { get; set; } @@ -82,9 +93,14 @@ namespace Tgstation.Server.Api public string? Password { get; } /// - /// If the header uses password or JWT authentication + /// The the is for, if any. /// - public bool IsTokenAuthentication => Token != null; + public OAuthProvider? OAuthProvider { get; } + + /// + /// If the header uses password or TGS JWT authentication. + /// + public bool IsTokenAuthentication => Token != null && !OAuthProvider.HasValue; /// /// Checks if a given is compatible with our own @@ -98,12 +114,15 @@ namespace Tgstation.Server.Api /// /// The value of /// The value of - public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null) + /// The value of . + public ApiHeaders(ProductHeaderValue userAgent, string token, OAuthProvider? oauthProvider = null) : this(userAgent, token, null, null) { if (userAgent == null) throw new ArgumentNullException(nameof(userAgent)); if (token == null) throw new ArgumentNullException(nameof(token)); + + OAuthProvider = oauthProvider; } /// @@ -191,7 +210,20 @@ namespace Tgstation.Server.Api switch (scheme.ToLowerInvariant()) #pragma warning restore CA1308 // Normalize strings to uppercase { - case JwtAuthenticationScheme: + case OAuthAuthenticationScheme: + if (requestHeaders.Headers.TryGetValue(OAuthProviderHeader, out StringValues oauthProviderValues)) + { + var oauthProviderString = oauthProviderValues.First(); + if (Enum.TryParse(oauthProviderString, out var oauthProvider)) + OAuthProvider = oauthProvider; + else + AddError(HeaderTypes.OAuthProvider, "Invalid OAuth provider!"); + } + else + AddError(HeaderTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!"); + + goto case BearerAuthenticationScheme; + case BearerAuthenticationScheme: Token = parameter; break; case BasicAuthenticationScheme: @@ -253,7 +285,7 @@ namespace Tgstation.Server.Api /// Set using the . This initially clears /// /// The to set - /// The instance for the request + /// The instance for the request public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null) { if (headers == null) @@ -263,12 +295,16 @@ namespace Tgstation.Server.Api headers.Clear(); headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - if (IsTokenAuthentication) - headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token); - else + if (!IsTokenAuthentication) headers.Authorization = new AuthenticationHeaderValue( BasicAuthenticationScheme, Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))); + else + { + headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token); + if (OAuthProvider.HasValue) + headers.Add(OAuthProviderHeader, OAuthProvider.ToString()); + } headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent)); headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString()); diff --git a/src/Tgstation.Server.Api/HeaderTypes.cs b/src/Tgstation.Server.Api/HeaderTypes.cs index a1d0a6e17e..f9a10739da 100644 --- a/src/Tgstation.Server.Api/HeaderTypes.cs +++ b/src/Tgstation.Server.Api/HeaderTypes.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Tgstation.Server.Api { @@ -24,13 +24,18 @@ namespace Tgstation.Server.Api Accept = 2, /// - /// Api header. + /// . /// Api = 4, /// /// /// - Authorization = 8 + Authorization = 8, + + /// + /// . + /// + OAuthProvider, } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 700ac27969..48517ea1cd 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -582,5 +582,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The requested port is either already in use by TGS or could not be allocated!")] PortNotAvailable, + + /// + /// Attempted to set for the admin user. + /// + [Description("The admin user cannot use OAuth connections!")] + AdminUserCannotOAuth, } } diff --git a/src/Tgstation.Server.Api/Models/OAuthConnection.cs b/src/Tgstation.Server.Api/Models/OAuthConnection.cs new file mode 100644 index 0000000000..973d2a1e6e --- /dev/null +++ b/src/Tgstation.Server.Api/Models/OAuthConnection.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Api.Models +{ + /// + /// Represents a valid OAuth connection. + /// + public class OAuthConnection + { + /// + /// The of the . + /// ] + [EnumDataType(typeof(OAuthProvider))] + public OAuthProvider Provider { get; set; } + + /// + /// The ID of the user in the . + /// + public ulong ExternalUserId { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs new file mode 100644 index 0000000000..fd404f3ef1 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// List of OAuth providers supported by TGS + /// + public enum OAuthProvider + { + /// + /// https://github.com + /// + GitHub, + } +} diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs index 60ecb7225d..9ac62d975e 100644 --- a/src/Tgstation.Server.Api/Models/User.cs +++ b/src/Tgstation.Server.Api/Models/User.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Api.Models +using System.Collections.Generic; + +namespace Tgstation.Server.Api.Models { /// public class User : Internal.User @@ -17,5 +19,10 @@ /// The who created this /// public Internal.User? CreatedBy { get; set; } + + /// + /// List of s associated with the . + /// + public ICollection? OAuthConnections { get; set; } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs index 02b984d7f0..73d9663d8f 100644 --- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs +++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs @@ -42,5 +42,10 @@ namespace Tgstation.Server.Api.Rights /// User can list and download s. /// DownloadLogs = 32, + + /// + /// User can modify their own . + /// + EditOwnOAuthConnections = 64, } } diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 79d13146a3..03fa86ccf3 100644 --- a/src/Tgstation.Server.Host/.config/dotnet-tools.json +++ b/src/Tgstation.Server.Host/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "3.1.7", + "version": "3.1.10", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs new file mode 100644 index 0000000000..a40ebf431d --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Host.Configuration +{ + /// + /// OAuth options for GitHub + /// + sealed class GitHubOAuthConfiguration + { + /// + /// The GitHub client ID. + /// + public string ClientId { get; set; } + + /// + /// The GitHub client secret. + /// + public string ClientSecret { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs index c0101eeca9..c6a74f2e4a 100644 --- a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs @@ -1,3 +1,5 @@ +using Tgstation.Server.Api.Models; + namespace Tgstation.Server.Host.Configuration { /// @@ -15,6 +17,11 @@ namespace Tgstation.Server.Host.Configuration /// const uint DefaultTokenExpiryMinutes = 15; + /// + /// Default value of . + /// + const uint DefaultOAuthTokenExpiryMinutes = 60 * 24; // 1 day + /// /// Default value of . /// @@ -26,12 +33,12 @@ namespace Tgstation.Server.Host.Configuration const uint DefaultTokenSigningKeyByteAmount = 256; /// - /// Amount of minutes until generated s expire. + /// Amount of minutes until s generated from passwords expire. /// public uint TokenExpiryMinutes { get; set; } = DefaultTokenExpiryMinutes; /// - /// Amount of minutes to skew the clock for validation. + /// Amount of minutes to skew the clock for validation. /// public uint TokenClockSkewMinutes { get; set; } = DefaultTokenClockSkewMinutes; @@ -40,9 +47,19 @@ namespace Tgstation.Server.Host.Configuration /// public uint TokenSigningKeyByteCount { get; set; } = DefaultTokenSigningKeyByteAmount; + /// + /// Amount of minutes until s generated from OAuth logins expire. + /// + public uint OAuthTokenExpiryMinutes { get; set; } = DefaultOAuthTokenExpiryMinutes; + /// /// A custom token signing key. Overrides . /// public string CustomTokenSigningKeyBase64 { get; set; } + + /// + /// OAuth options for GitHub. + /// + public GitHubOAuthConfiguration GitHubOAuth { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index c35de94da2..6095e2a3ba 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Octokit; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -115,14 +113,6 @@ namespace Tgstation.Server.Host.Controllers fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } - ObjectResult RateLimit(RateLimitExceededException exception) - { - Logger.LogWarning(exception, "Exceeded GitHub rate limit!"); - var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture); - Response.Headers.Add("Retry-After", new StringValues(secondsString)); - return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit)); - } - /// /// Try to download and apply an update with a given . /// diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 90a5c5f448..c42eee8d39 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -2,8 +2,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Octokit; using Serilog.Context; using System; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Mime; @@ -60,7 +63,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The value of /// The value of - public ApiController( + protected ApiController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, @@ -115,6 +118,22 @@ namespace Tgstation.Server.Host.Controllers /// A with the given . protected ObjectResult Created(object payload) => StatusCode((int)HttpStatusCode.Created, payload); + /// + /// 429 response for a given . + /// + /// The that occurred. + /// A . + protected ObjectResult RateLimit(RateLimitExceededException rateLimitException) + { + if (rateLimitException == null) + throw new ArgumentNullException(nameof(rateLimitException)); + + Logger.LogWarning(rateLimitException, "Exceeded GitHub rate limit!"); + var secondsString = Math.Ceiling((rateLimitException.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture); + Response.Headers.Add(HeaderNames.RetryAfter, secondsString); + return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit)); + } + /// /// Performs validation a request. /// diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 234f5299f7..4243fe9ad7 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using Octokit; using System; using System.Linq; using System.Threading; @@ -17,6 +18,7 @@ using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Security.OAuth; using Tgstation.Server.Host.System; using Wangkanai.Detection; @@ -53,6 +55,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly IIdentityCache identityCache; + /// + /// The for the . + /// + readonly IOAuthProviders oAuthProviders; + /// /// The for the /// @@ -78,6 +85,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of + /// The value of . /// The value of /// The containing the value of . /// The containing the value of @@ -90,6 +98,7 @@ namespace Tgstation.Server.Host.Controllers ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider, IIdentityCache identityCache, + IOAuthProviders oAuthProviders, IBrowserResolver browserResolver, IOptions generalConfigurationOptions, IOptions controlPanelConfigurationOptions, @@ -106,6 +115,7 @@ namespace Tgstation.Server.Host.Controllers this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); + this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders)); this.browserResolver = browserResolver; generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); @@ -140,7 +150,7 @@ namespace Tgstation.Server.Host.Controllers if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic) { Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), redirecting to control panel...", browserResolver.UserAgent); - return Redirect(Application.ControlPanelRoute); + return Redirect(Core.Application.ControlPanelRoute); } return ApiHeaders == null ? HeadersIssue() : Unauthorized(); @@ -168,26 +178,56 @@ namespace Tgstation.Server.Host.Controllers if (ApiHeaders.IsTokenAuthentication) return BadRequest(new ErrorMessage(ErrorCode.TokenWithToken)); - ISystemIdentity systemIdentity; - try - { - // trust the system over the database because a user's name can change while still having the same SID - systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false); - } - catch (NotImplementedException) - { - systemIdentity = null; - } + var oAuthLogin = ApiHeaders.OAuthProvider.HasValue; + + ISystemIdentity systemIdentity = null; + if (!oAuthLogin) + try + { + // trust the system over the database because a user's name can change while still having the same SID + systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false); + } + catch (NotImplementedException ex) + { + Logger.LogTrace(ex, "System identities not implemented!"); + } using (systemIdentity) { // Get the user from the database IQueryable query = DatabaseContext.Users.AsQueryable(); - string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username); - if (systemIdentity == null) - query = query.Where(x => x.CanonicalName == canonicalName); + if (oAuthLogin) + { + ulong? externalUserId; + try + { + externalUserId = await oAuthProviders + .GetValidator(ApiHeaders.OAuthProvider.Value) + .ValidateResponseCode(ApiHeaders.Token, cancellationToken) + .ConfigureAwait(false); + } + catch (RateLimitExceededException ex) + { + return RateLimit(ex); + } + + if (!externalUserId.HasValue) + return Unauthorized(); + + query = query.Where( + x => x.OAuthConnections.Any( + y => y.Provider == ApiHeaders.OAuthProvider.Value + && y.ExternalUserId == externalUserId.Value)); + } else - query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid); + { + string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username); + if (systemIdentity == null) + query = query.Where(x => x.CanonicalName == canonicalName); + else + query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid); + } + var users = await query.Select(x => new Models.User { Id = x.Id, @@ -213,32 +253,33 @@ namespace Tgstation.Server.Host.Controllers var originalHash = user.PasswordHash; var isDbUser = originalHash != null; bool usingSystemIdentity = systemIdentity != null && !isDbUser; - if (!usingSystemIdentity) - { - // DB User password check and update - if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password)) - return Unauthorized(); - if (user.PasswordHash != originalHash) + if (!oAuthLogin) + if (!usingSystemIdentity) { - Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id); - var updatedUser = new Models.User + // DB User password check and update + if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password)) + return Unauthorized(); + if (user.PasswordHash != originalHash) { - Id = user.Id - }; - DatabaseContext.Users.Attach(updatedUser); - updatedUser.PasswordHash = user.PasswordHash; + Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id); + var updatedUser = new Models.User + { + Id = user.Id + }; + DatabaseContext.Users.Attach(updatedUser); + updatedUser.PasswordHash = user.PasswordHash; + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + } + } + else if (systemIdentity.Username != user.Name) + { + // System identity username change update + Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id); + DatabaseContext.Users.Attach(user); + user.Name = systemIdentity.Username; + user.CanonicalName = Models.User.CanonicalizeName(user.Name); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); } - } - else if (systemIdentity.Username != user.Name) - { - // System identity username change update - Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id); - DatabaseContext.Users.Attach(user); - user.Name = systemIdentity.Username; - user.CanonicalName = Models.User.CanonicalizeName(user.Name); - await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - } // Now that the bookeeping is done, tell them to fuck off if necessary if (!user.Enabled.Value) @@ -247,7 +288,7 @@ namespace Tgstation.Server.Host.Controllers return Forbid(); } - var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false); + var token = await tokenFactory.CreateToken(user, oAuthLogin, cancellationToken).ConfigureAwait(false); if (usingSystemIdentity) { // expire the identity slightly after the auth token in case of lag @@ -262,6 +303,6 @@ namespace Tgstation.Server.Host.Controllers return Json(token); } } - #pragma warning restore CA1506 +#pragma warning restore CA1506 } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index d5926406e6..15c325389e 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -281,7 +281,9 @@ namespace Tgstation.Server.Host.Controllers } } +#pragma warning disable CA1508 // Avoid dead conditional code if (earlyOut != null) +#pragma warning restore CA1508 // Avoid dead conditional code return earlyOut; // Last test, ensure it's in the list of valid paths @@ -420,9 +422,9 @@ namespace Tgstation.Server.Host.Controllers var moveJob = await InstanceQuery() .SelectMany(x => x.Jobs). -#pragma warning disable CA1307 // Specify StringComparison +#pragma warning disable CA1310 // Specify StringComparison Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) -#pragma warning restore CA1307 // Specify StringComparison +#pragma warning restore CA1310 // Specify StringComparison .Select(x => new Models.Job { Id = x.Id @@ -614,9 +616,9 @@ namespace Tgstation.Server.Host.Controllers var moveJobs = await GetBaseQuery() .SelectMany(x => x.Jobs) -#pragma warning disable CA1307 // Specify StringComparison +#pragma warning disable CA1310 // Specify StringComparison .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) -#pragma warning restore CA1307 // Specify StringComparison +#pragma warning restore CA1310 // Specify StringComparison .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy) .Include(x => x.Instance) .ToListAsync(cancellationToken) @@ -687,9 +689,9 @@ namespace Tgstation.Server.Host.Controllers var moveJob = await QueryForUser() .SelectMany(x => x.Jobs) -#pragma warning disable CA1307 // Specify StringComparison +#pragma warning disable CA1310 // Specify StringComparison .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) -#pragma warning restore CA1307 // Specify StringComparison +#pragma warning restore CA1310 // Specify StringComparison .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 88de87bbce..1cc8604d72 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -426,7 +426,9 @@ namespace Tgstation.Server.Host.Controllers || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) return Forbid(); - if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0) +#pragma warning disable CA1508 // Avoid dead conditional code + if (model.AccessToken?.Length == 0 && model.AccessUser?.Length == 0) +#pragma warning restore CA1508 // Avoid dead conditional code { // setting an empty string clears everything currentModel.AccessUser = null; diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 485216faa4..d02249c4a1 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -74,7 +74,9 @@ namespace Tgstation.Server.Host.Controllers BadRequestObjectResult CheckValidName(UserUpdate model, bool newUser) { var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null; +#pragma warning disable CA1508 // https://github.com/dotnet/roslyn-analyzers/issues/3685 if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name))) +#pragma warning restore CA1508 return BadRequest(new ErrorMessage(ErrorCode.UserMissingName)); model.Name = model.Name?.Trim(); @@ -118,6 +120,9 @@ namespace Tgstation.Server.Host.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); + if (model.OAuthConnections?.Any(x => x == null) == true) + return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); + if (!(model.Password == null ^ model.SystemIdentifier == null)) return BadRequest(new ErrorMessage(ErrorCode.UserMismatchPasswordSid)); @@ -132,17 +137,7 @@ namespace Tgstation.Server.Host.Controllers if (fail != null) return fail; - var dbUser = new Models.User - { - AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None), - CreatedAt = DateTimeOffset.Now, - CreatedBy = AuthenticationContext.User, - Enabled = model.Enabled ?? false, - InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None), - Name = model.Name, - SystemIdentifier = model.SystemIdentifier, - InstanceUsers = new List() - }; + var dbUser = CreateNewUserFromModel(model); if (model.SystemIdentifier != null) try @@ -157,7 +152,7 @@ namespace Tgstation.Server.Host.Controllers { return RequiresPosixSystemIdentity(); } - else + else if (!(model.Password?.Length == 0 && model.OAuthConnections.Count != 0)) { var result = TrySetPassword(dbUser, model.Password, true); if (result != null) @@ -170,6 +165,8 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + Logger.LogInformation("Created new user {0} ({1})", dbUser.Name, dbUser.Id); + return Created(dbUser.ToApi(true)); } @@ -182,7 +179,7 @@ namespace Tgstation.Server.Host.Controllers /// updated successfully. /// Requested does not exist. [HttpPost] - [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] + [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)] [ProducesResponseType(typeof(Api.Models.User), 200)] [ProducesResponseType(typeof(ErrorMessage), 404)] #pragma warning disable CA1502 // TODO: Decomplexify @@ -192,19 +189,22 @@ namespace Tgstation.Server.Host.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); - if (!model.Id.HasValue) + if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); - var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); + var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); + var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword); + var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnOAuthConnections); - var originalUser = passwordEditOnly + var originalUser = passwordEdit ? AuthenticationContext.User : await DatabaseContext .Users .AsQueryable() .Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -214,13 +214,15 @@ namespace Tgstation.Server.Host.Controllers if (originalUser.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return Forbid(); - // Ensure they are only trying to edit password (system identity change will trigger a bad request) - if (passwordEditOnly + // Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request) + if ((!canEditAllUsers && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.Name != null)) + || (!passwordEdit && model.Password != null) + || (!oAuthEdit && model.OAuthConnections != null)) return Forbid(); if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) @@ -246,6 +248,22 @@ namespace Tgstation.Server.Host.Controllers originalUser.Enabled = model.Enabled.Value; } + if (model.OAuthConnections != null + && (model.OAuthConnections.Count != originalUser.OAuthConnections.Count + || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId)))) + { + if (originalUser.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName)) + return BadRequest(new ErrorMessage(ErrorCode.AdminUserCannotOAuth)); + + originalUser.OAuthConnections.Clear(); + foreach (var updatedConnection in model.OAuthConnections) + originalUser.OAuthConnections.Add(new Models.OAuthConnection + { + Provider = updatedConnection.Provider, + ExternalUserId = updatedConnection.ExternalUserId + }); + } + var fail = CheckValidName(model, false); if (fail != null) return fail; @@ -254,6 +272,8 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + Logger.LogInformation("Updated user {0} ({1})", originalUser.Name, originalUser.Id); + // return id only if not a self update and cannot read users return Json( model.Id == originalUser.Id @@ -293,6 +313,7 @@ namespace Tgstation.Server.Host.Controllers .AsQueryable() .Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections) .ToListAsync(cancellationToken).ConfigureAwait(false); return Json(users.Select(x => x.ToApi(true))); } @@ -321,6 +342,7 @@ namespace Tgstation.Server.Host.Controllers .AsQueryable() .Where(x => x.Id == id) .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) return NotFound(); @@ -330,5 +352,30 @@ namespace Tgstation.Server.Host.Controllers return Json(user.ToApi(true)); } + + /// + /// Creates a new from a given . + /// + /// The to use as a template. + /// A new . + Models.User CreateNewUserFromModel(Api.Models.User model) => new Models.User + { + AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None), + CreatedAt = DateTimeOffset.Now, + CreatedBy = AuthenticationContext.User, + Enabled = model.Enabled ?? false, + InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None), + Name = model.Name, + SystemIdentifier = model.SystemIdentifier, + InstanceUsers = new List(), + OAuthConnections = model + .OAuthConnections + ?.Select(x => new Models.OAuthConnection + { + Provider = x.Provider, + ExternalUserId = x.ExternalUserId + }) + .ToList() + }; } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 3511b8248f..c1294255b9 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -37,6 +37,7 @@ using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Properties; using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Security.OAuth; using Tgstation.Server.Host.Setup; using Tgstation.Server.Host.System; @@ -100,7 +101,6 @@ namespace Tgstation.Server.Host.Core // configure configuration services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); - services.UseStandardConfig(Configuration); // enable options which give us config reloading services.AddOptions(); @@ -159,24 +159,29 @@ namespace Tgstation.Server.Host.Core }); // configure bearer token validation - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions => - { - // this line isn't actually run until the first request is made - // at that point tokenFactory will be populated - jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters; - jwtBearerOptions.Events = new JwtBearerEvents + var authenticationBuilder = services + .AddAuthentication(options => { - // Application is our composition root so this monstrosity of a line is okay - // At least, that's what I tell myself to sleep at night - OnTokenValidated = ctx => ctx - .HttpContext - .RequestServices - .GetRequiredService() - .InjectClaimsIntoContext( - ctx, - ctx.HttpContext.RequestAborted) - }; - }); + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(jwtBearerOptions => + { + // this line isn't actually run until the first request is made + // at that point tokenFactory will be populated + jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters; + jwtBearerOptions.Events = new JwtBearerEvents + { + // Application is our composition root so this monstrosity of a line is okay + // At least, that's what I tell myself to sleep at night + OnTokenValidated = ctx => ctx + .HttpContext + .RequestServices + .GetRequiredService() + .InjectClaimsIntoContext( + ctx, + ctx.HttpContext.RequestAborted) + }; + }); // WARNING: STATIC CODE // fucking prevents converting 'sub' to M$ bs @@ -260,6 +265,7 @@ namespace Tgstation.Server.Host.Core // configure security services services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index b928dd6f2a..2d5a07a408 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -176,7 +176,7 @@ namespace Tgstation.Server.Host.Core In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Name = HeaderNames.Authorization, - Scheme = ApiHeaders.JwtAuthenticationScheme + Scheme = ApiHeaders.BearerAuthenticationScheme }); } diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 85f00bd68a..1c0eb0b5ee 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -87,10 +87,15 @@ namespace Tgstation.Server.Host.Database public DbSet TestMerges { get; set; } /// - /// The s om the + /// The s in the /// public DbSet RevInfoTestMerges { get; set; } + /// + /// The s in the + /// + public DbSet OAuthConnections { get; set; } + /// /// The for the / foreign key. /// @@ -132,6 +137,9 @@ namespace Tgstation.Server.Host.Database /// IDatabaseCollection IDatabaseContext.ReattachInformations => reattachInformationsCollection; + /// + IDatabaseCollection IDatabaseContext.OAuthConnections => oAuthConnections; + /// /// Backing field for . /// @@ -192,6 +200,11 @@ namespace Tgstation.Server.Host.Database /// readonly IDatabaseCollection reattachInformationsCollection; + /// + /// Backing field for . + /// + readonly IDatabaseCollection oAuthConnections; + /// /// Gets the configure action for a given . /// @@ -216,7 +229,7 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the . - public DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) + protected DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { usersCollection = new DatabaseCollection(Users); instancesCollection = new DatabaseCollection(Instances); @@ -230,6 +243,7 @@ namespace Tgstation.Server.Host.Database revisionInformationsCollection = new DatabaseCollection(RevisionInformations); jobsCollection = new DatabaseCollection(Jobs); reattachInformationsCollection = new DatabaseCollection(ReattachInformations); + oAuthConnections = new DatabaseCollection(OAuthConnections); } /// @@ -244,6 +258,9 @@ namespace Tgstation.Server.Host.Database userModel.HasIndex(x => x.CanonicalName).IsUnique(); userModel.HasIndex(x => x.SystemIdentifier).IsUnique(); userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict); + userModel.HasMany(x => x.OAuthConnections).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity().HasIndex(x => new { x.Provider, x.ExternalUserId }).IsUnique(); modelBuilder.Entity().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique(); @@ -316,6 +333,7 @@ namespace Tgstation.Server.Host.Database } /// +#pragma warning disable CA1502 // Cyclomatic complexity public async Task SchemaDowngradeForServerVersion( ILogger logger, Version version, @@ -338,7 +356,43 @@ namespace Tgstation.Server.Host.Database if (version < new Version(4, 1, 0)) throw new NotSupportedException("Cannot migrate below version 4.1.0!"); - if(version < new Version(4, 4, 0)) + if (version < new Version(4, 6, 0)) + switch (currentDatabaseType) + { + case DatabaseType.MariaDB: + case DatabaseType.MySql: + targetMigration = nameof(MYAddAdditionalDDParameters); + break; + case DatabaseType.PostgresSql: + targetMigration = nameof(PGAddAdditionalDDParameters); + break; + case DatabaseType.SqlServer: + case DatabaseType.Sqlite: + targetMigration = nameof(MSAddAdditionalDDParameters); + break; + default: + throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); + } + + if (version < new Version(4, 5, 0)) + switch (currentDatabaseType) + { + case DatabaseType.MariaDB: + case DatabaseType.MySql: + targetMigration = nameof(MYAddDeploymentColumns); + break; + case DatabaseType.PostgresSql: + targetMigration = nameof(PGAddDeploymentColumns); + break; + case DatabaseType.SqlServer: + case DatabaseType.Sqlite: + targetMigration = nameof(MSAddDeploymentColumns); + break; + default: + throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); + } + + if (version < new Version(4, 4, 0)) switch (currentDatabaseType) { case DatabaseType.MariaDB: @@ -403,5 +457,6 @@ namespace Tgstation.Server.Host.Database logger.LogCritical(e, "Failed to migrate!"); } } +#pragma warning restore CA1502 // Cyclomatic complexity } } diff --git a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs index b7679599cc..2057196e65 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Threading; @@ -14,65 +14,70 @@ namespace Tgstation.Server.Host.Database public interface IDatabaseContext { /// - /// The s in the + /// The s in the . /// IDatabaseCollection Users { get; } /// - /// The s in the + /// The s in the . /// IDatabaseCollection Instances { get; } /// - /// The s in the + /// The s in the . /// IDatabaseCollection InstanceUsers { get; } /// - /// The s in the + /// The s in the . /// IDatabaseCollection Jobs { get; } /// - /// The s in the + /// The s in the . /// IDatabaseCollection CompileJobs { get; } /// - /// The s in the + /// The s in the . /// IDatabaseCollection RevisionInformations { get; } /// - /// The in the + /// The in the . /// IDatabaseCollection DreamMakerSettings { get; } /// - /// The in the + /// The in the . /// IDatabaseCollection DreamDaemonSettings { get; } /// - /// The s in the + /// The s in the . /// IDatabaseCollection ChatBots { get; } /// - /// The in the + /// The in the . /// IDatabaseCollection ChatChannels { get; } /// - /// The in the + /// The in the . /// IDatabaseCollection RepositorySettings { get; } /// - /// The for s + /// The for s. /// IDatabaseCollection ReattachInformations { get; } + /// + /// The for s. + /// + IDatabaseCollection OAuthConnections { get; } + /// /// Saves changes made to the /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs new file mode 100644 index 0000000000..271369ebde --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs @@ -0,0 +1,820 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20201122231219_MSAddOAuthConnections")] + partial class MSAddOAuthConnections + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("decimal(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ExternalUserId") + .HasColumnType("decimal(20,0)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs new file mode 100644 index 0000000000..e99c868e17 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the OAuthConnections table for MSSQL. + /// + public partial class MSAddOAuthConnections : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.CreateTable( + name: "OAuthConnections", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Provider = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false), + UserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OAuthConnections", x => x.Id); + table.ForeignKey( + name: "FK_OAuthConnections_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_UserId", + table: "OAuthConnections", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_Provider_ExternalUserId", + table: "OAuthConnections", + columns: new[] { "Provider", "ExternalUserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropTable( + name: "OAuthConnections"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs new file mode 100644 index 0000000000..05e0d25108 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs @@ -0,0 +1,809 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20201122231327_MYAddOAuthConnections")] + partial class MYAddOAuthConnections + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("bigint unsigned"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .HasColumnType("bigint unsigned"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SystemIdentifier") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs new file mode 100644 index 0000000000..422a4f98b8 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the OAuthConnections table for MYSQL. + /// + public partial class MYAddOAuthConnections : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.CreateTable( + name: "OAuthConnections", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Provider = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false), + UserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OAuthConnections", x => x.Id); + table.ForeignKey( + name: "FK_OAuthConnections_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_UserId", + table: "OAuthConnections", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_Provider_ExternalUserId", + table: "OAuthConnections", + columns: new[] { "Provider", "ExternalUserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropTable( + name: "OAuthConnections"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs new file mode 100644 index 0000000000..9bbc1ecd6b --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs @@ -0,0 +1,817 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20201122231443_PGAddOAuthConnections")] + partial class PGAddOAuthConnections + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("numeric(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ExternalUserId") + .HasColumnType("numeric(20,0)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs new file mode 100644 index 0000000000..41c30fd23f --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the OAuthConnections table for PostgresSQL. + /// + public partial class PGAddOAuthConnections : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.CreateTable( + name: "OAuthConnections", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Provider = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false), + UserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OAuthConnections", x => x.Id); + table.ForeignKey( + name: "FK_OAuthConnections_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_UserId", + table: "OAuthConnections", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_Provider_ExternalUserId", + table: "OAuthConnections", + columns: new[] { "Provider", "ExternalUserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropTable( + name: "OAuthConnections"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs new file mode 100644 index 0000000000..73e8d1eb6c --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs @@ -0,0 +1,808 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20201122231546_SLAddOAuthConnections")] + partial class SLAddOAuthConnections + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondRights") + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstanceUserRights") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs new file mode 100644 index 0000000000..43c62a778f --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the OAuthConnections table for SQLite. + /// + public partial class SLAddOAuthConnections : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.CreateTable( + name: "OAuthConnections", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Provider = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false), + UserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OAuthConnections", x => x.Id); + table.ForeignKey( + name: "FK_OAuthConnections_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_UserId", + table: "OAuthConnections", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OAuthConnections_Provider_ExternalUserId", + table: "OAuthConnections", + columns: new[] { "Provider", "ExternalUserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropTable( + name: "OAuthConnections"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index e16999c281..dc88d0e657 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.7") + .HasAnnotation("ProductVersion", "3.1.10") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => @@ -373,6 +373,31 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Jobs"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .HasColumnType("bigint unsigned"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -705,6 +730,14 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index b19d291ae9..606f3e08b5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Database.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.7") + .HasAnnotation("ProductVersion", "3.1.10") .HasAnnotation("Relational:MaxIdentifierLength", 63); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => @@ -374,6 +374,32 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Jobs"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ExternalUserId") + .HasColumnType("numeric(20,0)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -712,6 +738,14 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index 2c468c069a..c9499288a9 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.7") + .HasAnnotation("ProductVersion", "3.1.10") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); @@ -376,6 +376,32 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Jobs"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ExternalUserId") + .HasColumnType("decimal(20,0)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -715,6 +741,14 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index de2eca1c74..ded21a4fb2 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.7"); + .HasAnnotation("ProductVersion", "3.1.10"); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { @@ -372,6 +372,31 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Jobs"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -704,6 +729,14 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") diff --git a/src/Tgstation.Server.Host/Models/OAuthConnection.cs b/src/Tgstation.Server.Host/Models/OAuthConnection.cs new file mode 100644 index 0000000000..590849a538 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/OAuthConnection.cs @@ -0,0 +1,26 @@ +namespace Tgstation.Server.Host.Models +{ + /// + public sealed class OAuthConnection : Api.Models.OAuthConnection + { + /// + /// The row Id. + /// + public long Id { get; set; } + + /// + /// The owning . + /// + public User User { get; set; } + + /// + /// Convert the to it's API form. + /// + /// A new . + public Api.Models.OAuthConnection ToApi() => new Api.Models.OAuthConnection + { + Provider = Provider, + ExternalUserId = ExternalUserId + }; + } +} diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index ed1ec273aa..5d4783dcf0 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -48,6 +48,11 @@ namespace Tgstation.Server.Host.Models /// public ICollection TestMerges { get; set; } + /// + /// The s made by the + /// + public ICollection OAuthConnections { get; set; } + /// /// Change a into a . /// diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs index 9ee9ba309c..90177bb524 100644 --- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -19,8 +19,9 @@ namespace Tgstation.Server.Host.Security /// Create a for a given /// /// The to create the token for. Must have the field available + /// Whether or not this is an OAuth login. /// The for the operation /// A resulting in a new - Task CreateToken(Models.User user, CancellationToken cancellationToken); + Task CreateToken(Models.User user, bool oAuth, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs new file mode 100644 index 0000000000..d1d1284854 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; +using Octokit; +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// for GitHub. + /// + sealed class GitHubOAuthValidator : IOAuthValidator + { + /// + public OAuthProvider Provider => OAuthProvider.GitHub; + + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly GitHubOAuthConfiguration gitHubOAuthConfiguration; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + public GitHubOAuthValidator( + IGitHubClientFactory gitHubClientFactory, + ILogger logger, + GitHubOAuthConfiguration gitHubOAuthConfiguration) + { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.gitHubOAuthConfiguration = gitHubOAuthConfiguration ?? throw new ArgumentNullException(nameof(gitHubOAuthConfiguration)); + } + + /// + public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) + { + if (code == null) + throw new ArgumentNullException(nameof(code)); + + var client = gitHubClientFactory.CreateClient(); + try + { + logger.LogTrace("Validating response code..."); + var response = await client + .Oauth + .CreateAccessToken( + new OauthTokenRequest( + gitHubOAuthConfiguration.ClientId, + gitHubOAuthConfiguration.ClientSecret, + code)) + .ConfigureAwait(false); + + var token = response.AccessToken; + if (token == null) + return null; + + var authenticatedClient = gitHubClientFactory.CreateClient(token); + + logger.LogTrace("Getting user details..."); + var userDetails = await authenticatedClient + .User + .Current() + .ConfigureAwait(false); + + return (ulong)userDetails.Id; + } + catch (RateLimitExceededException) + { + throw; + } + catch(ApiException ex) + { + logger.LogWarning(ex, "API error while completing OAuth handshake!"); + return null; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs new file mode 100644 index 0000000000..2468b36aa3 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -0,0 +1,17 @@ +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Contains s + /// + public interface IOAuthProviders + { + /// + /// Gets the for a given . + /// + /// The to get the validator for. + /// The for . + IOAuthValidator GetValidator(OAuthProvider oAuthProvider); + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs new file mode 100644 index 0000000000..5457af53d3 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Validates OAuth responses for a given . + /// + public interface IOAuthValidator + { + /// + /// The this validator is for. + /// + OAuthProvider Provider { get; } + + /// + /// Validate a given OAuth response . + /// + /// The OAuth response string from web application. + /// The for the operation. + /// A resulting in if authentication failed, if a rate limit occurred, and the validated otherwise. + Task ValidateResponseCode(string code, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs new file mode 100644 index 0000000000..cf0185e03f --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + sealed class OAuthProviders : IOAuthProviders + { + /// + /// The of s. + /// + readonly IReadOnlyCollection validators; + + /// + /// Initializes a new instance of the . + /// + /// The to use. + /// The to use. + /// The containing the to use. + public OAuthProviders( + IGitHubClientFactory gitHubClientFactory, + ILoggerFactory loggerFactory, + IOptions securityConfigurationOptions) + { + if (loggerFactory == null) + throw new ArgumentNullException(nameof(loggerFactory)); + + var securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); + + var validatorsBuilder = new List(); + + if (securityConfiguration.GitHubOAuth != null) + validatorsBuilder.Add( + new GitHubOAuthValidator( + gitHubClientFactory, + loggerFactory.CreateLogger(), + securityConfiguration.GitHubOAuth)); + + validators = validatorsBuilder; + } + + /// + public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.First(x => x.Provider == oAuthProvider); + } +} diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 40a898abd5..2c9a86df45 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Security } /// - public async Task CreateToken(Models.User user, CancellationToken cancellationToken) + public async Task CreateToken(Models.User user, bool oAuth, CancellationToken cancellationToken) { if (user == null) throw new ArgumentNullException(nameof(user)); @@ -93,7 +93,9 @@ namespace Tgstation.Server.Host.Security if (nowUnix == lpuUnix) await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - var expiry = now.AddMinutes(securityConfiguration.TokenExpiryMinutes); + var expiry = now.AddMinutes(oAuth + ? securityConfiguration.OAuthTokenExpiryMinutes + : securityConfiguration.TokenExpiryMinutes); var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, user.Id.Value.ToString(CultureInfo.InvariantCulture)), diff --git a/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs b/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs index acc04b6518..edc85dea40 100644 --- a/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs +++ b/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs @@ -1,4 +1,4 @@ -using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Setup @@ -18,6 +18,11 @@ namespace Tgstation.Server.Host.Setup /// DatabaseConfiguration DatabaseConfiguration { get; } + /// + /// The . + /// + SecurityConfiguration SecurityConfiguration { get; } + /// /// The . /// diff --git a/src/Tgstation.Server.Host/Setup/PostSetupServices.cs b/src/Tgstation.Server.Host/Setup/PostSetupServices.cs index f595e3ba65..1297381ef2 100644 --- a/src/Tgstation.Server.Host/Setup/PostSetupServices.cs +++ b/src/Tgstation.Server.Host/Setup/PostSetupServices.cs @@ -18,6 +18,9 @@ namespace Tgstation.Server.Host.Setup /// public DatabaseConfiguration DatabaseConfiguration => databaseConfigurationOptions.Value; + /// + public SecurityConfiguration SecurityConfiguration => securityConfigurationOptions.Value; + /// public FileLoggingConfiguration FileLoggingConfiguration => fileLoggingConfigurationOptions.Value; @@ -34,6 +37,11 @@ namespace Tgstation.Server.Host.Setup /// readonly IOptions databaseConfigurationOptions; + /// + /// Backing for . + /// + readonly IOptions securityConfigurationOptions; + /// /// Backing for . /// @@ -46,12 +54,14 @@ namespace Tgstation.Server.Host.Setup /// The used to create . /// The containing the value of . /// The containing the value of . + /// The containing the value of . /// The containing the value of . public PostSetupServices( IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, IOptions generalConfigurationOptions, IOptions databaseConfigurationOptions, + IOptions securityConfigurationOptions, IOptions fileLoggingConfigurationOptions) { PlatformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); @@ -61,6 +71,7 @@ namespace Tgstation.Server.Host.Setup Logger = loggerFactory.CreateLogger(); this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.databaseConfigurationOptions = databaseConfigurationOptions ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); + this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); this.fileLoggingConfigurationOptions = fileLoggingConfigurationOptions ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } } diff --git a/src/Tgstation.Server.Host/Setup/SetupApplication.cs b/src/Tgstation.Server.Host/Setup/SetupApplication.cs index 9f848efa8f..b6a04bd536 100644 --- a/src/Tgstation.Server.Host/Setup/SetupApplication.cs +++ b/src/Tgstation.Server.Host/Setup/SetupApplication.cs @@ -62,6 +62,7 @@ namespace Tgstation.Server.Host.Setup services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); + services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); ConfigureHostedService(services); diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index f4872a4579..f8d5296d9d 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -1,4 +1,4 @@ - + @@ -66,20 +66,20 @@ - - - + + + all runtime; build; native; contentfiles; analyzers - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -87,7 +87,7 @@ - + @@ -97,13 +97,13 @@ all runtime; build; native; contentfiles; analyzers - - + + - - - - + + + + diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 63f9edf1ff..6febf17312 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -54,6 +54,7 @@ "TokenExpiryMinutes": 15, "TokenClockSkewMinutes": 1, "TokenSigningKeyByteAmount": 256, - "CustomTokenSigningKeyBase64": null + "CustomTokenSigningKeyBase64": null, + "GitHubOAuth": null } } diff --git a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs index 0f78162c50..8a310fb869 100644 --- a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs +++ b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs @@ -1,9 +1,10 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Net.Http.Headers; using System.Net.Mime; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Api.Tests { @@ -21,6 +22,7 @@ namespace Tgstation.Server.Api.Tests Assert.ThrowsException(() => new ApiHeaders(null, null)); Assert.ThrowsException(() => new ApiHeaders(productHeaderValue, null)); var headers = new ApiHeaders(productHeaderValue, String.Empty); + headers = new ApiHeaders(productHeaderValue, String.Empty, OAuthProvider.GitHub); } [TestMethod] From c7ca0ddcce30d5c78033a2711dbe390fb6277cf0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 23 Nov 2020 11:42:52 -0500 Subject: [PATCH 002/154] Set Access-Control-Max-Age to 24 hours --- src/Tgstation.Server.Host/Core/Application.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 3511b8248f..206efd22b0 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -428,7 +428,10 @@ namespace Tgstation.Server.Host.Core var originalBuilder = corsBuilder; corsBuilder = builder => { - builder.AllowAnyHeader().AllowAnyMethod(); + builder + .AllowAnyHeader() + .AllowAnyMethod() + .SetPreflightMaxAge(TimeSpan.FromDays(1)); originalBuilder?.Invoke(builder); }; applicationBuilder.UseCors(corsBuilder); From 03ebbde7d97234b06976224b3f61afc28f59a2df Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 23 Nov 2020 12:20:43 -0500 Subject: [PATCH 003/154] ODR rule cleanup --- .../Controllers/ControlPanelController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs index 506d9cc12d..275acbe91a 100644 --- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs @@ -41,9 +41,7 @@ namespace Tgstation.Server.Host.Controllers { var contentTypeProvider = new FileExtensionContentTypeProvider(); if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType)) - { - contentType = "application/octet-stream"; - } + contentType = MediaTypeNames.Application.Octet; return File(appRoute, contentType); } From 288d0db2c2b21f91397d91484c9d245aeed15a6d Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 23 Nov 2020 15:12:10 -0500 Subject: [PATCH 004/154] Adds Discord/generic OAuth among other things --- src/Tgstation.Server.Api/ApiHeaders.cs | 10 +- .../Models/OAuthProvider.cs | 5 + .../Models/ServerInformation.cs | 8 +- .../Configuration/GitHubOAuthConfiguration.cs | 18 -- .../Configuration/OAuthConfiguration.cs | 37 +++++ .../Configuration/SecurityConfiguration.cs | 7 +- .../Controllers/HomeController.cs | 25 ++- src/Tgstation.Server.Host/Core/Application.cs | 6 + .../Core/GitHubClientFactory.cs | 7 +- .../Security/OAuth/DiscordOAuthValidator.cs | 52 ++++++ .../Security/OAuth/DiscordTokenRequest.cs | 33 ++++ .../Security/OAuth/GenericOAuthValidator.cs | 157 ++++++++++++++++++ .../Security/OAuth/GitHubOAuthValidator.cs | 17 +- .../Security/OAuth/IOAuthProviders.cs | 7 + .../Security/OAuth/IOAuthValidator.cs | 5 + .../Security/OAuth/OAuthProviders.cs | 20 +++ .../Security/OAuth/OAuthTokenRequest.cs | 27 +++ .../System/AssemblyInformationProvider.cs | 6 + .../System/IAssemblyInformationProvider.cs | 6 + src/Tgstation.Server.Host/appsettings.json | 3 +- .../Core/TestGitHubClientFactory.cs | 6 +- 21 files changed, 409 insertions(+), 53 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs create mode 100644 src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 5bc550d4d7..34aa8a0476 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -35,17 +35,17 @@ namespace Tgstation.Server.Api /// /// The JWT authentication header scheme /// - public const string BearerAuthenticationScheme = "bearer"; + public const string BearerAuthenticationScheme = "Bearer"; /// /// The JWT authentication header scheme /// - public const string BasicAuthenticationScheme = "basic"; + public const string BasicAuthenticationScheme = "Basic"; /// /// The JWT authentication header scheme /// - public const string OAuthAuthenticationScheme = "oauth"; + public const string OAuthAuthenticationScheme = "OAuth"; /// /// The current @@ -206,9 +206,7 @@ namespace Tgstation.Server.Api InstanceId = instanceId; } -#pragma warning disable CA1308 // Normalize strings to uppercase - switch (scheme.ToLowerInvariant()) -#pragma warning restore CA1308 // Normalize strings to uppercase + switch (scheme) { case OAuthAuthenticationScheme: if (requestHeaders.Headers.TryGetValue(OAuthProviderHeader, out StringValues oauthProviderValues)) diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs index fd404f3ef1..44c59c737c 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProvider.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -9,5 +9,10 @@ namespace Tgstation.Server.Api.Models /// https://github.com /// GitHub, + + /// + /// https://discord.com + /// + Discord, } } diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs index 26d9cbc0fd..a2d8958845 100644 --- a/src/Tgstation.Server.Api/Models/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; namespace Tgstation.Server.Api.Models { @@ -21,5 +22,10 @@ namespace Tgstation.Server.Api.Models /// The DMAPI version of the host. /// public Version? DMApiVersion { get; set; } + + /// + /// Map of to the server's associated client IDs for them. + /// + public IDictionary? OAuthProviderClientIds { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs deleted file mode 100644 index a40ebf431d..0000000000 --- a/src/Tgstation.Server.Host/Configuration/GitHubOAuthConfiguration.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Tgstation.Server.Host.Configuration -{ - /// - /// OAuth options for GitHub - /// - sealed class GitHubOAuthConfiguration - { - /// - /// The GitHub client ID. - /// - public string ClientId { get; set; } - - /// - /// The GitHub client secret. - /// - public string ClientSecret { get; set; } - } -} diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs new file mode 100644 index 0000000000..b0dc12fc21 --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -0,0 +1,37 @@ +using System; + +namespace Tgstation.Server.Host.Configuration +{ + /// + /// OAuth options. + /// + class OAuthConfiguration + { + /// + /// The client ID. + /// + public string ClientId { get; set; } + + /// + /// The client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Initializes a new instance of the . + /// + public OAuthConfiguration() { } + + /// + /// Initializes a new instance of the . + /// + /// The to copy settings from. + public OAuthConfiguration(OAuthConfiguration oAuthConfiguration) + { + if (oAuthConfiguration == null) + throw new ArgumentNullException(nameof(oAuthConfiguration)); + ClientId = oAuthConfiguration.ClientId; + ClientSecret = oAuthConfiguration.ClientSecret; + } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs index c6a74f2e4a..e2aa598b1d 100644 --- a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs @@ -60,6 +60,11 @@ namespace Tgstation.Server.Host.Configuration /// /// OAuth options for GitHub. /// - public GitHubOAuthConfiguration GitHubOAuth { get; set; } + public OAuthConfiguration GitHubOAuth { get; set; } + + /// + /// OAuth options for Discord. + /// + public OAuthConfiguration DiscordOAuth { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 4243fe9ad7..e725b0bcb5 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -129,23 +129,10 @@ namespace Tgstation.Server.Host.Controllers /// /// retrieved successfully. [HttpGet] - [TgsAuthorize] [AllowAnonymous] [ProducesResponseType(typeof(ServerInformation), 200)] public IActionResult Home() { - if (AuthenticationContext != null) - return Json(new ServerInformation - { - Version = assemblyInformationProvider.Version, - ApiVersion = ApiHeaders.Version, - DMApiVersion = DMApiConstants.Version, - MinimumPasswordLength = generalConfiguration.MinimumPasswordLength, - InstanceLimit = generalConfiguration.InstanceLimit, - UserLimit = generalConfiguration.UserLimit, - ValidInstancePaths = generalConfiguration.ValidInstancePaths - }); - // if we are using a browser and the control panel, soft redirect to the app page if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic) { @@ -153,7 +140,17 @@ namespace Tgstation.Server.Host.Controllers return Redirect(Core.Application.ControlPanelRoute); } - return ApiHeaders == null ? HeadersIssue() : Unauthorized(); + return Json(new ServerInformation + { + Version = assemblyInformationProvider.Version, + ApiVersion = ApiHeaders.Version, + DMApiVersion = DMApiConstants.Version, + MinimumPasswordLength = generalConfiguration.MinimumPasswordLength, + InstanceLimit = generalConfiguration.InstanceLimit, + UserLimit = generalConfiguration.UserLimit, + ValidInstancePaths = generalConfiguration.ValidInstancePaths, + OAuthProviderClientIds = oAuthProviders.ClientIds() + }); } /// diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index c1294255b9..452495474a 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -127,7 +127,10 @@ namespace Tgstation.Server.Host.Core config => { if (microsoftEventLevel.HasValue) + { config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value); + config.MinimumLevel.Override("System.Net.Http.HttpClient", microsoftEventLevel.Value); + } }, sinkConfig => { @@ -221,6 +224,9 @@ namespace Tgstation.Server.Host.Core // CORS conditionally enabled later services.AddCors(); + // Enable managed HTTP clients + services.AddHttpClient(); + void AddTypedContext() where TContext : DatabaseContext { var configureAction = DatabaseContext.GetConfigureAction(); diff --git a/src/Tgstation.Server.Host/Core/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Core/GitHubClientFactory.cs index e2bc292b18..218a19bf4f 100644 --- a/src/Tgstation.Server.Host/Core/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Core/GitHubClientFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using Octokit; using Tgstation.Server.Host.System; @@ -25,7 +25,10 @@ namespace Tgstation.Server.Host.Core /// Create a /// /// A new - GitHubClient CreateBaseClient() => new GitHubClient(new ProductHeaderValue(assemblyInformationProvider.VersionPrefix, assemblyInformationProvider.Version.ToString())); + GitHubClient CreateBaseClient() => new GitHubClient( + new ProductHeaderValue( + assemblyInformationProvider.ProductInfoHeaderValue.Product.Name, + assemblyInformationProvider.ProductInfoHeaderValue.Product.Version)); /// public IGitHubClient CreateClient() => CreateBaseClient(); diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs new file mode 100644 index 0000000000..4ecaea1227 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Globalization; +using System.Net.Http; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// OAuth validator for Discord. + /// + sealed class DiscordOAuthValidator : GenericOAuthValidator + { + /// + /// Initializes a new instance of the . + /// + /// The for the . + /// The for the . + /// The for the . + /// The for the . + public DiscordOAuthValidator( + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + OAuthConfiguration oAuthConfiguration) + : base(httpClientFactory, assemblyInformationProvider, logger, oAuthConfiguration) + { + } + + /// + public override OAuthProvider Provider => OAuthProvider.Discord; + + /// + protected override Uri TokenUrl => new Uri("https://discord.com/api/oauth2/token"); + + /// + protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me"); + + /// + protected override OAuthTokenRequest CreateTokenRequest(string code) => new DiscordTokenRequest(OAuthConfiguration, code); + + /// + protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; + + /// + protected override ulong? DecodeUserInformationPayload(dynamic responseJson) => UInt64.Parse( + (string)responseJson.id, + CultureInfo.InvariantCulture); + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs new file mode 100644 index 0000000000..fdd6dcc393 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs @@ -0,0 +1,33 @@ +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// for Discord. + /// + /// See https://discord.com/developers/docs/topics/oauth2 + sealed class DiscordTokenRequest : OAuthTokenRequest + { + /// + /// The 'grant_type' field. + /// + public string GrantType { get; } + + /// + /// The 'scope' field. + /// + public string Scope { get; } + + /// + /// Initializes a new instance of the . + /// + /// The for the . + /// The OAuth code for the . + public DiscordTokenRequest(OAuthConfiguration oAuthConfiguration, string code) + : base(oAuthConfiguration, code) + { + GrantType = "authorization_code"; + Scope = "identify"; + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs new file mode 100644 index 0000000000..c1fe0b1205 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// for generic OAuth2 endpoints. + /// + abstract class GenericOAuthValidator : IOAuthValidator + { + /// + public abstract OAuthProvider Provider { get; } + + /// + public string ClientId => OAuthConfiguration.ClientId; + + /// + /// The for the . + /// + protected ILogger Logger { get; } + + /// + /// The for the . + /// + protected OAuthConfiguration OAuthConfiguration { get; } + + /// + /// to to to get the access token. + /// + protected abstract Uri TokenUrl { get; } + + /// + /// to the user information payload from. + /// + protected abstract Uri UserInformationUrl { get; } + + /// + /// The for the . + /// + readonly IHttpClientFactory httpClientFactory; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public GenericOAuthValidator( + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + OAuthConfiguration oAuthConfiguration) + { + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + OAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration)); + } + + /// + /// Decode the token payload . + /// + /// The token payload . + /// The OAuth2 bearer access token on success, otherwise. + protected abstract string DecodeTokenPayload(dynamic responseJson); + + /// + /// Decode the user information payload . + /// + /// The user information payload . + /// The user ID on success, otherwise. + protected abstract ulong? DecodeUserInformationPayload(dynamic responseJson); + + /// + /// Create the for a given + /// + /// The OAuth code from the browser. + /// The to send to . + protected abstract OAuthTokenRequest CreateTokenRequest(string code); + + /// + public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) + { + using var httpClient = httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + httpClient.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue); + try + { + Logger.LogTrace("Validating response code..."); + using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, TokenUrl); + + var tokenRequestPayload = CreateTokenRequest(code); + + // roundabout but it works + var tokenRequestJson = JsonConvert.SerializeObject( + tokenRequestPayload, + new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }); + + var tokenRequestDictionary = JsonConvert.DeserializeObject>(tokenRequestJson); + tokenRequest.Content = new FormUrlEncodedContent(tokenRequestDictionary); + + var tokenResponse = await httpClient.SendAsync(tokenRequest, cancellationToken).ConfigureAwait(false); + tokenResponse.EnsureSuccessStatusCode(); + var tokenResponsePayload = await tokenResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + var tokenResponseJson = JObject.Parse(tokenResponsePayload); + + var accessToken = DecodeTokenPayload(tokenResponseJson); + if (accessToken == null) + return null; + + Logger.LogTrace("Getting user details..."); + using var userInformationRequest = new HttpRequestMessage(HttpMethod.Get, UserInformationUrl); + userInformationRequest.Headers.Authorization = new AuthenticationHeaderValue( + ApiHeaders.BearerAuthenticationScheme, + accessToken); + + var userInformationResponse = await httpClient.SendAsync(userInformationRequest, cancellationToken).ConfigureAwait(false); + userInformationResponse.EnsureSuccessStatusCode(); + + var userInformationPayload = await userInformationResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + var userInformationJson = JObject.Parse(userInformationPayload); + + return DecodeUserInformationPayload(userInformationJson); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error while completing OAuth handshake!"); + return null; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index d1d1284854..a2308c1155 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -17,6 +17,9 @@ namespace Tgstation.Server.Host.Security.OAuth /// public OAuthProvider Provider => OAuthProvider.GitHub; + /// + public string ClientId => oAuthConfiguration.ClientId; + /// /// The for the . /// @@ -28,24 +31,24 @@ namespace Tgstation.Server.Host.Security.OAuth readonly ILogger logger; /// - /// The for the . + /// The for the . /// - readonly GitHubOAuthConfiguration gitHubOAuthConfiguration; + readonly OAuthConfiguration oAuthConfiguration; /// /// Initializes a new instance of the . /// /// The value of . /// The value of . - /// The value of . + /// The value of . public GitHubOAuthValidator( IGitHubClientFactory gitHubClientFactory, ILogger logger, - GitHubOAuthConfiguration gitHubOAuthConfiguration) + OAuthConfiguration oAuthConfiguration) { this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.gitHubOAuthConfiguration = gitHubOAuthConfiguration ?? throw new ArgumentNullException(nameof(gitHubOAuthConfiguration)); + this.oAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration)); } /// @@ -62,8 +65,8 @@ namespace Tgstation.Server.Host.Security.OAuth .Oauth .CreateAccessToken( new OauthTokenRequest( - gitHubOAuthConfiguration.ClientId, - gitHubOAuthConfiguration.ClientSecret, + oAuthConfiguration.ClientId, + oAuthConfiguration.ClientSecret, code)) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index 2468b36aa3..c51e41dd12 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Security.OAuth @@ -13,5 +14,11 @@ namespace Tgstation.Server.Host.Security.OAuth /// The to get the validator for. /// The for . IOAuthValidator GetValidator(OAuthProvider oAuthProvider); + + /// + /// Gets a of the provider client IDs. + /// + /// A new of the provider client IDs. + Dictionary ClientIds(); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index 5457af53d3..8c29f97612 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Security.OAuth /// OAuthProvider Provider { get; } + /// + /// The OAuth client ID of validator. + /// + string ClientId { get; } + /// /// Validate a given OAuth response . /// diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index cf0185e03f..f345301aa8 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -3,9 +3,11 @@ using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Security.OAuth { @@ -21,10 +23,14 @@ namespace Tgstation.Server.Host.Security.OAuth /// Initializes a new instance of the . /// /// The to use. + /// The to use. + /// The to use. /// The to use. /// The containing the to use. public OAuthProviders( IGitHubClientFactory gitHubClientFactory, + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, ILoggerFactory loggerFactory, IOptions securityConfigurationOptions) { @@ -42,10 +48,24 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), securityConfiguration.GitHubOAuth)); + if (securityConfiguration.DiscordOAuth != null) + validatorsBuilder.Add( + new DiscordOAuthValidator( + httpClientFactory, + assemblyInformationProvider, + loggerFactory.CreateLogger(), + securityConfiguration.DiscordOAuth)); + validators = validatorsBuilder; } /// public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.First(x => x.Provider == oAuthProvider); + + /// + public Dictionary ClientIds() => validators + .ToDictionary( + x => x.Provider, + x => x.ClientId); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs new file mode 100644 index 0000000000..7f03023625 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -0,0 +1,27 @@ +using System; +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Generic OAuth token request. + /// + class OAuthTokenRequest : OAuthConfiguration + { + /// + /// The OAuth code. + /// + public string Code { get; } + + /// + /// Initializes a new instance of the + /// + /// The to build from. + /// The OAuth code received from the browser. + public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code) + : base(oAuthConfiguration) + { + Code = code ?? throw new ArgumentNullException(nameof(code)); + } + } +} diff --git a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs index 94c1c00664..b4933d7b6c 100644 --- a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs +++ b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http.Headers; using System.Reflection; using Tgstation.Server.Api; @@ -22,6 +23,11 @@ namespace Tgstation.Server.Host.System /// public string VersionString { get; } + /// + public ProductInfoHeaderValue ProductInfoHeaderValue => new ProductInfoHeaderValue( + VersionPrefix, + Version.ToString()); + /// /// Initializes a new instance of the . /// diff --git a/src/Tgstation.Server.Host/System/IAssemblyInformationProvider.cs b/src/Tgstation.Server.Host/System/IAssemblyInformationProvider.cs index 30c97a618d..162b627888 100644 --- a/src/Tgstation.Server.Host/System/IAssemblyInformationProvider.cs +++ b/src/Tgstation.Server.Host/System/IAssemblyInformationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http.Headers; using System.Reflection; namespace Tgstation.Server.Host.System @@ -32,5 +33,10 @@ namespace Tgstation.Server.Host.System /// The version of the assembly. /// Version Version { get; } + + /// + /// The for the assembly. + /// + ProductInfoHeaderValue ProductInfoHeaderValue { get; } } } diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 6febf17312..8409290cd1 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -55,6 +55,7 @@ "TokenClockSkewMinutes": 1, "TokenSigningKeyByteAmount": 256, "CustomTokenSigningKeyBase64": null, - "GitHubOAuth": null + "GitHubOAuth": null, + "DiscordOAuth": null } } diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs index 192bc07957..3c00447fc4 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs @@ -1,7 +1,8 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Octokit; using System; +using System.Net.Http.Headers; using System.Threading.Tasks; using Tgstation.Server.Host.System; @@ -17,8 +18,7 @@ namespace Tgstation.Server.Host.Core.Tests public async Task TestCreateBasicClient() { var mockApp = new Mock(); - mockApp.SetupGet(x => x.Version).Returns(new Version()).Verifiable(); - mockApp.SetupGet(x => x.VersionPrefix).Returns("TGSTests").Verifiable(); + mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); var factory = new GitHubClientFactory(mockApp.Object); From e36548f342b67aea99c4fe6876cb6a0ed2494aa6 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 23 Nov 2020 16:02:13 -0500 Subject: [PATCH 005/154] Cleanups and the like --- README.md | 15 +++-- docs/API.dox | 63 +++++++++++++------ src/Tgstation.Server.Api/ApiHeaders.cs | 12 ++-- src/Tgstation.Server.Api/HeaderTypes.cs | 2 +- .../Controllers/HomeController.cs | 4 +- src/Tgstation.Server.Host/Core/Application.cs | 7 +-- src/Tgstation.Server.Host/Security/README.md | 4 +- 7 files changed, 71 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index d77e02c804..a81fc8ad58 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,14 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `ControlPanel:AllowedOrigins`: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by `ControlPanel:AllowAnyOrigin` +- `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, and `Discord`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: +```json +"GitHubOAuth":{ + "ClientId": "...", + "ClientSecret": "..." +} +``` + ### Database Configuration If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk. @@ -372,10 +380,9 @@ TGS 4 can self update without stopping your DreamDaemon servers. Any V4 release Here are tools for interacting with the TGS 4 web API - [tgstation-server-control-panel]: Official client and included with the server (WIP). A react web app for using tgstation-server. -- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server -- [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server -- [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server -- [Postman](https://www.getpostman.com/): This repository contains [TGS.postman_collection.json](tools/TGS.postman_collection.json) which is used during development for testing. Contains example requests for all endpoints but takes some knowledge to use (Note that the pre-request script is configured to login the default admin user for every request) +- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server. Feature complete. +- [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server. Feature complete. +- [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server. Feature complete. Contact project maintainers to get your client added to this list diff --git a/docs/API.dox b/docs/API.dox index e4320e103b..a2a9f85d5d 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -77,6 +77,24 @@ TGS will only every return the response codes listed here - 501: Not implemented. Functionality not available in the current server version - 503: Service unavailable. The server is either starting up or shutting down and isn't ready to respond to requests. You can try again soon and a response/lack thereof will indicate which of the two events it was +@section api_ver Server Information + +The versions of the TGS host can be retireved with this request. + +Note: This endpoint does not require authentication + +GET "/" => @ref Tgstation.Server.Api.Models.ServerInformation + +The Version model fields are based on the C# one and looks like this: + +@code{.json} +{ + "version": "..." +} +@endcode + +Other fields may be present in the Version model but should be ignored. See a description of these version numbers here. + @section api_auth Authentication Every request made to TGS requires authentication. It is provided in the form of the Authorization header. @@ -87,7 +105,7 @@ POST "/" => @ref Tgstation.Server.Api.Models.Token Headers: -- Authorization:basic `` +- Authorization:Basic `` If the provided credentials are valid and your user account is enabled you will recieve a @ref Tgstation.Server.Api.Models.Token object @code{.json} @@ -100,10 +118,35 @@ If your account is disabled, you will recieve a 403 response. You may recognize the bearer value as a Json Web Token. This is a secure representation of your identity to the server. It expires after a set period of time or until your password changes. It must be present for requests made to all other APIs. To do so add the following header to your other requests -- Authorization:bearer `` +- Authorization:Bearer `` Continue to use this token until you begin to recieve 401 responses from the API. Then repeat the process to get a new one if your credentials are still valid +@subsection api_auth_o OAuth 2.0 + +TGS4 supports OAuth 2.0 with select providers for authentication. + +The flow for this is as follows: + +- Retrieve the @ref api_ver to find out available OAuth providers and their respective client IDs. +- Send the user to the Authorization Request endpoint for the provider using the client ID from above. See https://tools.ietf.org/html/rfc6749#section-4.1.1. DO NOT specify a redirect URI, this should be configured in the provider. +- Retrieve the authorization response code after successfully completing the authorize step above. +- Perform the following request: + +POST "/" => @ref Tgstation.Server.Api.Models.Token + +Headers: + +- Authorization:OAuth `OAuth Authorization Response Code` +- OAuthProvider: + +You will be granted a bearer token as in basic auth. This will have an extended expiration to avoid repeating the entire process. + +@subsubsection api_auth_o_providers Supported Providers + +- ID: 0, Name: GitHub, Documentation: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ +- ID: 1, Name: Discord, Documentation: https://discord.com/developers/docs/topics/oauth2 + @section api_perms Permissions Almost all actions available require some level of user permissions. Which are divided into two categories: @@ -150,22 +193,6 @@ I DELETE "/InstanceUser/{UserId}" => OK Users with the permission to modify @ref Tgstation.Server.Api.Models.Instance objects can also gain user editing rights for any Instance. See @ref api_instance -@section api_ver Version - -The versions of the TGS host can be retireved with this request - -GET "/" => @ref Tgstation.Server.Api.Models.ServerInformation - -The Version model fields are based on the C# one and looks like this: - -@code{.json} -{ - "version": "..." -} -@endcode - -Other fields may be present in the Version model but should be ignored. See a description of these version numbers here. - @section api_admin Server-wide Administrative Actions You can retrieve server update information with this diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 34aa8a0476..5ada556992 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -20,32 +20,32 @@ namespace Tgstation.Server.Api /// /// The header key. /// - public const string ApiVersionHeader = "Api"; + public static readonly string ApiVersionHeader = "Api"; /// /// The header key. /// - public const string InstanceIdHeader = "Instance"; + public static readonly string InstanceIdHeader = "Instance"; /// /// The header key. /// - public const string OAuthProviderHeader = "OAuthProvider"; + public static readonly string OAuthProviderHeader = "OAuthProvider"; /// /// The JWT authentication header scheme /// - public const string BearerAuthenticationScheme = "Bearer"; + public static readonly string BearerAuthenticationScheme = "Bearer"; /// /// The JWT authentication header scheme /// - public const string BasicAuthenticationScheme = "Basic"; + public static readonly string BasicAuthenticationScheme = "Basic"; /// /// The JWT authentication header scheme /// - public const string OAuthAuthenticationScheme = "OAuth"; + public static readonly string OAuthAuthenticationScheme = "OAuth"; /// /// The current diff --git a/src/Tgstation.Server.Api/HeaderTypes.cs b/src/Tgstation.Server.Api/HeaderTypes.cs index f9a10739da..b45b6b0380 100644 --- a/src/Tgstation.Server.Api/HeaderTypes.cs +++ b/src/Tgstation.Server.Api/HeaderTypes.cs @@ -36,6 +36,6 @@ namespace Tgstation.Server.Api /// /// . /// - OAuthProvider, + OAuthProvider = 16, } } diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index e725b0bcb5..c80fd3fdeb 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -161,9 +161,11 @@ namespace Tgstation.Server.Host.Controllers /// User logged in and generated successfully. /// User authentication failed. /// User authenticated but is disabled by an administrator. + /// OAuth authentication failed due to rate limiting. [HttpPost] [ProducesResponseType(typeof(Token), 200)] - #pragma warning disable CA1506 // TODO: Decomplexify + [ProducesResponseType(typeof(ErrorMessage), 429)] +#pragma warning disable CA1506 // TODO: Decomplexify public async Task CreateToken(CancellationToken cancellationToken) { if (ApiHeaders == null) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 452495474a..9f62941bd7 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -162,11 +162,8 @@ namespace Tgstation.Server.Host.Core }); // configure bearer token validation - var authenticationBuilder = services - .AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - }) + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(jwtBearerOptions => { // this line isn't actually run until the first request is made diff --git a/src/Tgstation.Server.Host/Security/README.md b/src/Tgstation.Server.Host/Security/README.md index aea0dbf6cf..80f5afaccf 100644 --- a/src/Tgstation.Server.Host/Security/README.md +++ b/src/Tgstation.Server.Host/Security/README.md @@ -7,4 +7,6 @@ - [IIdentityCache](./IIdentityCache.cs) and [implementation](./IdentityCache.cs) is used to store `ISystemIdentity`s for the duration of their associated tokens as [IdentityCacheObject](./IdentityCacheObject.cs)s. - [ITokenFactory](./ITokenFactory.cs) and [implementation](./TokenFactory.cs) is used to generate the Json Web Token for a session after a user successfully authenticates. - [ISystemIdentity](./ISystemIdentity.cs)s represent a logon session with the operating system for a given user. It contains a method to run code under the security context of said user. -- [ISystemIdentityFactory](./ISystemIdentityFactory.cs) is used to create `ISystemIdentity`s by attempting to log the user in with the OS with a given username and password. \ No newline at end of file +- [ISystemIdentityFactory](./ISystemIdentityFactory.cs) is used to create `ISystemIdentity`s by attempting to log the user in with the OS with a given username and password. + +- [OAuth](./OAuth) contains classes related to OAuth 2.0 authentication From baec815743558d55f2f13d2cd4246592ed2a8069 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 23 Nov 2020 16:11:12 -0500 Subject: [PATCH 006/154] Build fix --- src/Tgstation.Server.Api/ApiHeaders.cs | 12 ++++++------ .../Core/TestGitHubClientFactory.cs | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 5ada556992..34aa8a0476 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -20,32 +20,32 @@ namespace Tgstation.Server.Api /// /// The header key. /// - public static readonly string ApiVersionHeader = "Api"; + public const string ApiVersionHeader = "Api"; /// /// The header key. /// - public static readonly string InstanceIdHeader = "Instance"; + public const string InstanceIdHeader = "Instance"; /// /// The header key. /// - public static readonly string OAuthProviderHeader = "OAuthProvider"; + public const string OAuthProviderHeader = "OAuthProvider"; /// /// The JWT authentication header scheme /// - public static readonly string BearerAuthenticationScheme = "Bearer"; + public const string BearerAuthenticationScheme = "Bearer"; /// /// The JWT authentication header scheme /// - public static readonly string BasicAuthenticationScheme = "Basic"; + public const string BasicAuthenticationScheme = "Basic"; /// /// The JWT authentication header scheme /// - public static readonly string OAuthAuthenticationScheme = "OAuth"; + public const string OAuthAuthenticationScheme = "OAuth"; /// /// The current diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs index 3c00447fc4..7a8c565f6b 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs @@ -36,8 +36,7 @@ namespace Tgstation.Server.Host.Core.Tests public async Task TestCreateTokenClient() { var mockApp = new Mock(); - mockApp.SetupGet(x => x.Version).Returns(new Version()).Verifiable(); - mockApp.SetupGet(x => x.VersionPrefix).Returns("TGSTests").Verifiable(); + mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); var factory = new GitHubClientFactory(mockApp.Object); From 3ba79307a852e093b3cfabd2637e770c5e9ed92e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 24 Nov 2020 11:17:55 -0500 Subject: [PATCH 007/154] Simplify oauth development flow --- .github/CONTRIBUTING.md | 12 + .../Models/OAuthConnection.cs | 4 +- .../Configuration/SecurityConfiguration.cs | 10 +- .../Controllers/HomeController.cs | 6 +- ...eOAuthExternalIdColumnToString.Designer.cs | 822 ++++++++++++++++++ ...4_MSChangeOAuthExternalIdColumnToString.cs | 41 + ...eOAuthExternalIdColumnToString.Designer.cs | 811 +++++++++++++++++ ...2_MYChangeOAuthExternalIdColumnToString.cs | 41 + ...eOAuthExternalIdColumnToString.Designer.cs | 819 +++++++++++++++++ ...5_PGChangeOAuthExternalIdColumnToString.cs | 41 + ...eOAuthExternalIdColumnToString.Designer.cs | 810 +++++++++++++++++ ...1_SLChangeOAuthExternalIdColumnToString.cs | 41 + .../MySqlDatabaseContextModelSnapshot.cs | 6 +- ...PostgresSqlDatabaseContextModelSnapshot.cs | 6 +- .../SqlServerDatabaseContextModelSnapshot.cs | 6 +- .../SqliteDatabaseContextModelSnapshot.cs | 6 +- .../Security/OAuth/DiscordOAuthValidator.cs | 5 +- .../Security/OAuth/GenericOAuthValidator.cs | 4 +- .../Security/OAuth/GitHubOAuthValidator.cs | 5 +- .../Security/OAuth/IOAuthValidator.cs | 2 +- .../Security/OAuth/OAuthProviders.cs | 8 +- src/Tgstation.Server.Host/appsettings.json | 6 +- 22 files changed, 3478 insertions(+), 34 deletions(-) create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7c05ad9d61..f92ef35518 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -189,6 +189,18 @@ We have a script to do this. 1. Run `dotnet ef migrations add SL --context SqliteDatabaseContext`. 1. Follow the above steps. +## Adding OAuth Providers + +OAuth providers are hardcoded but it is fairly easy to add new ones. Follow the following steps: + +1. Add the name to the [Tgstation.Server.Api.Models.OAuthProviders](../src/Tgstation.Server.Api/Models/OAuthProviders.cs) enum (Also necessitates a minor HTTP API version bump). +1. Create an implementation of [IOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs). + - Most providers can simply override the [GenericOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs). +1. Construct the implementation in the [OAuthProviders] class. +1. Add a null entry to the default [appsettings.json](../src/Tgstation.Server.Host/appsettings.json). + +TGS should now be able to accept authentication response tokens from your provider. + ### Important Note About the \[Required\] Attribute. We use this attribute to ensure EFCore generated tables are not nullable for specific properties. They are valid to be null in API communication. Do not use this attribute expecting the model validator to prevent null data in API request. diff --git a/src/Tgstation.Server.Api/Models/OAuthConnection.cs b/src/Tgstation.Server.Api/Models/OAuthConnection.cs index 973d2a1e6e..d684a04e2e 100644 --- a/src/Tgstation.Server.Api/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Api/Models/OAuthConnection.cs @@ -16,6 +16,8 @@ namespace Tgstation.Server.Api.Models /// /// The ID of the user in the . /// - public ulong ExternalUserId { get; set; } + [Required] + [StringLength(Limits.MaximumIndexableStringLength)] + public string? ExternalUserId { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs index e2aa598b1d..1957a3b56d 100644 --- a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Configuration @@ -58,13 +59,8 @@ namespace Tgstation.Server.Host.Configuration public string CustomTokenSigningKeyBase64 { get; set; } /// - /// OAuth options for GitHub. + /// OAuth provider settings. /// - public OAuthConfiguration GitHubOAuth { get; set; } - - /// - /// OAuth options for Discord. - /// - public OAuthConfiguration DiscordOAuth { get; set; } + public IDictionary OAuth { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index c80fd3fdeb..85630d5565 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -197,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers IQueryable query = DatabaseContext.Users.AsQueryable(); if (oAuthLogin) { - ulong? externalUserId; + string externalUserId; try { externalUserId = await oAuthProviders @@ -210,13 +210,13 @@ namespace Tgstation.Server.Host.Controllers return RateLimit(ex); } - if (!externalUserId.HasValue) + if (externalUserId == null) return Unauthorized(); query = query.Where( x => x.OAuthConnections.Any( y => y.Provider == ApiHeaders.OAuthProvider.Value - && y.ExternalUserId == externalUserId.Value)); + && y.ExternalUserId == externalUserId)); } else { diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs new file mode 100644 index 0000000000..b129e448c0 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs @@ -0,0 +1,822 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20201124155144_MSChangeOAuthExternalIdColumnToString")] + partial class MSChangeOAuthExternalIdColumnToString + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("decimal(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs new file mode 100644 index 0000000000..dff0936983 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Changes the OAuthConnections ExternalUserId column to a string for MSSQL. + /// + public partial class MSChangeOAuthExternalIdColumnToString : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + maxLength: 100, + nullable: false, + oldClrType: typeof(decimal), + oldType: "decimal(20,0)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + type: "decimal(20,0)", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs new file mode 100644 index 0000000000..ffbf808040 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs @@ -0,0 +1,811 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20201124155232_MYChangeOAuthExternalIdColumnToString")] + partial class MYChangeOAuthExternalIdColumnToString + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("bigint unsigned"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SystemIdentifier") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs new file mode 100644 index 0000000000..7af7221c98 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Changes the OAuthConnections ExternalUserId column to a string for MYSQL. + /// + public partial class MYChangeOAuthExternalIdColumnToString : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + maxLength: 100, + nullable: false, + oldClrType: typeof(ulong), + oldType: "bigint unsigned"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + type: "bigint unsigned", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs new file mode 100644 index 0000000000..89f3c0de84 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs @@ -0,0 +1,819 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20201124155325_PGChangeOAuthExternalIdColumnToString")] + partial class PGChangeOAuthExternalIdColumnToString + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("numeric(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs new file mode 100644 index 0000000000..9be6a9fab5 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Changes the OAuthConnections ExternalUserId column to a string for PostgreSQL. + /// + public partial class PGChangeOAuthExternalIdColumnToString : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + maxLength: 100, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs new file mode 100644 index 0000000000..d5108aeeda --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs @@ -0,0 +1,810 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20201124155421_SLChangeOAuthExternalIdColumnToString")] + partial class SLChangeOAuthExternalIdColumnToString + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondRights") + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstanceUserRights") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs new file mode 100644 index 0000000000..f4a80062a7 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Changes the OAuthConnections ExternalUserId column to a string for SQLite. + /// + public partial class SLChangeOAuthExternalIdColumnToString : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + maxLength: 100, + nullable: false, + oldClrType: typeof(ulong), + oldType: "INTEGER"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "ExternalUserId", + table: "OAuthConnections", + type: "INTEGER", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index dc88d0e657..6859a6252b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -379,8 +379,10 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - b.Property("ExternalUserId") - .HasColumnType("bigint unsigned"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("int"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index 606f3e08b5..5cfd13f687 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -381,8 +381,10 @@ namespace Tgstation.Server.Host.Database.Migrations .HasColumnType("bigint") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ExternalUserId") - .HasColumnType("numeric(20,0)"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("integer"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index c9499288a9..8fba8e0ec6 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -383,8 +383,10 @@ namespace Tgstation.Server.Host.Database.Migrations .HasColumnType("bigint") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ExternalUserId") - .HasColumnType("decimal(20,0)"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("int"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index ded21a4fb2..c7ffe85f0f 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -378,8 +378,10 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("ExternalUserId") - .HasColumnType("INTEGER"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("INTEGER"); diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs index 4ecaea1227..f0fe8591f5 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Globalization; using System.Net.Http; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; @@ -45,8 +44,6 @@ namespace Tgstation.Server.Host.Security.OAuth protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; /// - protected override ulong? DecodeUserInformationPayload(dynamic responseJson) => UInt64.Parse( - (string)responseJson.id, - CultureInfo.InvariantCulture); + protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.id; } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index c1fe0b1205..b85c2d19dd 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -88,7 +88,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// The user information payload . /// The user ID on success, otherwise. - protected abstract ulong? DecodeUserInformationPayload(dynamic responseJson); + protected abstract string DecodeUserInformationPayload(dynamic responseJson); /// /// Create the for a given @@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Security.OAuth protected abstract OAuthTokenRequest CreateTokenRequest(string code); /// - public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) + public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) { using var httpClient = httpClientFactory.CreateClient(); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index a2308c1155..33eada0557 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Octokit; using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -52,7 +53,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) + public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) { if (code == null) throw new ArgumentNullException(nameof(code)); @@ -82,7 +83,7 @@ namespace Tgstation.Server.Host.Security.OAuth .Current() .ConfigureAwait(false); - return (ulong)userDetails.Id; + return userDetails.Id.ToString(CultureInfo.InvariantCulture); } catch (RateLimitExceededException) { diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index 8c29f97612..46163e1d83 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -25,6 +25,6 @@ namespace Tgstation.Server.Host.Security.OAuth /// The OAuth response string from web application. /// The for the operation. /// A resulting in if authentication failed, if a rate limit occurred, and the validated otherwise. - Task ValidateResponseCode(string code, CancellationToken cancellationToken); + Task ValidateResponseCode(string code, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index f345301aa8..7ee484a162 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -41,20 +41,20 @@ namespace Tgstation.Server.Host.Security.OAuth var validatorsBuilder = new List(); - if (securityConfiguration.GitHubOAuth != null) + if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.GitHub, out var gitHubConfig)) validatorsBuilder.Add( new GitHubOAuthValidator( gitHubClientFactory, loggerFactory.CreateLogger(), - securityConfiguration.GitHubOAuth)); + gitHubConfig)); - if (securityConfiguration.DiscordOAuth != null) + if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Discord, out var discordConfig)) validatorsBuilder.Add( new DiscordOAuthValidator( httpClientFactory, assemblyInformationProvider, loggerFactory.CreateLogger(), - securityConfiguration.DiscordOAuth)); + discordConfig)); validators = validatorsBuilder; } diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 8409290cd1..770aac931c 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -55,7 +55,9 @@ "TokenClockSkewMinutes": 1, "TokenSigningKeyByteAmount": 256, "CustomTokenSigningKeyBase64": null, - "GitHubOAuth": null, - "DiscordOAuth": null + "OAuth": { + "GitHub": null, + "Discord": null + } } } From 25b966816874c2444737b78bbbcb94717d076a97 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 24 Nov 2020 13:14:10 -0500 Subject: [PATCH 008/154] Add /tg/ forums OAuth --- .github/CONTRIBUTING.md | 4 +- README.md | 4 +- docs/API.dox | 1 + src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 +- .../Models/OAuthProvider.cs | 5 + .../Controllers/AdministrationController.cs | 7 +- .../Controllers/HomeController.cs | 7 +- src/Tgstation.Server.Host/Core/Application.cs | 2 +- .../Security/OAuth/BaseOAuthValidator.cs | 101 +++++++++++++ .../Security/OAuth/GenericOAuthValidator.cs | 65 ++------ .../Security/OAuth/GitHubOAuthValidator.cs | 6 +- .../Security/OAuth/IOAuthProviders.cs | 7 +- .../Security/OAuth/IOAuthValidator.cs | 6 +- .../Security/OAuth/OAuthProviders.cs | 26 +++- .../Security/OAuth/TGBaseResponse.cs | 23 +++ .../Security/OAuth/TGCreateSessionResponse.cs | 18 +++ .../Security/OAuth/TGForumsOAuthValidator.cs | 140 ++++++++++++++++++ .../OAuth/TGGetSessionInfoResponse.cs | 13 ++ src/Tgstation.Server.Host/appsettings.json | 3 +- 19 files changed, 372 insertions(+), 72 deletions(-) create mode 100644 src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs create mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f92ef35518..fe55a32094 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -191,13 +191,15 @@ We have a script to do this. ## Adding OAuth Providers -OAuth providers are hardcoded but it is fairly easy to add new ones. Follow the following steps: +OAuth providers are hardcoded but it is fairly easy to add new ones. The flow doesn't need to be strict OAuth either (r.e. /tg/ forums). Follow the following steps: 1. Add the name to the [Tgstation.Server.Api.Models.OAuthProviders](../src/Tgstation.Server.Api/Models/OAuthProviders.cs) enum (Also necessitates a minor HTTP API version bump). 1. Create an implementation of [IOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs). - Most providers can simply override the [GenericOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs). 1. Construct the implementation in the [OAuthProviders] class. 1. Add a null entry to the default [appsettings.json](../src/Tgstation.Server.Host/appsettings.json). +1. Update the main [README.md](../README.md) to indicate the new provider. +1. Update the [API documentation](../docs/API.dox) to indicate the new provider. TGS should now be able to accept authentication response tokens from your provider. diff --git a/README.md b/README.md index a81fc8ad58..1d6228e9cf 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `ControlPanel:AllowedOrigins`: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by `ControlPanel:AllowAnyOrigin` -- `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, and `Discord`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: +- `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: ```json "GitHubOAuth":{ - "ClientId": "...", + "ClientId": "...", // Note for "TGForums", this is the redirect_uri used "ClientSecret": "..." } ``` diff --git a/docs/API.dox b/docs/API.dox index a2a9f85d5d..6beed23239 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -146,6 +146,7 @@ You will be granted a bearer token as in basic auth. This will have an extended - ID: 0, Name: GitHub, Documentation: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ - ID: 1, Name: Discord, Documentation: https://discord.com/developers/docs/topics/oauth2 +- ID: 2, Name: TGForums, Documentation: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 @section api_perms Permissions diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 48517ea1cd..62977c6b2c 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -64,10 +64,10 @@ namespace Tgstation.Server.Api.Models CannotChangeServerSuite, /// - /// A required GitHub API request failed. + /// A required remote API request failed. /// - [Description("A required GitHub request returned an API error!")] - GitHubApiError, + [Description("A required remote API request returned an error!")] + RemoteApiError, /// /// A server update was requested while another was in progress. diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs index 44c59c737c..8d5a9eb354 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProvider.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -14,5 +14,10 @@ namespace Tgstation.Server.Api.Models /// https://discord.com /// Discord, + + /// + /// https://tgstation13.org + /// + TGForums, } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 6095e2a3ba..a41fb7895c 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -140,7 +140,10 @@ namespace Tgstation.Server.Host.Controllers catch (ApiException e) { Logger.LogWarning(e, OctokitException); - return StatusCode(HttpStatusCode.FailedDependency); + return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.RemoteApiError) + { + AdditionalData = e.Message + }); } releases = releases.Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)); @@ -234,7 +237,7 @@ namespace Tgstation.Server.Host.Controllers catch (ApiException e) { Logger.LogWarning(e, OctokitException); - return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.GitHubApiError) + return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.RemoteApiError) { AdditionalData = e.Message }); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 85630d5565..539b3dd9c0 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -124,14 +124,15 @@ namespace Tgstation.Server.Host.Controllers /// /// Main page of the /// + /// The for the operation. /// - /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise. + /// A resuting in the containing of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise. /// /// retrieved successfully. [HttpGet] [AllowAnonymous] [ProducesResponseType(typeof(ServerInformation), 200)] - public IActionResult Home() + public async Task Home(CancellationToken cancellationToken) { // if we are using a browser and the control panel, soft redirect to the app page if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic) @@ -149,7 +150,7 @@ namespace Tgstation.Server.Host.Controllers InstanceLimit = generalConfiguration.InstanceLimit, UserLimit = generalConfiguration.UserLimit, ValidInstancePaths = generalConfiguration.ValidInstancePaths, - OAuthProviderClientIds = oAuthProviders.ClientIds() + OAuthProviderClientIds = await oAuthProviders.ClientIds(cancellationToken).ConfigureAwait(false) }); } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 9069c1f94b..80d71e27c0 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -268,7 +268,7 @@ namespace Tgstation.Server.Host.Core // configure security services services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs new file mode 100644 index 0000000000..68cc17cf23 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Base for s. + /// + abstract class BaseOAuthValidator : IOAuthValidator + { + /// + public abstract OAuthProvider Provider { get; } + + /// + /// The for the . + /// + protected ILogger Logger { get; } + + /// + /// The for the . + /// + protected OAuthConfiguration OAuthConfiguration { get; } + + /// + /// The for the . + /// + readonly IHttpClientFactory httpClientFactory; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// Gets that should be used. + /// + /// A new . + protected static JsonSerializerSettings SerializerSettings() => new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public BaseOAuthValidator( + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + OAuthConfiguration oAuthConfiguration) + { + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + OAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration)); + } + + /// + public abstract Task GetClientId(CancellationToken cancellationToken); + + /// + public abstract Task ValidateResponseCode(string code, CancellationToken cancellationToken); + + /// + /// Create a new configured . + /// + /// A new configured . + protected HttpClient CreateHttpClient() + { + var httpClient = httpClientFactory.CreateClient(); + try + { + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + httpClient.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue); + return httpClient; + } + catch + { + httpClient.Dispose(); + throw; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index b85c2d19dd..8deff22537 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -1,16 +1,13 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; -using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; -using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; @@ -19,24 +16,8 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// for generic OAuth2 endpoints. /// - abstract class GenericOAuthValidator : IOAuthValidator + abstract class GenericOAuthValidator : BaseOAuthValidator { - /// - public abstract OAuthProvider Provider { get; } - - /// - public string ClientId => OAuthConfiguration.ClientId; - - /// - /// The for the . - /// - protected ILogger Logger { get; } - - /// - /// The for the . - /// - protected OAuthConfiguration OAuthConfiguration { get; } - /// /// to to to get the access token. /// @@ -47,33 +28,24 @@ namespace Tgstation.Server.Host.Security.OAuth /// protected abstract Uri UserInformationUrl { get; } - /// - /// The for the . - /// - readonly IHttpClientFactory httpClientFactory; - - /// - /// The for the . - /// - readonly IAssemblyInformationProvider assemblyInformationProvider; - /// /// Initializes a new instance of the . /// - /// The value of . - /// The value of . - /// The value of . - /// The value of . + /// The for the + /// The for the + /// The for the + /// The for the . public GenericOAuthValidator( IHttpClientFactory httpClientFactory, IAssemblyInformationProvider assemblyInformationProvider, ILogger logger, OAuthConfiguration oAuthConfiguration) + : base( + httpClientFactory, + assemblyInformationProvider, + logger, + oAuthConfiguration) { - this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - OAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration)); } /// @@ -98,11 +70,9 @@ namespace Tgstation.Server.Host.Security.OAuth protected abstract OAuthTokenRequest CreateTokenRequest(string code); /// - public async Task ValidateResponseCode(string code, CancellationToken cancellationToken) + public override async Task ValidateResponseCode(string code, CancellationToken cancellationToken) { - using var httpClient = httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - httpClient.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue); + using var httpClient = CreateHttpClient(); try { Logger.LogTrace("Validating response code..."); @@ -113,13 +83,7 @@ namespace Tgstation.Server.Host.Security.OAuth // roundabout but it works var tokenRequestJson = JsonConvert.SerializeObject( tokenRequestPayload, - new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - } - }); + SerializerSettings()); var tokenRequestDictionary = JsonConvert.DeserializeObject>(tokenRequestJson); tokenRequest.Content = new FormUrlEncodedContent(tokenRequestDictionary); @@ -153,5 +117,8 @@ namespace Tgstation.Server.Host.Security.OAuth return null; } } + + /// + public override Task GetClientId(CancellationToken cancellationToken) => Task.FromResult(OAuthConfiguration.ClientId); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index 33eada0557..b4ee1354e6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -18,9 +18,6 @@ namespace Tgstation.Server.Host.Security.OAuth /// public OAuthProvider Provider => OAuthProvider.GitHub; - /// - public string ClientId => oAuthConfiguration.ClientId; - /// /// The for the . /// @@ -95,5 +92,8 @@ namespace Tgstation.Server.Host.Security.OAuth return null; } } + + /// + public Task GetClientId(CancellationToken cancellationToken) => Task.FromResult(oAuthConfiguration.ClientId); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index c51e41dd12..66af75b5af 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Security.OAuth @@ -18,7 +20,8 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Gets a of the provider client IDs. /// - /// A new of the provider client IDs. - Dictionary ClientIds(); + /// The for the operation. + /// A resulting in a anew of the active provider client IDs. + Task> ClientIds(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index 46163e1d83..be33dcb992 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -15,9 +15,11 @@ namespace Tgstation.Server.Host.Security.OAuth OAuthProvider Provider { get; } /// - /// The OAuth client ID of validator. + /// Gets the OAuth client ID of validator. /// - string ClientId { get; } + /// The for the operation. + /// A resulting in the client ID of the validator on success, on failure. + Task GetClientId(CancellationToken cancellationToken); /// /// Validate a given OAuth response . diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 7ee484a162..86101957c5 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; @@ -56,6 +58,14 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), discordConfig)); + if(securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig)) + validatorsBuilder.Add( + new TGForumsOAuthValidator( + httpClientFactory, + assemblyInformationProvider, + loggerFactory.CreateLogger(), + tgConfig)); + validators = validatorsBuilder; } @@ -63,9 +73,19 @@ namespace Tgstation.Server.Host.Security.OAuth public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.First(x => x.Provider == oAuthProvider); /// - public Dictionary ClientIds() => validators - .ToDictionary( + public async Task> ClientIds(CancellationToken cancellationToken) + { + var providersAndTasks = validators.ToDictionary( x => x.Provider, - x => x.ClientId); + x => x.GetClientId(cancellationToken)); + + await Task.WhenAll(providersAndTasks.Values).ConfigureAwait(false); + + return providersAndTasks + .Where(x => x.Value.Result != null) + .ToDictionary( + x => x.Key, + x => x.Value.Result); + } } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs b/src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs new file mode 100644 index 0000000000..1f887179e3 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs @@ -0,0 +1,23 @@ +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Base for tgstation forum responses. + /// + abstract class TGBaseResponse + { + /// + /// Expected value of . + /// + public const string OkStatus = "OK"; + + /// + /// The response status. + /// + public string Status { get; set; } + + /// + /// The response error, if any. + /// + public string Error { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs b/src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs new file mode 100644 index 0000000000..7d8d53face --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Response when creating a tgstation forums session. + /// + sealed class TGCreateSessionResponse : TGBaseResponse + { + /// + /// The session's private token. Similar to OAuth authorization response code. + /// + public string SessionPrivateToken { get; set; } + + /// + /// The session's public token. Barely similar to OAuth client ID. + /// + public string SessionPublicToken { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs new file mode 100644 index 0000000000..afdd635974 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -0,0 +1,140 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// for /tg/ forums. + /// + sealed class TGForumsOAuthValidator : BaseOAuthValidator + { + /// + /// Amount of minutes until unused sessions that were created are forgotten. + /// + const uint SessionRetentionMinutes = 10; + + /// + public override OAuthProvider Provider => OAuthProvider.TGForums; + + /// + /// The active session. + /// + readonly List> sessions; + + /// + /// Initializes a new instance of the . + /// + /// The for the + /// The for the + /// The for the + /// The for the . + public TGForumsOAuthValidator( + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + OAuthConfiguration oAuthConfiguration) + : base( + httpClientFactory, + assemblyInformationProvider, + logger, + oAuthConfiguration) + { + sessions = new List>(); + } + + /// + public override async Task GetClientId(CancellationToken cancellationToken) + { + var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.Now); + if (expiredSessions > 0) + Logger.LogTrace("Expired {0} sessions", expiredSessions); + + Logger.LogTrace("Creating new session..."); + try + { + UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_create_session.php") + { + Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.ClientId)}" + }; + + using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); + using var httpClient = CreateHttpClient(); + + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var newSession = JsonConvert.DeserializeObject(json, SerializerSettings()); + + if (newSession.Status != TGBaseResponse.OkStatus) + { + Logger.LogWarning("Invalid status from /tg/ API! Status: {0}, Error: {1}", newSession.Status, newSession.Error); + return null; + } + + sessions.Add(Tuple.Create(newSession, DateTimeOffset.Now)); + return newSession.SessionPublicToken; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to create TG Forums session!"); + return null; + } + } + + /// + public override async Task ValidateResponseCode(string code, CancellationToken cancellationToken) + { + try + { + var sessionTuple = sessions.FirstOrDefault(x => x.Item1.SessionPublicToken == code); + if(sessionTuple == null) + { + Logger.LogWarning("No known session with this code active!"); + return null; + } + + Logger.LogTrace("Validating session..."); + + UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_get_session_info.php") + { + Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&session_private_token={HttpUtility.UrlEncode(sessionTuple.Item1.SessionPrivateToken)}" + }; + + using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); + using var httpClient = CreateHttpClient(); + + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var sessionInfo = JsonConvert.DeserializeObject(json, SerializerSettings()); + + if (sessionInfo.Status != TGBaseResponse.OkStatus) + { + Logger.LogWarning("Invalid status from /tg/ API! Status: {0}, Error: {1}", sessionInfo.Status, sessionInfo.Error); + return null; + } + + sessions.Remove(sessionTuple); + return sessionInfo.PhpbbUsername; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to create TG Forums session!"); + return null; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs b/src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs new file mode 100644 index 0000000000..89b834e769 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// Response when getting tgstation forum user's info. + /// + sealed class TGGetSessionInfoResponse : TGBaseResponse + { + /// + /// The user's forum account name. + /// + public string PhpbbUsername { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 770aac931c..00506b5762 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -57,7 +57,8 @@ "CustomTokenSigningKeyBase64": null, "OAuth": { "GitHub": null, - "Discord": null + "Discord": null, + "TGForums": null } } } From 59362b7a4930185ca5217def4c24c20dc980852e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 24 Nov 2020 13:36:31 -0500 Subject: [PATCH 009/154] Minor cleanups --- .github/CONTRIBUTING.md | 2 +- README.md | 2 +- .../Controllers/ApiController.cs | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fe55a32094..96eb6e3108 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -196,7 +196,7 @@ OAuth providers are hardcoded but it is fairly easy to add new ones. The flow do 1. Add the name to the [Tgstation.Server.Api.Models.OAuthProviders](../src/Tgstation.Server.Api/Models/OAuthProviders.cs) enum (Also necessitates a minor HTTP API version bump). 1. Create an implementation of [IOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs). - Most providers can simply override the [GenericOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs). -1. Construct the implementation in the [OAuthProviders] class. +1. Construct the implementation in the [OAuthProviders](../src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs) class. 1. Add a null entry to the default [appsettings.json](../src/Tgstation.Server.Host/appsettings.json). 1. Update the main [README.md](../README.md) to indicate the new provider. 1. Update the [API documentation](../docs/API.dox) to indicate the new provider. diff --git a/README.md b/README.md index 1d6228e9cf..3333810cd9 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: ```json "GitHubOAuth":{ - "ClientId": "...", // Note for "TGForums", this is the redirect_uri used + "ClientId": "... (Note for `TGForums`, this is the redirect_uri used)", "ClientSecret": "..." } ``` diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index c42eee8d39..b27ccf9e6d 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -258,9 +258,19 @@ namespace Tgstation.Server.Host.Controllers { if (ApiHeaders != null) Logger.LogDebug( - "Starting API Request: Version: {0}. User-Agent: {1}", + "Starting API request: Version: {0}. {1}: {2}", ApiHeaders.ApiVersion.Semver(), + HeaderNames.UserAgent, ApiHeaders.RawUserAgent); + else if (Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgents)) + Logger.LogDebug( + "Starting unauthorized API request. {0}: {1}", + HeaderNames.UserAgent, + userAgents); + else + Logger.LogDebug( + "Starting unauthorized API request. No {0}!", + HeaderNames.UserAgent); await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); } } From a513c22c23907137551e4e0ae849977f98974951 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 24 Nov 2020 16:06:15 -0500 Subject: [PATCH 010/154] Fix integration tests --- README.md | 2 +- src/Tgstation.Server.Api/ApiHeaders.cs | 14 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 + .../Controllers/ApiController.cs | 7 +- .../Controllers/HomeController.cs | 28 +- .../Controllers/UserController.cs | 2 +- ...22231546_SLAddOAuthConnections.Designer.cs | 6 +- .../20201122231546_SLAddOAuthConnections.cs | 2 +- ...eOAuthExternalIdColumnToString.Designer.cs | 810 ------------------ ...1_SLChangeOAuthExternalIdColumnToString.cs | 41 - .../Security/OAuth/OAuthProviders.cs | 2 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 29 +- tests/Tgstation.Server.Tests/RootTest.cs | 77 +- tests/Tgstation.Server.Tests/TestingServer.cs | 14 +- tests/Tgstation.Server.Tests/UsersTest.cs | 4 +- 15 files changed, 158 insertions(+), 886 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs diff --git a/README.md b/README.md index 3333810cd9..39da29f3a4 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ TGS 4 can self update without stopping your DreamDaemon servers. Any V4 release Here are tools for interacting with the TGS 4 web API - [tgstation-server-control-panel]: Official client and included with the server (WIP). A react web app for using tgstation-server. -- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server. Feature complete. +- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server. Feature complete but lacks OAuth login options. - [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server. Feature complete. - [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server. Feature complete. diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 34aa8a0476..9ec9c575b1 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -145,8 +145,10 @@ namespace Tgstation.Server.Api /// Construct and validates from a set of /// /// The containing the + /// If a missing should be ignored. /// Thrown if the constitue invalid . - public ApiHeaders(RequestHeaders requestHeaders) +#pragma warning disable CA1502 + public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth = false) { if (requestHeaders == null) throw new ArgumentNullException(nameof(requestHeaders)); @@ -183,7 +185,10 @@ namespace Tgstation.Server.Api AddError(HeaderTypes.Api, $"Malformed {ApiVersionHeader} header!"); if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization)) - AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!"); + { + if (!ignoreMissingAuth) + AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!"); + } else { var auth = authorization.First(); @@ -233,12 +238,12 @@ namespace Tgstation.Server.Api } catch { - throw new InvalidOperationException("Invalid basic Authorization header!"); + throw new InvalidOperationException($"Invalid basic {HeaderNames.Authorization} header!"); } var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); if (basicAuthSplits.Length < 2) - throw new InvalidOperationException("Invalid basic Authorization header!"); + throw new InvalidOperationException($"Invalid basic {HeaderNames.Authorization} header!"); Username = basicAuthSplits.First(); Password = String.Concat(basicAuthSplits.Skip(1)); @@ -256,6 +261,7 @@ namespace Tgstation.Server.Api ApiVersion = apiVersion!.Semver(); } +#pragma warning restore CA1502 /// /// Construct diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 62977c6b2c..4c08b12b30 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -588,5 +588,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The admin user cannot use OAuth connections!")] AdminUserCannotOAuth, + + /// + /// Attempted to login with a disabled OAuth provider. + /// + [Description("The requested OAuth provider is disabled via configuration!")] + OAuthProviderDisabled, } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index b27ccf9e6d..0a772be319 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -145,13 +145,14 @@ namespace Tgstation.Server.Host.Controllers /// /// Response for missing/Invalid headers. /// + /// Whether or not errors due to missing should be thrown. /// The appropriate . - protected IActionResult HeadersIssue() + protected IActionResult HeadersIssue(bool ignoreMissingAuth) { HeadersException headersException; try { - var _ = new ApiHeaders(Request.GetTypedHeaders()); + var _ = new ApiHeaders(Request.GetTypedHeaders(), ignoreMissingAuth); throw new InvalidOperationException("Expected a header parse exception!"); } catch (HeadersException ex) @@ -215,7 +216,7 @@ namespace Tgstation.Server.Host.Controllers { if (requireHeaders) { - await HeadersIssue() + await HeadersIssue(false) .ExecuteResultAsync(context) .ConfigureAwait(false); return; diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 539b3dd9c0..9d09033105 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -8,6 +9,7 @@ using Microsoft.Net.Http.Headers; using Octokit; using System; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -141,6 +143,21 @@ namespace Tgstation.Server.Host.Controllers return Redirect(Core.Application.ControlPanelRoute); } + // we only allow authorization header issues + if (ApiHeaders == null) + try + { + var headers = new ApiHeaders(Request.GetTypedHeaders(), true); + if (!headers.Compatible()) + return StatusCode( + HttpStatusCode.UpgradeRequired, + new ErrorMessage(ErrorCode.ApiMismatch)); + } + catch (HeadersException) + { + return HeadersIssue(true); + } + return Json(new ServerInformation { Version = assemblyInformationProvider.Version, @@ -172,7 +189,7 @@ namespace Tgstation.Server.Host.Controllers if (ApiHeaders == null) { Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\"")); - return HeadersIssue(); + return HeadersIssue(false); } if (ApiHeaders.IsTokenAuthentication) @@ -201,8 +218,13 @@ namespace Tgstation.Server.Host.Controllers string externalUserId; try { - externalUserId = await oAuthProviders - .GetValidator(ApiHeaders.OAuthProvider.Value) + var validator = oAuthProviders + .GetValidator(ApiHeaders.OAuthProvider.Value); + + if (validator == null) + return BadRequest(new ErrorMessage(ErrorCode.OAuthProviderDisabled)); + + externalUserId = await validator .ValidateResponseCode(ApiHeaders.Token, cancellationToken) .ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index d02249c4a1..19c25ad761 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -197,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword); var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnOAuthConnections); - var originalUser = passwordEdit + var originalUser = !canEditAllUsers ? AuthenticationContext.User : await DatabaseContext .Users diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs index 73e8d1eb6c..3a45f3b0a3 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.Designer.cs @@ -380,8 +380,10 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("ExternalUserId") - .HasColumnType("INTEGER"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("INTEGER"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs index 43c62a778f..9c8cdfb567 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231546_SLAddOAuthConnections.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Database.Migrations Id = table.Column(nullable: false) .Annotation("Sqlite:Autoincrement", true), Provider = table.Column(nullable: false), - ExternalUserId = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false, maxLength: 100), UserId = table.Column(nullable: true) }, constraints: table => diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs deleted file mode 100644 index d5108aeeda..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.Designer.cs +++ /dev/null @@ -1,810 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Tgstation.Server.Host.Database.Migrations -{ - [DbContext(typeof(SqliteDatabaseContext))] - [Migration("20201124155421_SLChangeOAuthExternalIdColumnToString")] - partial class SLChangeOAuthExternalIdColumnToString - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.10"); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChannelLimit") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("Enabled") - .HasColumnType("INTEGER"); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ReconnectionInterval") - .IsRequired() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "Name") - .IsUnique(); - - b.ToTable("ChatBots"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChatSettingsId") - .HasColumnType("INTEGER"); - - b.Property("DiscordChannelId") - .HasColumnType("INTEGER"); - - b.Property("IrcChannel") - .HasColumnType("TEXT") - .HasMaxLength(100); - - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("Tag") - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.HasKey("Id"); - - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique(); - - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique(); - - b.ToTable("ChatChannels"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DMApiMajorVersion") - .HasColumnType("INTEGER"); - - b.Property("DMApiMinorVersion") - .HasColumnType("INTEGER"); - - b.Property("DMApiPatchVersion") - .HasColumnType("INTEGER"); - - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DmeName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("GitHubDeploymentId") - .HasColumnType("INTEGER"); - - b.Property("GitHubRepoId") - .HasColumnType("INTEGER"); - - b.Property("JobId") - .HasColumnType("INTEGER"); - - b.Property("MinimumSecurityLevel") - .HasColumnType("INTEGER"); - - b.Property("Output") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RevisionInformationId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DirectoryName"); - - b.HasIndex("JobId") - .IsUnique(); - - b.HasIndex("RevisionInformationId"); - - b.ToTable("CompileJobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("AutoStart") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("HeartbeatSeconds") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("Port") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("SecurityLevel") - .HasColumnType("INTEGER"); - - b.Property("StartupTimeout") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("TopicRequestTimeout") - .IsRequired() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamDaemonSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiValidationPort") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("ApiValidationSecurityLevel") - .HasColumnType("INTEGER"); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("ProjectName") - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamMakerSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AutoUpdateInterval") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("ChatBotLimit") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("ConfigurationType") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("Online") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("Path") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Path") - .IsUnique(); - - b.ToTable("Instances"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ByondRights") - .HasColumnType("INTEGER"); - - b.Property("ChatBotRights") - .HasColumnType("INTEGER"); - - b.Property("ConfigurationRights") - .HasColumnType("INTEGER"); - - b.Property("DreamDaemonRights") - .HasColumnType("INTEGER"); - - b.Property("DreamMakerRights") - .HasColumnType("INTEGER"); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("InstanceUserRights") - .HasColumnType("INTEGER"); - - b.Property("RepositoryRights") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId"); - - b.HasIndex("UserId", "InstanceId") - .IsUnique(); - - b.ToTable("InstanceUsers"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CancelRight") - .HasColumnType("INTEGER"); - - b.Property("CancelRightsType") - .HasColumnType("INTEGER"); - - b.Property("Cancelled") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("CancelledById") - .HasColumnType("INTEGER"); - - b.Property("Description") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ErrorCode") - .HasColumnType("INTEGER"); - - b.Property("ExceptionDetails") - .HasColumnType("TEXT"); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("StartedAt") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("StartedById") - .HasColumnType("INTEGER"); - - b.Property("StoppedAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("CancelledById"); - - b.HasIndex("InstanceId"); - - b.HasIndex("StartedById"); - - b.ToTable("Jobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); - - b.ToTable("OAuthConnections"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("CompileJobId") - .HasColumnType("INTEGER"); - - b.Property("LaunchSecurityLevel") - .HasColumnType("INTEGER"); - - b.Property("Port") - .HasColumnType("INTEGER"); - - b.Property("ProcessId") - .HasColumnType("INTEGER"); - - b.Property("RebootState") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("CompileJobId"); - - b.ToTable("ReattachInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("AccessUser") - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("CommitterName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("RepositorySettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RevisionInformationId") - .HasColumnType("INTEGER"); - - b.Property("TestMergeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RevisionInformationId"); - - b.HasIndex("TestMergeId"); - - b.ToTable("RevInfoTestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CommitSha") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(40); - - b.Property("InstanceId") - .HasColumnType("INTEGER"); - - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(40); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); - - b.ToTable("RevisionInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Comment") - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("MergedAt") - .HasColumnType("TEXT"); - - b.Property("MergedById") - .HasColumnType("INTEGER"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(40); - - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MergedById"); - - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); - - b.ToTable("TestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AdministrationRights") - .HasColumnType("INTEGER"); - - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("CreatedById") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .IsRequired() - .HasColumnType("INTEGER"); - - b.Property("InstanceManagerRights") - .HasColumnType("INTEGER"); - - b.Property("LastPasswordUpdate") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("SystemIdentifier") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("CanonicalName") - .IsUnique(); - - b.HasIndex("CreatedById"); - - b.HasIndex("SystemIdentifier") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); - - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs deleted file mode 100644 index f4a80062a7..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155421_SLChangeOAuthExternalIdColumnToString.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using System; - -namespace Tgstation.Server.Host.Database.Migrations -{ - /// - /// Changes the OAuthConnections ExternalUserId column to a string for SQLite. - /// - public partial class SLChangeOAuthExternalIdColumnToString : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - maxLength: 100, - nullable: false, - oldClrType: typeof(ulong), - oldType: "INTEGER"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - type: "INTEGER", - nullable: false, - oldClrType: typeof(string), - oldMaxLength: 100); - } - } -} diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 86101957c5..91be97f0e2 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -70,7 +70,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.First(x => x.Provider == oAuthProvider); + public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); /// public async Task> ClientIds(CancellationToken cancellationToken) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 5fcb6e5a42..3953594b8d 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using Newtonsoft.Json; using System; using System.Diagnostics; using System.IO; @@ -12,6 +13,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Mime; using System.Net.Sockets; using System.Reflection; using System.Threading; @@ -37,9 +39,9 @@ namespace Tgstation.Server.Tests readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString())); [TestMethod] - public async Task TestUpdateProtocol() + public async Task TestUpdateProtocolAndDisabledOAuth() { - using var server = new TestingServer(); + using var server = new TestingServer(false); using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; var serverTask = server.Run(cancellationToken); @@ -47,11 +49,30 @@ namespace Tgstation.Server.Tests { var testUpdateVersion = new Version(4, 3, 0); using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + { + // Disabled OAuth test + using (var httpClient = new HttpClient()) + using (var request = new HttpRequestMessage(HttpMethod.Post, server.Url.ToString())) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, adminClient.Token.Bearer); + request.Headers.Add(ApiHeaders.OAuthProviderHeader, OAuthProvider.GitHub.ToString()); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(ErrorCode.OAuthProviderDisabled, message.ErrorCode); + } + //attempt to update to stable await adminClient.Administration.Update(new Administration { NewVersion = testUpdateVersion }, cancellationToken).ConfigureAwait(false); + } //wait up to 3 minutes for the dl and install await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(3), cancellationToken)).ConfigureAwait(false); @@ -207,7 +228,7 @@ namespace Tgstation.Server.Tests Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!"); } - using var server = new TestingServer(); + using var server = new TestingServer(true); const int MaximumTestMinutes = 20; using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaximumTestMinutes)); @@ -450,7 +471,7 @@ namespace Tgstation.Server.Tests [TestMethod] public async Task TestRepoParentLookup() { - using var testingServer = new TestingServer(); + using var testingServer = new TestingServer(false); LibGit2Sharp.Repository.Clone("https://github.com/Cyberboss/test", testingServer.Directory); var libGit2Repo = new LibGit2Sharp.Repository(testingServer.Directory); using var repo = new Host.Components.Repository.Repository( diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs index b94e2c3dc7..be2b87cb95 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RootTest.cs @@ -1,5 +1,6 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; +using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -56,12 +57,12 @@ namespace Tgstation.Server.Tests request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0"); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); using var response = await httpClient.SendAsync(request, cancellationToken); - Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); - var message = JsonConvert.DeserializeObject(content); - Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); + var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(ApiHeaders.Version, message.ApiVersion); } using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) @@ -70,7 +71,21 @@ namespace Tgstation.Server.Tests request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0"); - request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(ErrorCode.ApiMismatch, message.ErrorCode); + } + + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.Administration.Substring(1))) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0"); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); @@ -83,8 +98,8 @@ namespace Tgstation.Server.Tests request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0"); - request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); request.Content = new StringContent( "{ newVersion: 1234 }", Encoding.UTF8, @@ -101,7 +116,7 @@ namespace Tgstation.Server.Tests request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0"); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); using var response = await httpClient.SendAsync(request, cancellationToken); Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); } @@ -111,14 +126,29 @@ namespace Tgstation.Server.Tests request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0"); - request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); var message = JsonConvert.DeserializeObject(content); Assert.AreEqual(ErrorCode.InstanceHeaderRequired, message.ErrorCode); } + + using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString())) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, token); + request.Headers.Add(ApiHeaders.OAuthProviderHeader, "FakeProvider"); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); + } } async Task TestServerInformation(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken) @@ -140,12 +170,35 @@ namespace Tgstation.Server.Tests }; var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken); - await Assert.ThrowsExceptionAsync(() => badClient.Version(cancellationToken)).ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(() => badClient.Administration.Read(cancellationToken)).ConfigureAwait(false); + } + + async Task TestOAuthFails(IServerClient serverClient, CancellationToken cancellationToken) + { + var url = serverClient.Url; + var token = serverClient.Token.Bearer; + // check that 400s are returned appropriately + using var httpClient = new HttpClient(); + + // just hitting each type of oauth provider for coverage + foreach (var I in Enum.GetValues(typeof(OAuthProvider))) + using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString())) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, token); + request.Headers.Add(ApiHeaders.OAuthProviderHeader, I.ToString()); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } } public Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken) => Task.WhenAll( TestRequestValidation(serverClient, cancellationToken), + TestOAuthFails(serverClient, cancellationToken), TestServerInformation(clientFactory, serverClient, cancellationToken)); } } diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 8962ce2d58..7eee0642a1 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; @@ -31,7 +32,7 @@ namespace Tgstation.Server.Tests IServer realServer; - public TestingServer() + public TestingServer(bool enableOAuth) { Directory = Environment.GetEnvironmentVariable("TGS4_TEST_TEMP_DIRECTORY"); if (String.IsNullOrWhiteSpace(Directory)) @@ -86,6 +87,17 @@ namespace Tgstation.Server.Tests "General:ByondTopicTimeout=3000" }; + // enable all oauth providers + if (enableOAuth) + foreach (var I in Enum.GetValues(typeof(OAuthProvider))) + { + args.Add($"Security:OAuth:{I}:ClientId=Fake"); + args.Add($"Security:OAuth:{I}:ClientSecret=Faker"); + } + + // SPECIFICALLY DELETE THE DEV APPSETTINGS, WE DON'T WANT IT IN THE WAY + File.Delete("appsettings.Development.json"); + if (!String.IsNullOrEmpty(gitHubAccessToken)) args.Add(String.Format(CultureInfo.InvariantCulture, "General:GitHubAccessToken={0}", gitHubAccessToken)); diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 7241361cf9..fade7832a0 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; @@ -29,7 +29,7 @@ namespace Tgstation.Server.Tests async Task BasicTests(CancellationToken cancellationToken) { - var user = await this.client.Read(cancellationToken).ConfigureAwait(false); + var user = await client.Read(cancellationToken).ConfigureAwait(false); Assert.IsNotNull(user); Assert.AreEqual("Admin", user.Name); Assert.IsNull(user.SystemIdentifier); From 749455ad516810f5ebc97996d61de07f22d79fa4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 24 Nov 2020 16:44:40 -0500 Subject: [PATCH 011/154] Undo those shit migrations --- ...22231219_MSAddOAuthConnections.Designer.cs | 6 +- .../20201122231219_MSAddOAuthConnections.cs | 2 +- ...22231327_MYAddOAuthConnections.Designer.cs | 6 +- .../20201122231327_MYAddOAuthConnections.cs | 2 +- ...22231443_PGAddOAuthConnections.Designer.cs | 6 +- .../20201122231443_PGAddOAuthConnections.cs | 2 +- ...eOAuthExternalIdColumnToString.Designer.cs | 822 ------------------ ...4_MSChangeOAuthExternalIdColumnToString.cs | 41 - ...eOAuthExternalIdColumnToString.Designer.cs | 811 ----------------- ...2_MYChangeOAuthExternalIdColumnToString.cs | 41 - ...eOAuthExternalIdColumnToString.Designer.cs | 819 ----------------- ...5_PGChangeOAuthExternalIdColumnToString.cs | 41 - 12 files changed, 15 insertions(+), 2584 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs delete mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs index 271369ebde..2a705bfa38 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.Designer.cs @@ -385,8 +385,10 @@ namespace Tgstation.Server.Host.Database.Migrations .HasColumnType("bigint") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ExternalUserId") - .HasColumnType("decimal(20,0)"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("int"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs index e99c868e17..f965ae37de 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231219_MSAddOAuthConnections.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Database.Migrations Id = table.Column(nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Provider = table.Column(nullable: false), - ExternalUserId = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false, maxLength: 100), UserId = table.Column(nullable: true) }, constraints: table => diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs index 05e0d25108..9b2bac237d 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.Designer.cs @@ -381,8 +381,10 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - b.Property("ExternalUserId") - .HasColumnType("bigint unsigned"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("int"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs index 422a4f98b8..d2741b785b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231327_MYAddOAuthConnections.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Database.Migrations Id = table.Column(nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), Provider = table.Column(nullable: false), - ExternalUserId = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false, maxLength: 100), UserId = table.Column(nullable: true) }, constraints: table => diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs index 9bbc1ecd6b..48cbdcf3a7 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.Designer.cs @@ -383,8 +383,10 @@ namespace Tgstation.Server.Host.Database.Migrations .HasColumnType("bigint") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ExternalUserId") - .HasColumnType("numeric(20,0)"); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); b.Property("Provider") .HasColumnType("integer"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs index 41c30fd23f..b660fe83e0 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201122231443_PGAddOAuthConnections.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Database.Migrations Id = table.Column(nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Provider = table.Column(nullable: false), - ExternalUserId = table.Column(nullable: false), + ExternalUserId = table.Column(nullable: false, maxLength: 100), UserId = table.Column(nullable: true) }, constraints: table => diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs deleted file mode 100644 index b129e448c0..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.Designer.cs +++ /dev/null @@ -1,822 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Tgstation.Server.Host.Database.Migrations -{ - [DbContext(typeof(SqlServerDatabaseContext))] - [Migration("20201124155144_MSChangeOAuthExternalIdColumnToString")] - partial class MSChangeOAuthExternalIdColumnToString - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.10") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ChannelLimit") - .HasColumnType("int"); - - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("Enabled") - .HasColumnType("bit"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("int"); - - b.Property("ReconnectionInterval") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "Name") - .IsUnique(); - - b.ToTable("ChatBots"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ChatSettingsId") - .HasColumnType("bigint"); - - b.Property("DiscordChannelId") - .HasColumnType("decimal(20,0)"); - - b.Property("IrcChannel") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); - - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("bit"); - - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("bit"); - - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("bit"); - - b.Property("Tag") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.HasKey("Id"); - - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique() - .HasFilter("[DiscordChannelId] IS NOT NULL"); - - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique() - .HasFilter("[IrcChannel] IS NOT NULL"); - - b.ToTable("ChatChannels"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("DMApiMajorVersion") - .HasColumnType("int"); - - b.Property("DMApiMinorVersion") - .HasColumnType("int"); - - b.Property("DMApiPatchVersion") - .HasColumnType("int"); - - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("uniqueidentifier"); - - b.Property("DmeName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("GitHubDeploymentId") - .HasColumnType("int"); - - b.Property("GitHubRepoId") - .HasColumnType("bigint"); - - b.Property("JobId") - .HasColumnType("bigint"); - - b.Property("MinimumSecurityLevel") - .HasColumnType("int"); - - b.Property("Output") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("RevisionInformationId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("DirectoryName"); - - b.HasIndex("JobId") - .IsUnique(); - - b.HasIndex("RevisionInformationId"); - - b.ToTable("CompileJobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("bit"); - - b.Property("AutoStart") - .IsRequired() - .HasColumnType("bit"); - - b.Property("HeartbeatSeconds") - .HasColumnType("bigint"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("Port") - .HasColumnType("int"); - - b.Property("SecurityLevel") - .HasColumnType("int"); - - b.Property("StartupTimeout") - .HasColumnType("bigint"); - - b.Property("TopicRequestTimeout") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamDaemonSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ApiValidationPort") - .HasColumnType("int"); - - b.Property("ApiValidationSecurityLevel") - .HasColumnType("int"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("ProjectName") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("bit"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamMakerSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("AutoUpdateInterval") - .HasColumnType("bigint"); - - b.Property("ChatBotLimit") - .HasColumnType("int"); - - b.Property("ConfigurationType") - .HasColumnType("int"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("Online") - .IsRequired() - .HasColumnType("bit"); - - b.Property("Path") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.HasKey("Id"); - - b.HasIndex("Path") - .IsUnique(); - - b.ToTable("Instances"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ByondRights") - .HasColumnType("decimal(20,0)"); - - b.Property("ChatBotRights") - .HasColumnType("decimal(20,0)"); - - b.Property("ConfigurationRights") - .HasColumnType("decimal(20,0)"); - - b.Property("DreamDaemonRights") - .HasColumnType("decimal(20,0)"); - - b.Property("DreamMakerRights") - .HasColumnType("decimal(20,0)"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("InstanceUserRights") - .HasColumnType("decimal(20,0)"); - - b.Property("RepositoryRights") - .HasColumnType("decimal(20,0)"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId"); - - b.HasIndex("UserId", "InstanceId") - .IsUnique(); - - b.ToTable("InstanceUsers"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CancelRight") - .HasColumnType("decimal(20,0)"); - - b.Property("CancelRightsType") - .HasColumnType("decimal(20,0)"); - - b.Property("Cancelled") - .IsRequired() - .HasColumnType("bit"); - - b.Property("CancelledById") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("ErrorCode") - .HasColumnType("bigint"); - - b.Property("ExceptionDetails") - .HasColumnType("nvarchar(max)"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("StartedAt") - .IsRequired() - .HasColumnType("datetimeoffset"); - - b.Property("StartedById") - .HasColumnType("bigint"); - - b.Property("StoppedAt") - .HasColumnType("datetimeoffset"); - - b.HasKey("Id"); - - b.HasIndex("CancelledById"); - - b.HasIndex("InstanceId"); - - b.HasIndex("StartedById"); - - b.ToTable("Jobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("int"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); - - b.ToTable("OAuthConnections"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("CompileJobId") - .HasColumnType("bigint"); - - b.Property("LaunchSecurityLevel") - .HasColumnType("int"); - - b.Property("Port") - .HasColumnType("int"); - - b.Property("ProcessId") - .HasColumnType("int"); - - b.Property("RebootState") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("CompileJobId"); - - b.ToTable("ReattachInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("AccessToken") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("AccessUser") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("bit"); - - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("bit"); - - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("CommitterName") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("bit"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("bit"); - - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("bit"); - - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("bit"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("RepositorySettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("RevisionInformationId") - .HasColumnType("bigint"); - - b.Property("TestMergeId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("RevisionInformationId"); - - b.HasIndex("TestMergeId"); - - b.ToTable("RevInfoTestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CommitSha") - .IsRequired() - .HasColumnType("nvarchar(40)") - .HasMaxLength(40); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("nvarchar(40)") - .HasMaxLength(40); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); - - b.ToTable("RevisionInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Author") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Comment") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("MergedAt") - .HasColumnType("datetimeoffset"); - - b.Property("MergedById") - .HasColumnType("bigint"); - - b.Property("Number") - .HasColumnType("int"); - - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("bigint"); - - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("nvarchar(40)") - .HasMaxLength(40); - - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Url") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.HasIndex("MergedById"); - - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); - - b.ToTable("TestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("AdministrationRights") - .HasColumnType("decimal(20,0)"); - - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("datetimeoffset"); - - b.Property("CreatedById") - .HasColumnType("bigint"); - - b.Property("Enabled") - .IsRequired() - .HasColumnType("bit"); - - b.Property("InstanceManagerRights") - .HasColumnType("decimal(20,0)"); - - b.Property("LastPasswordUpdate") - .HasColumnType("datetimeoffset"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); - - b.Property("PasswordHash") - .HasColumnType("nvarchar(max)"); - - b.Property("SystemIdentifier") - .HasColumnType("nvarchar(450)"); - - b.HasKey("Id"); - - b.HasIndex("CanonicalName") - .IsUnique(); - - b.HasIndex("CreatedById"); - - b.HasIndex("SystemIdentifier") - .IsUnique() - .HasFilter("[SystemIdentifier] IS NOT NULL"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); - - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs deleted file mode 100644 index dff0936983..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155144_MSChangeOAuthExternalIdColumnToString.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using System; - -namespace Tgstation.Server.Host.Database.Migrations -{ - /// - /// Changes the OAuthConnections ExternalUserId column to a string for MSSQL. - /// - public partial class MSChangeOAuthExternalIdColumnToString : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - maxLength: 100, - nullable: false, - oldClrType: typeof(decimal), - oldType: "decimal(20,0)"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - type: "decimal(20,0)", - nullable: false, - oldClrType: typeof(string), - oldMaxLength: 100); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs deleted file mode 100644 index ffbf808040..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.Designer.cs +++ /dev/null @@ -1,811 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Tgstation.Server.Host.Database.Migrations -{ - [DbContext(typeof(MySqlDatabaseContext))] - [Migration("20201124155232_MYChangeOAuthExternalIdColumnToString")] - partial class MYChangeOAuthExternalIdColumnToString - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.10") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("ChannelLimit") - .IsRequired() - .HasColumnType("smallint unsigned"); - - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("Enabled") - .HasColumnType("tinyint(1)"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("Name") - .IsRequired() - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("int"); - - b.Property("ReconnectionInterval") - .IsRequired() - .HasColumnType("int unsigned"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "Name") - .IsUnique(); - - b.ToTable("ChatBots"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("ChatSettingsId") - .HasColumnType("bigint"); - - b.Property("DiscordChannelId") - .HasColumnType("bigint unsigned"); - - b.Property("IrcChannel") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); - - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("Tag") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.HasKey("Id"); - - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique(); - - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique(); - - b.ToTable("ChatChannels"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("DMApiMajorVersion") - .HasColumnType("int"); - - b.Property("DMApiMinorVersion") - .HasColumnType("int"); - - b.Property("DMApiPatchVersion") - .HasColumnType("int"); - - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("char(36)"); - - b.Property("DmeName") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("GitHubDeploymentId") - .HasColumnType("int"); - - b.Property("GitHubRepoId") - .HasColumnType("bigint"); - - b.Property("JobId") - .HasColumnType("bigint"); - - b.Property("MinimumSecurityLevel") - .HasColumnType("int"); - - b.Property("Output") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("RevisionInformationId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("DirectoryName"); - - b.HasIndex("JobId") - .IsUnique(); - - b.HasIndex("RevisionInformationId"); - - b.ToTable("CompileJobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("AutoStart") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("HeartbeatSeconds") - .IsRequired() - .HasColumnType("int unsigned"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("Port") - .IsRequired() - .HasColumnType("smallint unsigned"); - - b.Property("SecurityLevel") - .HasColumnType("int"); - - b.Property("StartupTimeout") - .IsRequired() - .HasColumnType("int unsigned"); - - b.Property("TopicRequestTimeout") - .IsRequired() - .HasColumnType("int unsigned"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamDaemonSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("ApiValidationPort") - .IsRequired() - .HasColumnType("smallint unsigned"); - - b.Property("ApiValidationSecurityLevel") - .HasColumnType("int"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("ProjectName") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamMakerSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("AutoUpdateInterval") - .IsRequired() - .HasColumnType("int unsigned"); - - b.Property("ChatBotLimit") - .IsRequired() - .HasColumnType("smallint unsigned"); - - b.Property("ConfigurationType") - .HasColumnType("int"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("Online") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("Path") - .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); - - b.HasKey("Id"); - - b.HasIndex("Path") - .IsUnique(); - - b.ToTable("Instances"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("ByondRights") - .HasColumnType("bigint unsigned"); - - b.Property("ChatBotRights") - .HasColumnType("bigint unsigned"); - - b.Property("ConfigurationRights") - .HasColumnType("bigint unsigned"); - - b.Property("DreamDaemonRights") - .HasColumnType("bigint unsigned"); - - b.Property("DreamMakerRights") - .HasColumnType("bigint unsigned"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("InstanceUserRights") - .HasColumnType("bigint unsigned"); - - b.Property("RepositoryRights") - .HasColumnType("bigint unsigned"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId"); - - b.HasIndex("UserId", "InstanceId") - .IsUnique(); - - b.ToTable("InstanceUsers"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("CancelRight") - .HasColumnType("bigint unsigned"); - - b.Property("CancelRightsType") - .HasColumnType("bigint unsigned"); - - b.Property("Cancelled") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("CancelledById") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("ErrorCode") - .HasColumnType("int unsigned"); - - b.Property("ExceptionDetails") - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("StartedAt") - .IsRequired() - .HasColumnType("datetime(6)"); - - b.Property("StartedById") - .HasColumnType("bigint"); - - b.Property("StoppedAt") - .HasColumnType("datetime(6)"); - - b.HasKey("Id"); - - b.HasIndex("CancelledById"); - - b.HasIndex("InstanceId"); - - b.HasIndex("StartedById"); - - b.ToTable("Jobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("int"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); - - b.ToTable("OAuthConnections"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("CompileJobId") - .HasColumnType("bigint"); - - b.Property("LaunchSecurityLevel") - .HasColumnType("int"); - - b.Property("Port") - .HasColumnType("smallint unsigned"); - - b.Property("ProcessId") - .HasColumnType("int"); - - b.Property("RebootState") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("CompileJobId"); - - b.ToTable("ReattachInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("AccessToken") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("AccessUser") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("CommitterName") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("RepositorySettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("RevisionInformationId") - .HasColumnType("bigint"); - - b.Property("TestMergeId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("RevisionInformationId"); - - b.HasIndex("TestMergeId"); - - b.ToTable("RevInfoTestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("CommitSha") - .IsRequired() - .HasColumnType("varchar(40) CHARACTER SET utf8mb4") - .HasMaxLength(40); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("varchar(40) CHARACTER SET utf8mb4") - .HasMaxLength(40); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); - - b.ToTable("RevisionInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("Author") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("Comment") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("MergedAt") - .HasColumnType("datetime(6)"); - - b.Property("MergedById") - .HasColumnType("bigint"); - - b.Property("Number") - .HasColumnType("int"); - - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("bigint"); - - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("varchar(40) CHARACTER SET utf8mb4") - .HasMaxLength(40); - - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("Url") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.HasKey("Id"); - - b.HasIndex("MergedById"); - - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); - - b.ToTable("TestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("AdministrationRights") - .HasColumnType("bigint unsigned"); - - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); - - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("datetime(6)"); - - b.Property("CreatedById") - .HasColumnType("bigint"); - - b.Property("Enabled") - .IsRequired() - .HasColumnType("tinyint(1)"); - - b.Property("InstanceManagerRights") - .HasColumnType("bigint unsigned"); - - b.Property("LastPasswordUpdate") - .HasColumnType("datetime(6)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); - - b.Property("PasswordHash") - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("SystemIdentifier") - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); - - b.HasKey("Id"); - - b.HasIndex("CanonicalName") - .IsUnique(); - - b.HasIndex("CreatedById"); - - b.HasIndex("SystemIdentifier") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); - - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs deleted file mode 100644 index 7af7221c98..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155232_MYChangeOAuthExternalIdColumnToString.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using System; - -namespace Tgstation.Server.Host.Database.Migrations -{ - /// - /// Changes the OAuthConnections ExternalUserId column to a string for MYSQL. - /// - public partial class MYChangeOAuthExternalIdColumnToString : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - maxLength: 100, - nullable: false, - oldClrType: typeof(ulong), - oldType: "bigint unsigned"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - type: "bigint unsigned", - nullable: false, - oldClrType: typeof(string), - oldMaxLength: 100); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs deleted file mode 100644 index 89f3c0de84..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.Designer.cs +++ /dev/null @@ -1,819 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Tgstation.Server.Host.Database.Migrations -{ - [DbContext(typeof(PostgresSqlDatabaseContext))] - [Migration("20201124155325_PGChangeOAuthExternalIdColumnToString")] - partial class PGChangeOAuthExternalIdColumnToString - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ChannelLimit") - .HasColumnType("integer"); - - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("Name") - .IsRequired() - .HasColumnType("character varying(100)") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("integer"); - - b.Property("ReconnectionInterval") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "Name") - .IsUnique(); - - b.ToTable("ChatBots"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ChatSettingsId") - .HasColumnType("bigint"); - - b.Property("DiscordChannelId") - .HasColumnType("numeric(20,0)"); - - b.Property("IrcChannel") - .HasColumnType("character varying(100)") - .HasMaxLength(100); - - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("Tag") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.HasKey("Id"); - - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique(); - - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique(); - - b.ToTable("ChatChannels"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("text"); - - b.Property("DMApiMajorVersion") - .HasColumnType("integer"); - - b.Property("DMApiMinorVersion") - .HasColumnType("integer"); - - b.Property("DMApiPatchVersion") - .HasColumnType("integer"); - - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("uuid"); - - b.Property("DmeName") - .IsRequired() - .HasColumnType("text"); - - b.Property("GitHubDeploymentId") - .HasColumnType("integer"); - - b.Property("GitHubRepoId") - .HasColumnType("bigint"); - - b.Property("JobId") - .HasColumnType("bigint"); - - b.Property("MinimumSecurityLevel") - .HasColumnType("integer"); - - b.Property("Output") - .IsRequired() - .HasColumnType("text"); - - b.Property("RevisionInformationId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("DirectoryName"); - - b.HasIndex("JobId") - .IsUnique(); - - b.HasIndex("RevisionInformationId"); - - b.ToTable("CompileJobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("AutoStart") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("HeartbeatSeconds") - .HasColumnType("bigint"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("Port") - .HasColumnType("integer"); - - b.Property("SecurityLevel") - .HasColumnType("integer"); - - b.Property("StartupTimeout") - .HasColumnType("bigint"); - - b.Property("TopicRequestTimeout") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamDaemonSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ApiValidationPort") - .HasColumnType("integer"); - - b.Property("ApiValidationSecurityLevel") - .HasColumnType("integer"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("ProjectName") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("DreamMakerSettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AutoUpdateInterval") - .HasColumnType("bigint"); - - b.Property("ChatBotLimit") - .HasColumnType("integer"); - - b.Property("ConfigurationType") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("Online") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("Path") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Path") - .IsUnique(); - - b.ToTable("Instances"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ByondRights") - .HasColumnType("numeric(20,0)"); - - b.Property("ChatBotRights") - .HasColumnType("numeric(20,0)"); - - b.Property("ConfigurationRights") - .HasColumnType("numeric(20,0)"); - - b.Property("DreamDaemonRights") - .HasColumnType("numeric(20,0)"); - - b.Property("DreamMakerRights") - .HasColumnType("numeric(20,0)"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("InstanceUserRights") - .HasColumnType("numeric(20,0)"); - - b.Property("RepositoryRights") - .HasColumnType("numeric(20,0)"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId"); - - b.HasIndex("UserId", "InstanceId") - .IsUnique(); - - b.ToTable("InstanceUsers"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("CancelRight") - .HasColumnType("numeric(20,0)"); - - b.Property("CancelRightsType") - .HasColumnType("numeric(20,0)"); - - b.Property("Cancelled") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("CancelledById") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("ErrorCode") - .HasColumnType("bigint"); - - b.Property("ExceptionDetails") - .HasColumnType("text"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("StartedAt") - .IsRequired() - .HasColumnType("timestamp with time zone"); - - b.Property("StartedById") - .HasColumnType("bigint"); - - b.Property("StoppedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CancelledById"); - - b.HasIndex("InstanceId"); - - b.HasIndex("StartedById"); - - b.ToTable("Jobs"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("character varying(100)") - .HasMaxLength(100); - - b.Property("Provider") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); - - b.ToTable("OAuthConnections"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("text"); - - b.Property("CompileJobId") - .HasColumnType("bigint"); - - b.Property("LaunchSecurityLevel") - .HasColumnType("integer"); - - b.Property("Port") - .HasColumnType("integer"); - - b.Property("ProcessId") - .HasColumnType("integer"); - - b.Property("RebootState") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CompileJobId"); - - b.ToTable("ReattachInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AccessToken") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("AccessUser") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("CommitterName") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("RepositorySettings"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("RevisionInformationId") - .HasColumnType("bigint"); - - b.Property("TestMergeId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("RevisionInformationId"); - - b.HasIndex("TestMergeId"); - - b.ToTable("RevInfoTestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("CommitSha") - .IsRequired() - .HasColumnType("character varying(40)") - .HasMaxLength(40); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("character varying(40)") - .HasMaxLength(40); - - b.HasKey("Id"); - - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); - - b.ToTable("RevisionInformations"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("MergedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MergedById") - .HasColumnType("bigint"); - - b.Property("Number") - .HasColumnType("integer"); - - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("bigint"); - - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("character varying(40)") - .HasMaxLength(40); - - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("text"); - - b.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("MergedById"); - - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); - - b.ToTable("TestMerges"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("AdministrationRights") - .HasColumnType("numeric(20,0)"); - - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedById") - .HasColumnType("bigint"); - - b.Property("Enabled") - .IsRequired() - .HasColumnType("boolean"); - - b.Property("InstanceManagerRights") - .HasColumnType("numeric(20,0)"); - - b.Property("LastPasswordUpdate") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("SystemIdentifier") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CanonicalName") - .IsUnique(); - - b.HasIndex("CreatedById"); - - b.HasIndex("SystemIdentifier") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); - - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs b/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs deleted file mode 100644 index 9be6a9fab5..0000000000 --- a/src/Tgstation.Server.Host/Database/Migrations/20201124155325_PGChangeOAuthExternalIdColumnToString.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using System; - -namespace Tgstation.Server.Host.Database.Migrations -{ - /// - /// Changes the OAuthConnections ExternalUserId column to a string for PostgreSQL. - /// - public partial class PGChangeOAuthExternalIdColumnToString : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - maxLength: 100, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(20,0)"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - throw new ArgumentNullException(nameof(migrationBuilder)); - - migrationBuilder.AlterColumn( - name: "ExternalUserId", - table: "OAuthConnections", - type: "numeric(20,0)", - nullable: false, - oldClrType: typeof(string), - oldMaxLength: 100); - } - } -} From a8f910692d13ba861fd496d08a3796445b49d522 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 25 Nov 2020 21:04:28 -0500 Subject: [PATCH 012/154] Casually replaces base64 file transfers - Now uses a proper streaming service --- docs/API.dox | 14 + src/Tgstation.Server.Api/Models/Byond.cs | 12 +- .../Models/ConfigurationFile.cs | 5 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 8 +- .../Models/FileTicketResult.cs | 13 + .../Models/Internal/RawData.cs | 15 - src/Tgstation.Server.Api/Models/Limits.cs | 9 +- src/Tgstation.Server.Api/Models/LogFile.cs | 3 +- src/Tgstation.Server.Api/Routes.cs | 7 +- .../AdministrationClient.cs | 26 +- src/Tgstation.Server.Client/ApiClient.cs | 109 ++++++- .../Components/ByondClient.cs | 20 +- .../Components/ConfigurationClient.cs | 58 +++- .../Components/IByondClient.cs | 6 +- .../Components/IConfigurationClient.cs | 13 +- .../IAdministrationClient.cs | 8 +- src/Tgstation.Server.Client/IApiClient.cs | 37 ++- .../Components/Byond/ByondManager.cs | 52 ++-- .../Components/Byond/IByondManager.cs | 9 +- .../Components/InstanceFactory.cs | 19 +- .../Components/StaticFiles/Configuration.cs | 105 +++++-- .../Components/StaticFiles/IConfiguration.cs | 9 +- .../Controllers/AdministrationController.cs | 17 +- .../Controllers/ByondController.cs | 82 +++++- .../Controllers/ConfigurationController.cs | 11 +- .../Controllers/TransferController.cs | 126 ++++++++ src/Tgstation.Server.Host/Core/Application.cs | 6 +- .../IO/DefaultIOManager.cs | 21 +- src/Tgstation.Server.Host/IO/IIOManager.cs | 17 +- src/Tgstation.Server.Host/Server.cs | 7 +- .../Transfer/FileDownloadProvider.cs | 41 +++ .../Transfer/FileTransferService.cs | 272 ++++++++++++++++++ .../Transfer/FileUploadProvider.cs | 108 +++++++ .../Transfer/IFileTransferStreamHandler.cs | 31 ++ .../Transfer/IFileTransferTicketProvider.cs | 23 ++ .../Transfer/IFileUploadTicket.cs | 33 +++ .../AdministrationTest.cs | 10 +- .../Instance/ByondTest.cs | 29 +- .../Instance/ConfigurationTest.cs | 16 +- .../Instance/WatchdogTest.cs | 1 + 40 files changed, 1230 insertions(+), 178 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/FileTicketResult.cs delete mode 100644 src/Tgstation.Server.Api/Models/Internal/RawData.cs create mode 100644 src/Tgstation.Server.Host/Controllers/TransferController.cs create mode 100644 src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs create mode 100644 src/Tgstation.Server.Host/Transfer/FileTransferService.cs create mode 100644 src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs create mode 100644 src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs create mode 100644 src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs create mode 100644 src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs diff --git a/docs/API.dox b/docs/API.dox index 6beed23239..f33f901cb2 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -162,6 +162,20 @@ I GET "/InstanceUser" => @ref Tgstation.Server.Api.Models.InstanceUser See individual documentation of each permission enum for their usage +@section api_transfer File Transfers + +Certain responses inherit from @ref Tgstation.Server.Api.Models.FileTicketResult. These are special in that, having that model's @ref Tgstation.Server.Api.Models.FileTicketResult.FileTicket field populated indicates there is a pending file download or upload to take place. These are handled in the "/Transfer" endpoint. + +To perform a file download make the following request: + +GET "/Transfer?ticket=<@ref Tgstation.Server.Api.Models.FileTicketResult.FileTicket>" => application/octet-stream + +To perform a file upload make the following request: + +PUT "/Transfer?ticket=<@ref Tgstation.Server.Api.Models.FileTicketResult.FileTicket>" application/octet-stream => OK + +File tickets are only valid for a short time after the initial request is made and should be dealt with immediately. Ensure that the file tickets are properly URL encoded. + @section api_user User Management TGS start with one user: "Admin". The password is "ISolemlySwearToDeleteTheDataDirectory" diff --git a/src/Tgstation.Server.Api/Models/Byond.cs b/src/Tgstation.Server.Api/Models/Byond.cs index 89e7db2a91..6241e28948 100644 --- a/src/Tgstation.Server.Api/Models/Byond.cs +++ b/src/Tgstation.Server.Api/Models/Byond.cs @@ -1,12 +1,11 @@ -using System; -using Tgstation.Server.Api.Models.Internal; +using System; namespace Tgstation.Server.Api.Models { /// - /// Represents a BYOND installation. is used to upload custom BYOND version zip files, though must still be set. + /// Represents a BYOND installation. is used to upload custom BYOND version zip files, though must still be set. /// - public sealed class Byond : RawData + public sealed class Byond : FileTicketResult { /// /// The of the installation used for new compiles. Will be if the user does not have permission to view it or there is no BYOND version installed. Only considers the and numbers. @@ -17,5 +16,10 @@ namespace Tgstation.Server.Api.Models /// The being used to install a new /// public Job? InstallJob { get; set; } + + /// + /// If a custom BYOND version is to be uploaded. + /// + public bool? UploadCustomZip { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/ConfigurationFile.cs b/src/Tgstation.Server.Api/Models/ConfigurationFile.cs index 83f952d9ca..7d5e0168e9 100644 --- a/src/Tgstation.Server.Api/Models/ConfigurationFile.cs +++ b/src/Tgstation.Server.Api/Models/ConfigurationFile.cs @@ -1,12 +1,11 @@ -using System.ComponentModel.DataAnnotations; -using Tgstation.Server.Api.Models.Internal; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models { /// /// Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete files /// - public sealed class ConfigurationFile : RawData + public sealed class ConfigurationFile : FileTicketResult { /// /// The path to the file diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 4c08b12b30..c372458735 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -166,7 +166,7 @@ namespace Tgstation.Server.Api.Models RequiresPosixSystemIdentity, /// - /// A was attem updated + /// A was updated. /// [Description("This existing file hash does not match, the file has beeen updated!")] ConfigurationFileUpdated, @@ -594,5 +594,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The requested OAuth provider is disabled via configuration!")] OAuthProviderDisabled, + + /// + /// A requiring a file upload did not receive it before timing out. + /// + [Description("The job did not receive a required upload before timing out!")] + FileUploadExpired, } } diff --git a/src/Tgstation.Server.Api/Models/FileTicketResult.cs b/src/Tgstation.Server.Api/Models/FileTicketResult.cs new file mode 100644 index 0000000000..87d22a78b2 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/FileTicketResult.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// Response for when file transfers are necessary. + /// + public class FileTicketResult + { + /// + /// The ticket to use to access the controller. + /// + public string? FileTicket { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Internal/RawData.cs b/src/Tgstation.Server.Api/Models/Internal/RawData.cs deleted file mode 100644 index 4414e9bba0..0000000000 --- a/src/Tgstation.Server.Api/Models/Internal/RawData.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Tgstation.Server.Api.Models.Internal -{ - /// - /// Represents raw bytes. - /// - public abstract class RawData - { - /// - /// The bytes of the . - /// -#pragma warning disable CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space - public byte[]? Content { get; set; } -#pragma warning restore CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space - } -} diff --git a/src/Tgstation.Server.Api/Models/Limits.cs b/src/Tgstation.Server.Api/Models/Limits.cs index c7108f2f25..6acd8a35bf 100644 --- a/src/Tgstation.Server.Api/Models/Limits.cs +++ b/src/Tgstation.Server.Api/Models/Limits.cs @@ -1,3 +1,5 @@ +using System; + namespace Tgstation.Server.Api.Models { /// @@ -19,5 +21,10 @@ namespace Tgstation.Server.Api.Models /// Length limit for git commit SHAs. /// public const int MaximumCommitShaLength = 40; + + /// + /// The maximum size for file transfers. + /// + public const int MaximumFileTransferSize = Int32.MaxValue; } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Api/Models/LogFile.cs b/src/Tgstation.Server.Api/Models/LogFile.cs index 3f67f93d11..873528fcb4 100644 --- a/src/Tgstation.Server.Api/Models/LogFile.cs +++ b/src/Tgstation.Server.Api/Models/LogFile.cs @@ -1,12 +1,11 @@ using System; -using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Api.Models { /// /// Represents a server log file. /// - public sealed class LogFile : RawData + public sealed class LogFile : FileTicketResult { /// /// The name of the log file. diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index 231475f728..64e46067ba 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -88,6 +88,11 @@ namespace Tgstation.Server.Api /// public const string Jobs = Root + nameof(Models.Job); + /// + /// The transfer controller. + /// + public const string Transfer = Root + "Transfer"; + /// /// The postfix for list operations /// @@ -109,7 +114,7 @@ namespace Tgstation.Server.Api public static string ListRoute(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/{1}", route, List); /// - /// Sanitize a path for use in a GET . + /// Sanitize a path for use in a GET . /// /// The path to sanitize. /// The sanitized path. diff --git a/src/Tgstation.Server.Client/AdministrationClient.cs b/src/Tgstation.Server.Client/AdministrationClient.cs index ec1d99513d..ca00100e8b 100644 --- a/src/Tgstation.Server.Client/AdministrationClient.cs +++ b/src/Tgstation.Server.Client/AdministrationClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Web; @@ -38,10 +39,25 @@ namespace Tgstation.Server.Client public Task> ListLogs(CancellationToken cancellationToken) => apiClient.Read>(Routes.Logs, cancellationToken); /// - public Task GetLog(LogFile logFile, CancellationToken cancellationToken) => apiClient.Read( - Routes.Logs + Routes.SanitizeGetPath( - HttpUtility.UrlEncode( - logFile?.Name ?? throw new ArgumentNullException(nameof(logFile)))), - cancellationToken); + public async Task> GetLog(LogFile logFile, CancellationToken cancellationToken) + { + var resultFile = await apiClient.Read( + Routes.Logs + Routes.SanitizeGetPath( + HttpUtility.UrlEncode( + logFile?.Name ?? throw new ArgumentNullException(nameof(logFile)))), + cancellationToken) + .ConfigureAwait(false); + + var stream = await apiClient.Download(resultFile, cancellationToken).ConfigureAwait(false); + try + { + return Tuple.Create(resultFile, stream); + } + catch + { + stream.Dispose(); + throw; + } + } } } diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 0e107e91f8..558628d0e3 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -1,15 +1,18 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Web; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; @@ -143,29 +146,71 @@ namespace Tgstation.Server.Client /// If this is a token refresh operation. /// The for the operation /// A resulting in the response on success - async Task RunRequest(string route, object? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken) + Task RunRequest( + string route, + object? body, + HttpMethod method, + long? instanceId, + bool tokenRefresh, + CancellationToken cancellationToken) + { + HttpContent? content = null; + if(body != null) + content = new StringContent( + JsonConvert.SerializeObject(body, GetSerializerSettings()), + Encoding.UTF8, + MediaTypeNames.Application.Json); + + return RunRequest( + route, + content, + method, + instanceId, + tokenRefresh, + cancellationToken); + } + + /// + /// Main request method + /// + /// The resulting POCO type + /// The route to run + /// The of the request if any. + /// The method of the request + /// The optional instance for the request + /// If this is a token refresh operation. + /// The for the operation + /// A resulting in the response on success + async Task RunRequest( + string route, + HttpContent? content, + HttpMethod method, + long? instanceId, + bool tokenRefresh, + CancellationToken cancellationToken) { if (route == null) throw new ArgumentNullException(nameof(route)); if (method == null) throw new ArgumentNullException(nameof(method)); - if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put)) - throw new InvalidOperationException("Body cannot be null for POST or PUT!"); + if (content == null && (method == HttpMethod.Post || method == HttpMethod.Put)) + throw new InvalidOperationException("content cannot be null for POST or PUT!"); HttpResponseMessage response; var fullUri = new Uri(Url, route); var serializerSettings = GetSerializerSettings(); + var fileDownload = typeof(TResult) == typeof(Stream); using (var request = new HttpRequestMessage(method, fullUri)) { - if (body != null) - request.Content = new StringContent( - JsonConvert.SerializeObject(body, serializerSettings), - Encoding.UTF8, - MediaTypeNames.Application.Json); + if (content != null) + request.Content = content; var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers; headersToUse.SetRequestHeaders(request.Headers, instanceId); + if (fileDownload) + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Octet)); + await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false); response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); @@ -175,6 +220,12 @@ namespace Tgstation.Server.Client { await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false); + if (fileDownload && response.IsSuccessStatusCode) + { + // just stream + return (TResult)(object)await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) @@ -182,7 +233,7 @@ namespace Tgstation.Server.Client if (!tokenRefresh && response.StatusCode == HttpStatusCode.Unauthorized && await RefreshToken(cancellationToken).ConfigureAwait(false)) - return await RunRequest(route, body, method, instanceId, false, cancellationToken).ConfigureAwait(false); + return await RunRequest(route, content, method, instanceId, false, cancellationToken).ConfigureAwait(false); HandleBadResponse(response, json); } @@ -277,5 +328,41 @@ namespace Tgstation.Server.Client /// public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger))); + + /// + public Task Download(FileTicketResult ticket, CancellationToken cancellationToken) + { + if (ticket == null) + throw new ArgumentNullException(nameof(ticket)); + + return RunRequest( + $"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}", + null, + HttpMethod.Get, + null, + false, + cancellationToken); + } + + /// + public async Task Upload(FileTicketResult ticket, Stream? uploadStream, CancellationToken cancellationToken) + { + if (ticket == null) + throw new ArgumentNullException(nameof(ticket)); + + MemoryStream? memoryStream = null; + if (uploadStream == null) + memoryStream = new MemoryStream(); + + using (memoryStream) + await RunRequest( + $"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}", + new StreamContent(uploadStream ?? memoryStream), + HttpMethod.Put, + null, + false, + cancellationToken) + .ConfigureAwait(false); + } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index 23d56dc64a..15eb50bb7d 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -38,6 +39,19 @@ namespace Tgstation.Server.Client.Components 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); + public async Task SetActiveVersion(Byond byond, Stream zipFileStream, CancellationToken cancellationToken) + { + var result = await apiClient.Update( + Routes.Byond, + byond ?? throw new ArgumentNullException(nameof(byond)), + instance.Id, + cancellationToken) + .ConfigureAwait(false); + + if (byond.UploadCustomZip == true) + await apiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false); + + return result; + } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index 3134fbcf37..f233d070c4 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -1,5 +1,9 @@ -using System; +using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -41,17 +45,61 @@ namespace Tgstation.Server.Client.Components public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory), instance.Id, cancellationToken); /// - public Task Read(ConfigurationFile file, CancellationToken cancellationToken) + public async Task> Read(ConfigurationFile file, CancellationToken cancellationToken) { if (file == null) throw new ArgumentNullException(nameof(file)); - return apiClient.Read( + var configFile = await apiClient.Read( Routes.ConfigurationFile + Routes.SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))), instance.Id, - cancellationToken); + cancellationToken) + .ConfigureAwait(false); + var downloadStream = await apiClient.Download(configFile, cancellationToken).ConfigureAwait(false); + try + { + return Tuple.Create(configFile, downloadStream); + } + catch + { + downloadStream.Dispose(); + throw; + } } /// - public Task Write(ConfigurationFile file, CancellationToken cancellationToken) => apiClient.Update(Routes.Configuration, file ?? throw new ArgumentNullException(nameof(file)), instance.Id, cancellationToken); + public async Task Write(ConfigurationFile file, Stream uploadStream, CancellationToken cancellationToken) + { + MemoryStream? memoryStream = null; + if (uploadStream != null) + memoryStream = new MemoryStream(); + + using (memoryStream) + { + var configFileTask = apiClient.Update( + Routes.Configuration, + file ?? throw new ArgumentNullException(nameof(file)), + instance.Id, + cancellationToken); + + if (uploadStream != null) + await uploadStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + + var configFile = await configFileTask.ConfigureAwait(false); + + // minor improvement to "fix" a lost feature that used to be in API 7 + // since LastReadHash is no longer updated until the next GET request, we can use the same calculations here to generate it. + if (uploadStream != null) +#pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1. + using (var sha1 = new SHA1Managed()) +#pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1. + configFile.LastReadHash = String.Join(String.Empty, sha1.ComputeHash(memoryStream).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + else + configFile.LastReadHash = null; + + await apiClient.Upload(configFile, memoryStream, cancellationToken).ConfigureAwait(false); + + return configFile; + } + } } } diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs index da4f4d45a3..44c90f64c4 100644 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ b/src/Tgstation.Server.Client/Components/IByondClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -28,8 +29,9 @@ namespace Tgstation.Server.Client.Components /// Updates the information /// /// The information to update + /// The for the .zip file if is . /// The for the operation /// A resulting in the updated information - Task SetActiveVersion(Byond byond, CancellationToken cancellationToken); + Task SetActiveVersion(Byond byond, Stream zipFileStream, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs index d61e426af3..e82220c028 100644 --- a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -23,16 +25,17 @@ namespace Tgstation.Server.Client.Components /// /// The file to read /// The for the operation - /// A representing the running operation - Task Read(ConfigurationFile file, CancellationToken cancellationToken); + /// A resulting in a containing the and downloaded . + Task> Read(ConfigurationFile file, CancellationToken cancellationToken); /// /// Overwrite a file /// /// The file to write + /// The of uploaded data. If , a delete will be attempted. /// The for the operation - /// A resulting in the new - Task Write(ConfigurationFile file, CancellationToken cancellationToken); + /// A resulting in the new . + Task Write(ConfigurationFile file, Stream uploadStream, CancellationToken cancellationToken); /// /// Delete an empty diff --git a/src/Tgstation.Server.Client/IAdministrationClient.cs b/src/Tgstation.Server.Client/IAdministrationClient.cs index bbffab6f0f..b77e7475f7 100644 --- a/src/Tgstation.Server.Client/IAdministrationClient.cs +++ b/src/Tgstation.Server.Client/IAdministrationClient.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -44,7 +46,7 @@ namespace Tgstation.Server.Client /// /// The to download. /// The for the operation - /// A resulting in the downloaded . - Task GetLog(LogFile logFile, CancellationToken cancellationToken); + /// A resulting a containing the downloaded and associated . + Task> GetLog(LogFile logFile, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs index bee5ea5328..94cc657f0a 100644 --- a/src/Tgstation.Server.Client/IApiClient.cs +++ b/src/Tgstation.Server.Client/IApiClient.cs @@ -1,7 +1,9 @@ -using System; +using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client { @@ -113,7 +115,7 @@ namespace Tgstation.Server.Client /// The type of the response body /// The server route to make the request to /// The request body - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken); @@ -123,7 +125,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Create(string route, long instanceId, CancellationToken cancellationToken); @@ -133,7 +135,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Patch(string route, long instanceId, CancellationToken cancellationToken); @@ -143,7 +145,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Read(string route, long instanceId, CancellationToken cancellationToken); @@ -155,7 +157,7 @@ namespace Tgstation.Server.Client /// The type of the response body /// The server route to make the request to /// The request body - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken); @@ -164,7 +166,7 @@ namespace Tgstation.Server.Client /// Run an HTTP DELETE request /// /// The server route to make the request to - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A representing the running operation Task Delete(string route, long instanceId, CancellationToken cancellationToken); @@ -175,7 +177,7 @@ namespace Tgstation.Server.Client /// The type to of the request body /// The server route to make the request to /// The request body - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A representing the running operation Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken); @@ -185,9 +187,26 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The instance to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Delete(string route, long instanceId, CancellationToken cancellationToken); + + /// + /// Downloads a file for a given . + /// + /// The to download. + /// The for the operation. + /// A resulting in the downloaded . + Task Download(FileTicketResult ticket, CancellationToken cancellationToken); + + /// + /// Uploads a given for a given . + /// + /// The to download. + /// The to upload. represents an empty file. + /// The for the operation. + /// A representing the running operation. + Task Upload(FileTicketResult ticket, Stream? uploadStream, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 72ea830c82..5d97995cb6 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Text; @@ -121,10 +122,10 @@ namespace Tgstation.Server.Host.Components.Byond /// Installs a BYOND if it isn't already /// /// The BYOND to install - /// Custom zip file bytes to use. Will cause a number to be added. + /// Custom zip file to use. Will cause a number to be added. /// The for the operation /// A representing the running operation - async Task InstallVersion(Version version, byte[] versionZipBytes, CancellationToken cancellationToken) + async Task InstallVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken) { var ourTcs = new TaskCompletionSource(); Task inProgressTask; @@ -132,7 +133,7 @@ namespace Tgstation.Server.Host.Components.Byond bool installed; lock (installedVersions) { - if (versionZipBytes != null) + if (customVersionStream != null) { int customInstallationNumber = 1; do @@ -157,7 +158,7 @@ namespace Tgstation.Server.Host.Components.Byond return versionKey; } - if (versionZipBytes != null) + if (customVersionStream != null) logger.LogInformation("Installing custom BYOND version as {0}...", versionKey); else if (version.Build > 0) throw new JobException(ErrorCode.ByondNonExistentCustomVersion); @@ -168,26 +169,43 @@ namespace Tgstation.Server.Host.Components.Byond try { await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List { versionKey }, cancellationToken).ConfigureAwait(false); - var zipFileBytesTask = versionZipBytes == null - ? byondInstaller.DownloadVersion(version, cancellationToken) - : Task.FromResult(versionZipBytes); - await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false); + var extractPath = ioManager.ResolvePath(versionKey); + async Task DirectoryCleanup() + { + await ioManager.DeleteDirectory(extractPath, cancellationToken).ConfigureAwait(false); + await ioManager.CreateDirectory(extractPath, cancellationToken).ConfigureAwait(false); + } + var directoryCleanupTask = DirectoryCleanup(); try { - versionZipBytes = await zipFileBytesTask.ConfigureAwait(false); - await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false); + Stream versionZipStream; + Stream downloadedStream = null; + if (customVersionStream == null) + { + var bytes = await byondInstaller.DownloadVersion(version, cancellationToken).ConfigureAwait(false); + downloadedStream = new MemoryStream(bytes); + versionZipStream = downloadedStream; + } + else + versionZipStream = customVersionStream; - var extractPath = ioManager.ResolvePath(versionKey); - logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath); - await ioManager.ZipToDirectory(extractPath, versionZipBytes, cancellationToken).ConfigureAwait(false); - versionZipBytes = null; + using (downloadedStream) + { + await directoryCleanupTask.ConfigureAwait(false); + logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath); + await ioManager.ZipToDirectory(extractPath, versionZipStream, cancellationToken).ConfigureAwait(false); + } await byondInstaller.InstallByond(extractPath, version, cancellationToken).ConfigureAwait(false); // make sure to do this last because this is what tells us we have a valid version in the future - await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false); + await ioManager.WriteAllBytes( + ioManager.ConcatPath(versionKey, VersionFileName), + Encoding.UTF8.GetBytes(versionKey), + cancellationToken) + .ConfigureAwait(false); } catch (WebException e) { @@ -220,12 +238,12 @@ namespace Tgstation.Server.Host.Components.Byond } /// - public async Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken) + public async Task ChangeVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken) { if (version == null) throw new ArgumentNullException(nameof(version)); - var versionKey = await InstallVersion(version, customVersionBytes, cancellationToken).ConfigureAwait(false); + var versionKey = await InstallVersion(version, customVersionStream, cancellationToken).ConfigureAwait(false); using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs index 650bce0209..129fa54037 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs @@ -1,6 +1,7 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -25,10 +26,10 @@ namespace Tgstation.Server.Host.Components.Byond /// Change the active BYOND version /// /// The new - /// Optional s of a custom BYOND version zip file. + /// Optional of a custom BYOND version zip file. /// The for the operation /// A representing the running operation - Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken); + Task ChangeVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken); /// /// Lock the current installation's location and return a @@ -38,4 +39,4 @@ namespace Tgstation.Server.Host.Components.Byond /// A resulting in the requested Task UseExecutables(Version requiredVersion, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 548410eeef..1045c8ed8b 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -19,6 +19,7 @@ using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Transfer; namespace Tgstation.Server.Host.Components { @@ -125,6 +126,11 @@ namespace Tgstation.Server.Host.Components /// readonly IServerPortProvider serverPortProvider; + /// + /// The for the . + /// + readonly IFileTransferTicketProvider fileTransferService; + /// /// The for the . /// @@ -153,6 +159,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The value of . /// The containing the value of . public InstanceFactory( IIOManager ioManager, @@ -175,6 +182,7 @@ namespace Tgstation.Server.Host.Components ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands repositoryCommands, IServerPortProvider serverPortProvider, + IFileTransferTicketProvider fileTransferService, IOptions generalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -197,6 +205,7 @@ namespace Tgstation.Server.Host.Components this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); + this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } @@ -214,7 +223,15 @@ namespace Tgstation.Server.Host.Components var diagnosticsIOManager = new ResolvingIOManager(instanceIoManager, "Diagnostics"); var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration"); - var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger()); + var configuration = new StaticFiles.Configuration( + configurationIoManager, + synchronousIOManager, + symlinkFactory, + processExecutor, + postWriteHandler, + platformIdentifier, + fileTransferService, + loggerFactory.CreateLogger()); var eventConsumer = new EventConsumer(configuration); var repoManager = new RepositoryManager( repositoryFactory, diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 5805339a58..6260047ce3 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -15,6 +15,7 @@ using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Transfer; namespace Tgstation.Server.Host.Components.StaticFiles { @@ -76,6 +77,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The for . + /// > + readonly IFileTransferTicketProvider fileTransferService; + /// /// The for /// @@ -86,6 +92,16 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// readonly SemaphoreSlim semaphore; + /// + /// The that is triggered when is called. + /// + readonly CancellationTokenSource disposeCts; + + /// + /// The culmination of all upload file transfer callbacks. + /// + Task uploadTasks; + /// /// Construct /// @@ -95,6 +111,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The value of /// The value of /// The value of + /// The value of . /// The value of public Configuration( IIOManager ioManager, @@ -103,6 +120,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, + IFileTransferTicketProvider fileTransferService, ILogger logger) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -111,13 +129,21 @@ namespace Tgstation.Server.Host.Components.StaticFiles this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); semaphore = new SemaphoreSlim(1); + disposeCts = new CancellationTokenSource(); + uploadTasks = Task.CompletedTask; } /// - public void Dispose() => semaphore.Dispose(); + public void Dispose() + { + semaphore.Dispose(); + disposeCts.Cancel(); + disposeCts.Dispose(); + } /// /// Get the proper path to @@ -246,17 +272,39 @@ namespace Tgstation.Server.Host.Components.StaticFiles lock (semaphore) try { - var content = synchronousIOManager.ReadFile(path); - string sha1String; + string GetFileSha() + { + var content = synchronousIOManager.ReadFile(path); #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1. - using (var sha1 = new SHA1Managed()) + using var sha1 = new SHA1Managed(); #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1. - sha1String = String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + return String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + } + + var originalSha = GetFileSha(); + + var disposeToken = disposeCts.Token; + var fileTicket = fileTransferService.CreateDownload( + new FileDownloadProvider( + cancellationToken => + { + if (disposeToken.IsCancellationRequested) + return Task.FromResult(ErrorCode.InstanceOffline); + + var newSha = GetFileSha(); + if (newSha != originalSha) + return Task.FromResult(ErrorCode.ConfigurationFileUpdated); + + return Task.FromResult(null); + }, + path, + false)); + result = new ConfigurationFile { - Content = content, + FileTicket = fileTicket.FileTicket, IsDirectory = false, - LastReadHash = sha1String, + LastReadHash = originalSha, AccessDenied = false, Path = configurationRelativePath }; @@ -365,7 +413,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public async Task Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken) + public async Task Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -377,20 +425,45 @@ namespace Tgstation.Server.Host.Components.StaticFiles lock (semaphore) try { - var fileHash = previousHash; - var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken); - if (!success) - return; - if (data != null) - postWriteHandler.HandleWrite(path); + var fileTicket = fileTransferService.CreateUpload(); + var uploadCancellationToken = disposeCts.Token; + async Task UploadHandler() + { + using (fileTicket) + { + byte[] data; + var fileHash = previousHash; + using (var ms = new MemoryStream()) + { + using (var stream = await fileTicket.GetResult(uploadCancellationToken).ConfigureAwait(false)) + await stream.CopyToAsync(ms, uploadCancellationToken).ConfigureAwait(false); + data = ms.ToArray(); + if (data.Length == 0) + data = null; + } + + var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken); + if (!success) + fileTicket.SetErrorMessage(new ErrorMessage(ErrorCode.ConfigurationFileUpdated) + { + AdditionalData = fileHash + }); + else if(data != null) + postWriteHandler.HandleWrite(path); + } + } + result = new ConfigurationFile { - Content = data, + FileTicket = fileTicket.Ticket.FileTicket, + LastReadHash = previousHash, IsDirectory = false, - LastReadHash = fileHash, AccessDenied = false, Path = configurationRelativePath }; + + lock (disposeCts) + uploadTasks = Task.WhenAll(uploadTasks, UploadHandler()); } catch (UnauthorizedAccessException) { diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index b57497a4d1..34a42d299e 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Threading; @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The relative path in the Configuration directory /// The for the operation. If , the operation will be performed as the user of the /// The for the operation - /// A resulting in the s for the items in the directory. and will both be + /// A resulting in the s for the items in the directory. and will both be Task> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); /// @@ -72,10 +72,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// /// The relative path in the Configuration directory /// The for the operation. If , the operation will be performed as the user of the - /// The data to write. If , the file is deleted /// The hash any existing file must match in order for the write to succeed /// The for the operation. Usage may result in partial writes /// A resulting in the updated or if the write failed due to conflicts - Task Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken); + Task Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index a41fb7895c..28f13c3c97 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -20,6 +20,7 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Transfer; namespace Tgstation.Server.Host.Controllers { @@ -56,6 +57,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The for the . + /// + readonly IFileTransferTicketProvider fileTransferService; + /// /// The for the /// @@ -81,6 +87,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of + /// The value of . /// The for the /// The containing value of /// The containing value of @@ -93,6 +100,7 @@ namespace Tgstation.Server.Host.Controllers IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPlatformIdentifier platformIdentifier, + IFileTransferTicketProvider fileTransferService, ILogger logger, IOptions updatesConfigurationOptions, IOptions generalConfigurationOptions, @@ -108,6 +116,7 @@ namespace Tgstation.Server.Host.Controllers this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); @@ -386,13 +395,19 @@ namespace Tgstation.Server.Host.Controllers path); try { + var fileTransferTicket = fileTransferService.CreateDownload( + new FileDownloadProvider( + cancellationToken => Task.FromResult(null), + fullPath, + true)); + var readTask = ioManager.ReadAllBytes(fullPath, cancellationToken); return Ok(new LogFile { Name = path, LastModified = await ioManager.GetLastModified(fullPath, cancellationToken).ConfigureAwait(false), - Content = await readTask.ConfigureAwait(false) + FileTicket = fileTransferTicket.FileTicket }); } catch (IOException ex) diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index cab2dd1fb2..93dbd33cbc 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -1,7 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,6 +14,7 @@ using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Transfer; namespace Tgstation.Server.Host.Controllers { @@ -27,6 +29,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly IJobManager jobManager; + /// + /// The for the . + /// + readonly IFileTransferTicketProvider fileTransferService; + /// /// Construct a /// @@ -34,12 +41,14 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the . /// The value of + /// The value of . /// The for the public ByondController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, + IFileTransferTicketProvider fileTransferService, ILogger logger) : base( instanceManager, @@ -48,6 +57,7 @@ namespace Tgstation.Server.Host.Controllers logger) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); } /// @@ -104,14 +114,16 @@ namespace Tgstation.Server.Host.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); + var uploadingZip = model.UploadCustomZip == true; + if (model.Version == null || model.Version.Revision != -1 - || (model.Content != null && model.Version.Build > 0)) + || (uploadingZip && model.Version.Build > 0)) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); var userByondRights = AuthenticationContext.InstanceUser.ByondRights.Value; - if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && model.Content == null) - || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && model.Content != null)) + if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip) + || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip)) return Forbid(); // remove cruff fields @@ -120,7 +132,7 @@ namespace Tgstation.Server.Host.Controllers async instance => { var byondManager = instance.ByondManager; - if (model.Content == null && byondManager.InstalledVersions.Any(x => x == model.Version)) + if (!uploadingZip && byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation( "User ID {0} changing instance ID {1} BYOND version to {2}", @@ -146,21 +158,61 @@ namespace Tgstation.Server.Host.Controllers // run the install through the job manager var job = new Models.Job { - Description = $"Install {(model.Content == null ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", + Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; - await jobManager.RegisterOperation( - job, - (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => core.ByondManager.ChangeVersion( - model.Version, - model.Content, - jobCancellationToken), - cancellationToken) - .ConfigureAwait(false); - result.InstallJob = job.ToApi(); + + IFileUploadTicket fileUploadTicket = null; + if (uploadingZip) + fileUploadTicket = fileTransferService.CreateUpload(); + + try + { + await jobManager.RegisterOperation( + job, + async (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => + { + Stream zipFileStream = null; + if (fileUploadTicket != null) + using (fileUploadTicket) + { + var uploadStream = await fileUploadTicket.GetResult(jobCancellationToken).ConfigureAwait(false); + if (uploadStream == null) + throw new JobException(ErrorCode.FileUploadExpired); + + zipFileStream = new MemoryStream(); + try + { + await uploadStream.CopyToAsync(zipFileStream, jobCancellationToken).ConfigureAwait(false); + } + catch + { + await zipFileStream.DisposeAsync().ConfigureAwait(false); + throw; + } + } + + using (zipFileStream) + await core.ByondManager.ChangeVersion( + model.Version, + zipFileStream, + jobCancellationToken) + .ConfigureAwait(false); + }, + cancellationToken) + .ConfigureAwait(false); + + result.InstallJob = job.ToApi(); + result.FileTicket = fileUploadTicket?.Ticket.FileTicket; + } + catch + { + fileUploadTicket?.Dispose(); + throw; + } } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 3a76b8bc5b..3d82b23fe8 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -77,11 +77,11 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the for the operation. /// File updated successfully. - /// File created successfully. + /// File upload ticket created successfully. [HttpPost] [TgsAuthorize(ConfigurationRights.Write)] [ProducesResponseType(typeof(ConfigurationFile), 200)] - [ProducesResponseType(typeof(ConfigurationFile), 201)] + [ProducesResponseType(typeof(ConfigurationFile), 202)] public async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken) { if (model == null) @@ -99,16 +99,11 @@ namespace Tgstation.Server.Host.Controllers .Write( model.Path, systemIdentity, - model.Content, model.LastReadHash, cancellationToken) .ConfigureAwait(false); - if (newFile == null) - return Conflict(new ErrorMessage(ErrorCode.ConfigurationFileUpdated)); - newFile.Content = null; - - return model.LastReadHash == null ? (IActionResult)Created(newFile) : Json(newFile); + return model.LastReadHash == null ? (IActionResult)Accepted(newFile) : Json(newFile); }) .ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Controllers/TransferController.cs b/src/Tgstation.Server.Host/Controllers/TransferController.cs new file mode 100644 index 0000000000..5dcf554842 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/TransferController.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Net; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Transfer; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for file streaming. + /// + [Route(Routes.Transfer)] + [RequestSizeLimit(Limits.MaximumFileTransferSize)] + public sealed class TransferController : ApiController + { + /// + /// The for the . + /// + readonly IFileTransferStreamHandler fileTransferService; + + /// + /// Initializes a new instance of the . + /// + /// The for the + /// The for the + /// The value of . + /// The for the + public TransferController( + IDatabaseContext databaseContext, + IAuthenticationContextFactory authenticationContextFactory, + IFileTransferStreamHandler fileTransferService, + ILogger logger) + : base( + databaseContext, + authenticationContextFactory, + logger, + true) + { + this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); + } + + /// + /// Downloads a file with a given . + /// + /// The for the download. + /// The for the operation. + /// A resulting in the of the method. + /// Started streaming download successfully. + /// The was no longer or was never valid. + [TgsAuthorize] + [HttpGet] + [Produces(MediaTypeNames.Application.Octet, MediaTypeNames.Application.Json)] + public async Task Download([FromQuery] string ticket, CancellationToken cancellationToken) + { + if (ticket == null) + throw new ArgumentNullException(nameof(ticket)); + + var streamAccept = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); + if (!Request.GetTypedHeaders().Accept.Any(x => streamAccept.IsSubsetOf(x))) + return StatusCode(HttpStatusCode.NotAcceptable, new ErrorMessage(ErrorCode.BadHeaders) + { + AdditionalData = $"File downloads must accept both {MediaTypeNames.Application.Octet} and {MediaTypeNames.Application.Json}!" + }); + + var fileTicketResult = new FileTicketResult + { + FileTicket = ticket + }; + + var tuple = await fileTransferService.RetrieveDownloadStream(fileTicketResult, cancellationToken).ConfigureAwait(false); + var stream = tuple.Item1; + try + { + if (tuple.Item2 != null) + return Conflict(tuple.Item2); + + if (stream == null) + return Gone(); + + return new FileStreamResult(stream, new MediaTypeHeaderValue(MediaTypeNames.Application.Octet)); + } + catch + { + stream.Dispose(); + throw; + } + } + + /// + /// Uploads a file with a given . + /// + /// The for the upload. + /// The for the operation. + /// A resulting in the of the method. + /// Uploaded file successfully. + /// The was no longer or was never valid. + [TgsAuthorize] + [HttpPut] + public async Task Upload([FromQuery] string ticket, CancellationToken cancellationToken) + { + if (ticket == null) + throw new ArgumentNullException(nameof(ticket)); + + var fileTicketResult = new FileTicketResult + { + FileTicket = ticket + }; + + var result = await fileTransferService.SetUploadStream(fileTicketResult, Request.Body, cancellationToken).ConfigureAwait(false); + if (result != null) + return Conflict(result); + + return Created(new object()); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 80d71e27c0..77cdd26224 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -40,6 +40,7 @@ using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; using Tgstation.Server.Host.Setup; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Transfer; namespace Tgstation.Server.Host.Core { @@ -304,12 +305,15 @@ namespace Tgstation.Server.Host.Core } // configure misc services + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); // configure component services services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index d63c32753c..6c5a55ab69 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -122,7 +122,7 @@ namespace Tgstation.Server.Host.IO /// public async Task CopyDirectory(string src, string dest, IEnumerable ignore, CancellationToken cancellationToken) { - if (dest == null) + if (src == null) throw new ArgumentNullException(nameof(src)); if (dest == null) throw new ArgumentNullException(nameof(src)); @@ -134,12 +134,7 @@ namespace Tgstation.Server.Host.IO } /// - public string ConcatPath(params string[] paths) - { - if (paths == null) - throw new ArgumentNullException(nameof(paths)); - return Path.Combine(paths); - } + public string ConcatPath(params string[] paths) => Path.Combine(paths); /// public async Task CopyFile(string src, string dest, CancellationToken cancellationToken) @@ -308,14 +303,13 @@ namespace Tgstation.Server.Host.IO } /// - public Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { path = ResolvePath(path); - if (zipFileBytes == null) - throw new ArgumentNullException(nameof(zipFileBytes)); + if (zipFile == null) + throw new ArgumentNullException(nameof(zipFile)); - using var ms = new MemoryStream(zipFileBytes); - using var archive = new ZipArchive(ms, ZipArchiveMode.Read); + using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read); archive.ExtractToDirectory(path); }, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); @@ -329,5 +323,8 @@ namespace Tgstation.Server.Host.IO var fileInfo = new FileInfo(path); return new DateTimeOffset(fileInfo.LastWriteTimeUtc); }, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); + + /// + public Stream GetFileStream(string path, bool shareWrite) => new FileStream(ResolvePath(path), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None), DefaultBufferSize, true); } } diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index d18e117864..2f2c3caac3 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -187,13 +188,13 @@ namespace Tgstation.Server.Host.IO Task DownloadFile(Uri url, CancellationToken cancellationToken); /// - /// Extract a set of to a given + /// Extract a set of to a given /// /// The path to unzip to - /// The s of the + /// The of the /// The for the operation /// A representing the running operation - Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken); + Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken); /// /// Get the of when a given was last modified. @@ -202,5 +203,13 @@ namespace Tgstation.Server.Host.IO /// The for the operation. /// A resulting in the of when the file was last modified. Task GetLastModified(string path, CancellationToken cancellationToken); + + /// + /// Gets the for a given file . + /// + /// The path of the file. + /// If should be used. + /// The of the file. + Stream GetFileStream(string path, bool shareWrite); } } diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 8ce353d0ae..dbfaee0b7a 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -194,8 +194,11 @@ namespace Tgstation.Server.Host var cancellationToken = cancellationTokenSource.Token; logger.LogTrace("Downloading zip package..."); - var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false); - + using var updateZipData = new MemoryStream( + await ioManager.DownloadFile( + updateZipUrl, + cancellationToken) + .ConfigureAwait(false)); try { logger.LogTrace("Exctracting zip package to {0}...", updatePath); diff --git a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs new file mode 100644 index 0000000000..a01bdee992 --- /dev/null +++ b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Transfer +{ + /// + /// Represents a file on disk to be downloaded. + /// + public sealed class FileDownloadProvider + { + /// + /// A of a to run before providing the download. If it returns a non-null , a 400 error with that code will be returned instead of a download stream. + /// + public Func> ActivationCallback { get; } + + /// + /// The full path to the file on disk to download. + /// + public string FilePath { get; } + + /// + /// If the file read stream should be allowed to share writes. + /// + public bool ShareWrite { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + public FileDownloadProvider(Func> activationCallback, string filePath, bool shareWrite) + { + ActivationCallback = activationCallback ?? throw new ArgumentNullException(nameof(activationCallback)); + FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + ShareWrite = shareWrite; + } + } +} diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs new file mode 100644 index 0000000000..9b520f9690 --- /dev/null +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -0,0 +1,272 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Transfer +{ + /// + /// Implementation of the file transfer service. + /// + sealed class FileTransferService : IFileTransferTicketProvider, IFileTransferStreamHandler, IAsyncDisposable + { + /// + /// Number of minutes before transfer ticket expire. + /// + const int TicketValidityMinutes = 5; + + /// + /// The for the . + /// + readonly ICryptographySuite cryptographySuite; + + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// of s to upload s. + /// + readonly Dictionary uploadTickets; + + /// + /// of s to s. + /// + readonly Dictionary downloadTickets; + + /// + /// that is triggered when is called. + /// + readonly CancellationTokenSource disposeCts; + + /// + /// used to update . + /// + readonly object synchronizationLock; + + /// + /// Combined of all calls. + /// + Task expireTask; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public FileTransferService( + ICryptographySuite cryptographySuite, + IIOManager ioManager, + IAsyncDelayer asyncDelayer, + ILogger logger) + { + this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + uploadTickets = new Dictionary(); + downloadTickets = new Dictionary(); + + disposeCts = new CancellationTokenSource(); + + expireTask = Task.CompletedTask; + synchronizationLock = new object(); + } + + /// + public async ValueTask DisposeAsync() + { + Task toAwait; + lock (synchronizationLock) + if (expireTask != null) + { + disposeCts.Cancel(); + disposeCts.Dispose(); + toAwait = expireTask; + expireTask = null; + } + else + toAwait = Task.CompletedTask; + + await toAwait.ConfigureAwait(false); + } + + /// + /// Creates a new . + /// + /// A new . + FileTicketResult CreateTicket() => new FileTicketResult + { + FileTicket = cryptographySuite.GetSecureString() + }; + + void QueueExpiry(Action expireAction) + { + Task oldExpireTask = null; + + async Task ExpireAsync() + { + var expireAt = DateTimeOffset.Now + TimeSpan.FromMinutes(TicketValidityMinutes); + try + { + await oldExpireTask.WithToken(disposeCts.Token).ConfigureAwait(false); + + var now = DateTimeOffset.Now; + if (now < expireAt) + await asyncDelayer.Delay(expireAt - now, disposeCts.Token).ConfigureAwait(false); + } + finally + { + expireAction(); + } + } + + lock (synchronizationLock) + { + oldExpireTask = expireTask; + expireTask = ExpireAsync(); + } + } + + /// + public FileTicketResult CreateDownload(FileDownloadProvider downloadProvider) + { + if (downloadProvider == null) + throw new ArgumentNullException(nameof(downloadProvider)); + + logger.LogDebug("Creating download ticket for path {0}", downloadProvider.FilePath); + var ticketResult = CreateTicket(); + + lock (downloadTickets) + downloadTickets.Add(ticketResult.FileTicket, downloadProvider); + + QueueExpiry(() => + { + logger.LogTrace("Expiring download ticket {0}...", ticketResult.FileTicket); + lock (downloadTickets) + downloadTickets.Remove(ticketResult.FileTicket); + }); + + logger.LogTrace("Created download ticket {0}", ticketResult.FileTicket); + + return ticketResult; + } + + /// + public IFileUploadTicket CreateUpload() + { + logger.LogDebug("Creating upload ticket..."); + var uploadTicket = new FileUploadProvider(CreateTicket()); + + lock (uploadTickets) + uploadTickets.Add(uploadTicket.Ticket.FileTicket, uploadTicket); + + QueueExpiry(() => + { + logger.LogTrace("Expiring upload ticket {0}...", uploadTicket.Ticket.FileTicket); + lock (uploadTickets) + uploadTickets.Remove(uploadTicket.Ticket.FileTicket); + + uploadTicket.Expire(); + }); + + logger.LogTrace("Created upload ticket {0}", uploadTicket.Ticket.FileTicket); + + return uploadTicket; + } + + /// + public async Task> RetrieveDownloadStream(FileTicketResult ticket, CancellationToken cancellationToken) + { + if (ticket == null) + throw new ArgumentNullException(nameof(ticket)); + + FileDownloadProvider downloadProvider; + lock (downloadTickets) + { + if (!downloadTickets.TryGetValue(ticket.FileTicket, out downloadProvider)) + { + logger.LogTrace("Download ticket {0} not found!", ticket.FileTicket); + return Tuple.Create(null, null); + } + + downloadTickets.Remove(ticket.FileTicket); + } + + var errorCode = await downloadProvider.ActivationCallback(cancellationToken).ConfigureAwait(false); + if (errorCode.HasValue) + { + logger.LogDebug("Download ticket {0} failed activation!", ticket.FileTicket); + return Tuple.Create(null, new ErrorMessage(errorCode.Value)); + } + + Stream stream; + try + { + stream = ioManager.GetFileStream(downloadProvider.FilePath, downloadProvider.ShareWrite); + } + catch (IOException ex) + { + return Tuple.Create( + null, + new ErrorMessage(ErrorCode.IOError) + { + AdditionalData = ex.ToString() + }); + } + + try + { + logger.LogTrace("Ticket {0} downloading...", ticket.FileTicket); + return Tuple.Create(stream, null); + } + catch + { + stream.Dispose(); + throw; + } + } + + /// + public async Task SetUploadStream(FileTicketResult ticket, Stream stream, CancellationToken cancellationToken) + { + if (ticket == null) + throw new ArgumentNullException(nameof(ticket)); + + FileUploadProvider uploadProvider; + lock (uploadTickets) + { + if (!uploadTickets.TryGetValue(ticket.FileTicket, out uploadProvider)) + { + logger.LogTrace("Upload ticket {0} not found!", ticket.FileTicket); + return new ErrorMessage(ErrorCode.ResourceNotPresent); + } + + uploadTickets.Remove(ticket.FileTicket); + } + + return await uploadProvider.Completion(stream, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs new file mode 100644 index 0000000000..21c3f3f142 --- /dev/null +++ b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Extensions; + +namespace Tgstation.Server.Host.Transfer +{ + /// + sealed class FileUploadProvider : IFileUploadTicket + { + /// + public FileTicketResult Ticket { get; } + + /// + /// The for the ticket duration. + /// + readonly CancellationTokenSource ticketExpiryCts; + + /// + /// The for the . + /// + readonly TaskCompletionSource taskCompletionSource; + + /// + /// The that completes in or when is called. + /// + readonly TaskCompletionSource completionTcs; + + /// + /// The that occurred while processing the upload if any. + /// + ErrorMessage errorMessage; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public FileUploadProvider(FileTicketResult ticket) + { + Ticket = ticket ?? throw new ArgumentNullException(nameof(ticket)); + + ticketExpiryCts = new CancellationTokenSource(); + taskCompletionSource = new TaskCompletionSource(); + completionTcs = new TaskCompletionSource(); + } + + /// + public void Dispose() + { + ticketExpiryCts.Dispose(); + completionTcs.TrySetResult(null); + } + + /// + public async Task GetResult(CancellationToken cancellationToken) + { + using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled())) + using (ticketExpiryCts.Token.Register(() => taskCompletionSource.TrySetResult(null))) + return await taskCompletionSource.Task.ConfigureAwait(false); + } + + /// + /// Expire the . + /// + public void Expire() + { + if (!completionTcs.Task.IsCompleted) + ticketExpiryCts.Cancel(); + } + + /// + /// Resolve the for the and awaits the upload. + /// + /// The containing uploaded data. + /// The for the operation. + /// A resulting in , otherwise. + public async Task Completion(Stream stream, CancellationToken cancellationToken) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (ticketExpiryCts.IsCancellationRequested) + return new ErrorMessage(ErrorCode.ResourceNotPresent); + + taskCompletionSource.TrySetResult(stream); + + await completionTcs.Task.WithToken(cancellationToken).ConfigureAwait(false); + return errorMessage; + } + + /// + public void SetErrorMessage(ErrorMessage errorMessage) + { + if (errorMessage == null) +#pragma warning disable IDE0016 // Use 'throw' expression + throw new ArgumentNullException(nameof(errorMessage)); +#pragma warning restore IDE0016 // Use 'throw' expression + + if (this.errorMessage != null) + throw new InvalidOperationException("ErrorMessage already set!"); + + this.errorMessage = errorMessage; + completionTcs.TrySetResult(null); + } + } +} diff --git a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs new file mode 100644 index 0000000000..fdf16b455f --- /dev/null +++ b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Transfer +{ + /// + /// Reads and writes to s associated with s. + /// + public interface IFileTransferStreamHandler + { + /// + /// Sets the for a given associated with a pending upload. + /// + /// The . + /// The with uploaded data. + /// The for the operation. + /// if the upload completed successfully, otherwise. + Task SetUploadStream(FileTicketResult ticket, Stream stream, CancellationToken cancellationToken); + + /// + /// Gets the the for a given associated with a pending download. + /// + /// The . + /// The for the operation. + /// A containing either a containing the data to download or an to return. + Task> RetrieveDownloadStream(FileTicketResult ticket, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs b/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs new file mode 100644 index 0000000000..7e697e841c --- /dev/null +++ b/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs @@ -0,0 +1,23 @@ +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Transfer +{ + /// + /// Service for temporarily storing files to be downloaded or uploaded. + /// + public interface IFileTransferTicketProvider + { + /// + /// Create a for a download. + /// + /// The . + /// A new for a download. + FileTicketResult CreateDownload(FileDownloadProvider fileDownloadProvider); + + /// + /// Create a . + /// + /// A new . + IFileUploadTicket CreateUpload(); + } +} diff --git a/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs b/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs new file mode 100644 index 0000000000..4eb1ea622c --- /dev/null +++ b/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Transfer +{ + /// + /// A that waits for a pending upload. + /// + public interface IFileUploadTicket : IDisposable + { + /// + /// The . + /// + FileTicketResult Ticket { get; } + + /// + /// Gets the for the uploaded file. + /// + /// The for the operation. + /// A resulting in the uploaded of the file on success, if the ticket timed out. + /// The resulting is short lived and should be buffered if it needs use outside the lifetime of the . + Task GetResult(CancellationToken cancellationToken); + + /// + /// Sets an for the upload. Will be returned in upload request as a 409 error. + /// + /// The to set. + void SetErrorMessage(ErrorMessage errorMessage); + } +} diff --git a/tests/Tgstation.Server.Tests/AdministrationTest.cs b/tests/Tgstation.Server.Tests/AdministrationTest.cs index 578e7f3e91..cddd0b0aa4 100644 --- a/tests/Tgstation.Server.Tests/AdministrationTest.cs +++ b/tests/Tgstation.Server.Tests/AdministrationTest.cs @@ -33,12 +33,12 @@ namespace Tgstation.Server.Tests var logFile = logs.First(); Assert.IsNotNull(logFile); Assert.IsFalse(String.IsNullOrWhiteSpace(logFile.Name)); - Assert.IsNull(logFile.Content); + Assert.IsNull(logFile.FileTicket); - var downloaded = await client.GetLog(logFile, cancellationToken); - Assert.AreEqual(logFile.Name, downloaded.Name); - Assert.IsTrue(logFile.LastModified <= downloaded.LastModified); - Assert.IsNull(logFile.Content); + var downloadedTuple = await client.GetLog(logFile, cancellationToken); + Assert.AreEqual(logFile.Name, downloadedTuple.Item1.Name); + Assert.IsTrue(logFile.LastModified <= downloadedTuple.Item1.LastModified); + Assert.IsNull(logFile.FileTicket); await ApiAssert.ThrowsException(() => client.GetLog(new LogFile { diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs index 9266e01da6..11a24484e4 100644 --- a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs @@ -1,4 +1,4 @@ -using Castle.Core.Logging; +using Castle.Core.Logging; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -45,7 +45,7 @@ namespace Tgstation.Server.Tests.Instance { Version = new Version(5011, 1385) }; - var test = await byondClient.SetActiveVersion(newModel, cancellationToken).ConfigureAwait(false); + var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken).ConfigureAwait(false); Assert.IsNotNull(test.InstallJob); await WaitForJob(test.InstallJob, 60, true, ErrorCode.ByondDownloadFail, cancellationToken).ConfigureAwait(false); } @@ -56,7 +56,7 @@ namespace Tgstation.Server.Tests.Instance { Version = TestVersion }; - var test = await byondClient.SetActiveVersion(newModel, cancellationToken).ConfigureAwait(false); + var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken).ConfigureAwait(false); Assert.IsNotNull(test.InstallJob); Assert.IsNull(test.Version); await WaitForJob(test.InstallJob, 60, false, null, cancellationToken).ConfigureAwait(false); @@ -98,11 +98,18 @@ namespace Tgstation.Server.Tests.Instance Mock.Of>()); // get the bytes for stable - var test = await byondClient.SetActiveVersion(new Api.Models.Byond - { - Version = TestVersion, - Content = await byondInstaller.DownloadVersion(TestVersion, cancellationToken) - }, cancellationToken).ConfigureAwait(false); + using var stableBytesMs = new MemoryStream( + await byondInstaller.DownloadVersion(TestVersion, cancellationToken)); + + var test = await byondClient.SetActiveVersion( + new Api.Models.Byond + { + Version = TestVersion, + UploadCustomZip = true + }, + stableBytesMs, + cancellationToken) + .ConfigureAwait(false); Assert.IsNotNull(test.InstallJob); await WaitForJob(test.InstallJob, 60, false, null, cancellationToken).ConfigureAwait(false); @@ -114,17 +121,17 @@ namespace Tgstation.Server.Tests.Instance newSettings = await byondClient.SetActiveVersion(new Api.Models.Byond { Version = TestVersion - }, cancellationToken); + }, null, cancellationToken); Assert.IsNull(newSettings.InstallJob); await ApiAssert.ThrowsException(() => byondClient.SetActiveVersion(new Api.Models.Byond { Version = new Version(TestVersion.Major, TestVersion.Minor, 2) - }, cancellationToken), ErrorCode.ByondNonExistentCustomVersion); + }, null, cancellationToken), ErrorCode.ByondNonExistentCustomVersion); newSettings = await byondClient.SetActiveVersion(new Api.Models.Byond { Version = new Version(TestVersion.Major, TestVersion.Minor, 1) - }, cancellationToken); + }, null, cancellationToken); Assert.IsNull(newSettings.InstallJob); } } diff --git a/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs index b30178169f..26abb403c5 100644 --- a/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; using System.Text; @@ -25,7 +25,8 @@ namespace Tgstation.Server.Tests.Instance { var tmp = (file.Path?.StartsWith('/') ?? false) ? '.' + file.Path : file.Path; var path = Path.Combine(instance.Path, "Configuration", tmp); - return File.Exists(path); + var result = File.Exists(path); + return result; } async Task TestDeleteDirectory(CancellationToken cancellationToken) @@ -39,18 +40,21 @@ namespace Tgstation.Server.Tests.Instance await configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken).ConfigureAwait(false); //try to delete non-empty + using var uploadMs = new MemoryStream(Encoding.UTF8.GetBytes("Hello world!")); var file = await configurationClient.Write(new ConfigurationFile { - Content = Encoding.UTF8.GetBytes("Hello world!"), Path = TestDir.Path + "/test.txt" - }, cancellationToken).ConfigureAwait(false); + }, uploadMs, cancellationToken).ConfigureAwait(false); Assert.IsTrue(FileExists(file)); + var updatedFile = await configurationClient.Read(file, cancellationToken).ConfigureAwait(false); + Assert.AreEqual(file.LastReadHash, updatedFile.Item1.LastReadHash); + await ApiAssert.ThrowsException(() => configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken), ErrorCode.ConfigurationDirectoryNotEmpty).ConfigureAwait(false); - file.Content = null; - await configurationClient.Write(file, cancellationToken).ConfigureAwait(false); + file.FileTicket = null; + await configurationClient.Write(file, null, cancellationToken).ConfigureAwait(false); await configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken).ConfigureAwait(false); } diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 0c6e8915df..35dddd9d35 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -396,6 +396,7 @@ namespace Tgstation.Server.Tests.Instance { Version = versionToInstall }, + null, cancellationToken); var byondInstallJob = await byondInstallJobTask; From f78d1716d9da85ce4fdd3fc5acb4ce82601537b4 Mon Sep 17 00:00:00 2001 From: Kyle Spier-Swenson Date: Tue, 1 Dec 2020 23:47:51 -0800 Subject: [PATCH 013/154] Don't tell people to use the port configured during the setup wizard during a step that happens before the setup wizard is ran. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39da29f3a4..512c58e203 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ docker run \ --name="tgs" \ # Name for the container --cap-add=sys_nice \ # Recommended, allows tgs to schedule DreamDaemon as a higher priority process --init \ #Highly recommended, reaps potential zombie processes - -p : \ # Port bridge for accessing TGS + -p 5000:5000 \ # Port bridge for accessing TGS, you can change this if you need -p 0.0.0.0:: \ # Port bridge for accessing DreamDaemon -v /path/to/your/configfile/directory:/config_data \ # Recommended, create a volume mapping for server configuration -v /path/to/store/instances:/tgs4_instances \ # Recommended, create a volume mapping for server instances From 7f5de5118bf623928339da172ca770395d888420 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 2 Dec 2020 22:07:26 -0500 Subject: [PATCH 014/154] Fixes and tests for new transfer service --- src/Tgstation.Server.Client/ApiClient.cs | 16 +++- .../CachedResponseStream.cs | 96 +++++++++++++++++++ .../Components/ConfigurationClient.cs | 26 ++--- .../Components/Repository/LibGit2Commands.cs | 7 +- .../Components/StaticFiles/Configuration.cs | 46 ++++++--- .../Controllers/AdministrationController.cs | 3 +- .../Controllers/ByondController.cs | 2 +- .../Controllers/LimitedFileStreamResult.cs | 44 +++++++++ .../LimitedFileStreamResultExecutor.cs | 76 +++++++++++++++ .../Controllers/TransferController.cs | 10 +- src/Tgstation.Server.Host/Core/Application.cs | 3 + .../IO/DefaultIOManager.cs | 4 +- src/Tgstation.Server.Host/IO/IIOManager.cs | 5 +- .../IO/ISynchronousIOManager.cs | 7 +- .../IO/SynchronousIOManager.cs | 31 +++--- .../Transfer/FileDownloadProvider.cs | 20 +++- .../Transfer/FileTransferService.cs | 33 ++++--- .../Transfer/FileUploadProvider.cs | 28 +++++- .../Transfer/IFileTransferStreamHandler.cs | 2 +- .../Transfer/IFileTransferTicketProvider.cs | 3 +- .../Instance/ConfigurationTest.cs | 22 ++++- .../Instance/RepositoryTest.cs | 2 +- tests/Tgstation.Server.Tests/RootTest.cs | 72 +++++++++++++- 23 files changed, 457 insertions(+), 101 deletions(-) create mode 100644 src/Tgstation.Server.Client/CachedResponseStream.cs create mode 100644 src/Tgstation.Server.Host/Controllers/LimitedFileStreamResult.cs create mode 100644 src/Tgstation.Server.Host/Controllers/LimitedFileStreamResultExecutor.cs diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 558628d0e3..574006f475 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -216,16 +216,22 @@ namespace Tgstation.Server.Client response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); } - using (response) + try { await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false); + // just stream if (fileDownload && response.IsSuccessStatusCode) - { - // just stream - return (TResult)(object)await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - } + return (TResult)(object)await CachedResponseStream.Create(response).ConfigureAwait(false); + } + catch + { + response.Dispose(); + throw; + } + using (response) + { var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) diff --git a/src/Tgstation.Server.Client/CachedResponseStream.cs b/src/Tgstation.Server.Client/CachedResponseStream.cs new file mode 100644 index 0000000000..9ad446f75c --- /dev/null +++ b/src/Tgstation.Server.Client/CachedResponseStream.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Tgstation.Server.Client +{ + /// + /// Caches the from a for later use. + /// + sealed class CachedResponseStream : Stream + { + /// + /// The for the . + /// + readonly HttpResponseMessage response; + + /// + /// The reponse content . + /// + readonly Stream responseStream; + + /// + /// Asyncronously creates a new . + /// + /// The to build from. + /// A resulting in a new . + public static async Task Create(HttpResponseMessage response) + { + var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return new CachedResponseStream(response, stream); + } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + CachedResponseStream(HttpResponseMessage response, Stream responseStream) + { + this.response = response; + this.responseStream = responseStream; + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + responseStream.Dispose(); + response.Dispose(); + } + + /// + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync().ConfigureAwait(false); + await responseStream.DisposeAsync().ConfigureAwait(false); + response.Dispose(); + } + + /// + public override bool CanRead => responseStream.CanRead; + + /// + public override bool CanSeek => responseStream.CanSeek; + + /// + public override bool CanWrite => responseStream.CanWrite; + + /// + public override long Length => responseStream.Length; + + /// + public override long Position + { + get => responseStream.Position; + set => responseStream.Position = value; + } + + /// + public override void Flush() => responseStream.Flush(); + + /// + public override int Read(byte[] buffer, int offset, int count) => responseStream.Read(buffer, offset, count); + + /// + public override long Seek(long offset, SeekOrigin origin) => responseStream.Seek(offset, origin); + + /// + public override void SetLength(long value) => responseStream.SetLength(value); + + /// + public override void Write(byte[] buffer, int offset, int count) => responseStream.Write(buffer, offset, count); + } +} diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index f233d070c4..25f8a0060d 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -69,9 +66,12 @@ namespace Tgstation.Server.Client.Components /// public async Task Write(ConfigurationFile file, Stream uploadStream, CancellationToken cancellationToken) { + long initialStreamPosition = 0; MemoryStream? memoryStream = null; - if (uploadStream != null) + if (uploadStream?.CanSeek == false) memoryStream = new MemoryStream(); + else if (uploadStream != null) + initialStreamPosition = uploadStream.Position; using (memoryStream) { @@ -81,22 +81,14 @@ namespace Tgstation.Server.Client.Components instance.Id, cancellationToken); - if (uploadStream != null) - await uploadStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + if (memoryStream != null) + await uploadStream!.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); var configFile = await configFileTask.ConfigureAwait(false); - // minor improvement to "fix" a lost feature that used to be in API 7 - // since LastReadHash is no longer updated until the next GET request, we can use the same calculations here to generate it. - if (uploadStream != null) -#pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1. - using (var sha1 = new SHA1Managed()) -#pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1. - configFile.LastReadHash = String.Join(String.Empty, sha1.ComputeHash(memoryStream).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); - else - configFile.LastReadHash = null; - - await apiClient.Upload(configFile, memoryStream, cancellationToken).ConfigureAwait(false); + var streamUsed = memoryStream ?? uploadStream; + streamUsed?.Seek(initialStreamPosition, SeekOrigin.Begin); + await apiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false); return configFile; } diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2Commands.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2Commands.cs index 65c1f21049..58158078ab 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2Commands.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2Commands.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using System; using System.Collections.Generic; @@ -22,13 +22,10 @@ namespace Tgstation.Server.Host.Components.Repository if (libGit2Repo == null) throw new ArgumentNullException(nameof(libGit2Repo)); - if (!(libGit2Repo is LibGit2Sharp.Repository concreteRepo)) - throw new ArgumentException("libGit2Repo must be an instance of LibGit2Sharp.Repository!", nameof(libGit2Repo)); - if (remote == null) throw new ArgumentNullException(nameof(remote)); - Commands.Fetch(concreteRepo, remote.Name, refSpecs, fetchOptions, logMessage); + Commands.Fetch((LibGit2Sharp.Repository)libGit2Repo, remote.Name, refSpecs, fetchOptions, logMessage); } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 6260047ce3..427636e6b3 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -286,16 +286,32 @@ namespace Tgstation.Server.Host.Components.StaticFiles var disposeToken = disposeCts.Token; var fileTicket = fileTransferService.CreateDownload( new FileDownloadProvider( - cancellationToken => + () => { if (disposeToken.IsCancellationRequested) - return Task.FromResult(ErrorCode.InstanceOffline); + return ErrorCode.InstanceOffline; var newSha = GetFileSha(); if (newSha != originalSha) - return Task.FromResult(ErrorCode.ConfigurationFileUpdated); + return ErrorCode.ConfigurationFileUpdated; - return Task.FromResult(null); + return null; + }, + async cancellationToken => + { + FileStream result = null; + void GetFileStream() + { + result = ioManager.GetFileStream(path, false); + } + + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + if (systemIdentity == null) + await Task.Factory.StartNew(GetFileStream, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false); + else + await systemIdentity.RunImpersonated(GetFileStream, cancellationToken).ConfigureAwait(false); + + return result; }, path, false)); @@ -425,30 +441,32 @@ namespace Tgstation.Server.Host.Components.StaticFiles lock (semaphore) try { - var fileTicket = fileTransferService.CreateUpload(); + var fileTicket = fileTransferService.CreateUpload(true); var uploadCancellationToken = disposeCts.Token; async Task UploadHandler() { using (fileTicket) { - byte[] data; var fileHash = previousHash; - using (var ms = new MemoryStream()) + using var uploadStream = await fileTicket.GetResult(uploadCancellationToken).ConfigureAwait(false); + bool success = false; + void WriteCallback() { - using (var stream = await fileTicket.GetResult(uploadCancellationToken).ConfigureAwait(false)) - await stream.CopyToAsync(ms, uploadCancellationToken).ConfigureAwait(false); - data = ms.ToArray(); - if (data.Length == 0) - data = null; + success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, cancellationToken); } - var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken); + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + if (systemIdentity == null) + await Task.Factory.StartNew(WriteCallback, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false); + else + await systemIdentity.RunImpersonated(WriteCallback, cancellationToken).ConfigureAwait(false); + if (!success) fileTicket.SetErrorMessage(new ErrorMessage(ErrorCode.ConfigurationFileUpdated) { AdditionalData = fileHash }); - else if(data != null) + else if(uploadStream.Length > 0) postWriteHandler.HandleWrite(path); } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 28f13c3c97..e920efacb3 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -397,7 +397,8 @@ namespace Tgstation.Server.Host.Controllers { var fileTransferTicket = fileTransferService.CreateDownload( new FileDownloadProvider( - cancellationToken => Task.FromResult(null), + () => null, + null, fullPath, true)); diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 93dbd33cbc..8c503ba47b 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -167,7 +167,7 @@ namespace Tgstation.Server.Host.Controllers IFileUploadTicket fileUploadTicket = null; if (uploadingZip) - fileUploadTicket = fileTransferService.CreateUpload(); + fileUploadTicket = fileTransferService.CreateUpload(false); try { diff --git a/src/Tgstation.Server.Host/Controllers/LimitedFileStreamResult.cs b/src/Tgstation.Server.Host/Controllers/LimitedFileStreamResult.cs new file mode 100644 index 0000000000..eefafb2aa6 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/LimitedFileStreamResult.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.IO; +using System.Net.Mime; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// Very similar to except it's contains a fix for https://github.com/dotnet/aspnetcore/issues/28189. + /// + public sealed class LimitedFileStreamResult : FileResult + { + /// + /// The representing the file to download. + /// + public FileStream FileStream { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public LimitedFileStreamResult(FileStream stream) + : base(MediaTypeNames.Application.Octet) + { + FileStream = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var executor = context + .HttpContext + .RequestServices + .GetRequiredService>(); + return executor.ExecuteAsync(context, this); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/LimitedFileStreamResultExecutor.cs b/src/Tgstation.Server.Host/Controllers/LimitedFileStreamResultExecutor.cs new file mode 100644 index 0000000000..19df00ae1e --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/LimitedFileStreamResultExecutor.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for s. + /// + public class LimitedFileStreamResultExecutor : FileResultExecutorBase, IActionResultExecutor + { + /// + /// Initializes a new instance of the . + /// + /// The for the . + public LimitedFileStreamResultExecutor(ILogger logger) + : base(logger) + { + } + + /// + public async Task ExecuteAsync(ActionContext context, LimitedFileStreamResult result) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + if (result == null) + throw new ArgumentNullException(nameof(result)); + + using (result.FileStream) + { + var contentLength = result.FileStream.Length; + var (range, rangeLength, serveBody) = SetHeadersAndLog(context, result, contentLength, result.EnableRangeProcessing); + if (!serveBody) + return; + + try + { + var cancellationToken = context.HttpContext.RequestAborted; + var outputStream = context.HttpContext.Response.Body; + if (range == null) + { + await StreamCopyOperation.CopyToAsync( + result.FileStream, + outputStream, + contentLength, + BufferSize, + cancellationToken) + .ConfigureAwait(false); + } + else + { + result.FileStream.Seek(range.From.Value, SeekOrigin.Begin); + await StreamCopyOperation.CopyToAsync( + result.FileStream, + outputStream, + rangeLength, + BufferSize, + cancellationToken) + .ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // Don't throw this exception, it's most likely caused by the client disconnecting. + // However, if it was cancelled for any other reason we need to prevent empty responses. + context.HttpContext.Abort(); + } + } + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/TransferController.cs b/src/Tgstation.Server.Host/Controllers/TransferController.cs index 5dcf554842..e4cd2fbf39 100644 --- a/src/Tgstation.Server.Host/Controllers/TransferController.cs +++ b/src/Tgstation.Server.Host/Controllers/TransferController.cs @@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Controllers public async Task Download([FromQuery] string ticket, CancellationToken cancellationToken) { if (ticket == null) - throw new ArgumentNullException(nameof(ticket)); + return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); var streamAccept = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); if (!Request.GetTypedHeaders().Accept.Any(x => streamAccept.IsSubsetOf(x))) @@ -87,7 +87,7 @@ namespace Tgstation.Server.Host.Controllers if (stream == null) return Gone(); - return new FileStreamResult(stream, new MediaTypeHeaderValue(MediaTypeNames.Application.Octet)); + return new LimitedFileStreamResult(stream); } catch { @@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Controllers public async Task Upload([FromQuery] string ticket, CancellationToken cancellationToken) { if (ticket == null) - throw new ArgumentNullException(nameof(ticket)); + return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); var fileTicketResult = new FileTicketResult { @@ -118,7 +118,9 @@ namespace Tgstation.Server.Host.Controllers var result = await fileTransferService.SetUploadStream(fileTicketResult, Request.Body, cancellationToken).ConfigureAwait(false); if (result != null) - return Conflict(result); + return result.ErrorCode == ErrorCode.ResourceNotPresent + ? Gone() + : Conflict(result); return Created(new object()); } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 77cdd26224..cf80be7bfd 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -31,6 +32,7 @@ using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; @@ -306,6 +308,7 @@ namespace Tgstation.Server.Host.Core // configure misc services services.AddScoped(); + services.AddTransient, LimitedFileStreamResultExecutor>(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index 6c5a55ab69..b79fa1640a 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -145,6 +145,8 @@ namespace Tgstation.Server.Host.IO throw new ArgumentNullException(nameof(dest)); using var srcStream = new FileStream(ResolvePath(src), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true); using var destStream = new FileStream(ResolvePath(dest), FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true); + + // value taken from documentation await srcStream.CopyToAsync(destStream, 81920, cancellationToken).ConfigureAwait(false); } @@ -325,6 +327,6 @@ namespace Tgstation.Server.Host.IO }, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// - public Stream GetFileStream(string path, bool shareWrite) => new FileStream(ResolvePath(path), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None), DefaultBufferSize, true); + public FileStream GetFileStream(string path, bool shareWrite) => new FileStream(ResolvePath(path), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None), DefaultBufferSize, true); } } diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index 2f2c3caac3..90c13b8cdb 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -209,7 +209,8 @@ namespace Tgstation.Server.Host.IO /// /// The path of the file. /// If should be used. - /// The of the file. - Stream GetFileStream(string path, bool shareWrite); + /// The of the file. + /// This function is sychronous. + FileStream GetFileStream(string path, bool shareWrite); } } diff --git a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs index 69f80050b5..20e6656aec 100644 --- a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IO; using System.Threading; namespace Tgstation.Server.Host.IO @@ -50,11 +51,11 @@ namespace Tgstation.Server.Host.IO /// Write to a file at a given /// /// The path to the file to write - /// The new contents of the file + /// A containing the new contents of the file /// The function only succeeds if this parameter matches the SHA-1 hash of the contents of the current file. Contains the SHA1 of the file on disk once the function returns /// The for the operation /// on success, if the operation failed due to not matching the file's contents - bool WriteFileChecked(string path, byte[] data, ref string sha1InOut, CancellationToken cancellationToken); + bool WriteFileChecked(string path, Stream data, ref string sha1InOut, CancellationToken cancellationToken); /// /// Checks if a given is a directory diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index 2a6530faa0..c3c66bf92e 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -74,30 +74,24 @@ namespace Tgstation.Server.Host.IO } /// - public bool WriteFileChecked(string path, byte[] data, ref string sha1InOut, CancellationToken cancellationToken) + public bool WriteFileChecked(string path, Stream data, ref string sha1InOut, CancellationToken cancellationToken) { if (path == null) throw new ArgumentNullException(nameof(path)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + cancellationToken.ThrowIfCancellationRequested(); var directory = Path.GetDirectoryName(path); + Directory.CreateDirectory(directory); cancellationToken.ThrowIfCancellationRequested(); using (var file = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { cancellationToken.ThrowIfCancellationRequested(); - // as nice as it would be to not have to arrayify the memory stream, we have to - // because, oddly enough sha1(memorystream) != sha1(memorystream.ToArray()) - // vOv - byte[] originalBytes; - using (var readMs = new MemoryStream()) - { - file.CopyTo(readMs); - originalBytes = readMs.ToArray(); - } - // no sha1? no write - if (originalBytes.Length != 0 && sha1InOut == null) + if (file.Length != 0 && sha1InOut == null) return false; // suppressed due to only using for consistency checks @@ -105,8 +99,8 @@ namespace Tgstation.Server.Host.IO using (var sha1 = new SHA1Managed()) #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1. { - string GetSha1(byte[] dataToHash) => dataToHash != null && dataToHash.Length != 0 ? String.Join(String.Empty, sha1.ComputeHash(dataToHash).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))) : null; - var originalSha1 = GetSha1(originalBytes); + string GetSha1(Stream dataToHash) => dataToHash != null && dataToHash.Length != 0 ? String.Join(String.Empty, sha1.ComputeHash(dataToHash).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))) : null; + var originalSha1 = GetSha1(file); if (originalSha1 != sha1InOut) { sha1InOut = originalSha1; @@ -118,17 +112,18 @@ namespace Tgstation.Server.Host.IO cancellationToken.ThrowIfCancellationRequested(); - if (data != null) + if (data.Length != 0) { file.Seek(0, SeekOrigin.Begin); + data.Seek(0, SeekOrigin.Begin); cancellationToken.ThrowIfCancellationRequested(); file.SetLength(data.Length); - file.Write(data, 0, data.Length); + data.CopyTo(file); } } - if (data == null) + if (data.Length == 0) File.Delete(path); return true; } diff --git a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs index a01bdee992..16f78aad1a 100644 --- a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -11,9 +12,14 @@ namespace Tgstation.Server.Host.Transfer public sealed class FileDownloadProvider { /// - /// A of a to run before providing the download. If it returns a non-null , a 400 error with that code will be returned instead of a download stream. + /// A to run before providing the download. If it returns a non-null , a 400 error with that code will be returned instead of a download stream. /// - public Func> ActivationCallback { get; } + public Func ActivationCallback { get; } + + /// + /// A to specially provide a returning the . + /// + public Func> FileStreamProvider { get; } /// /// The full path to the file on disk to download. @@ -21,7 +27,7 @@ namespace Tgstation.Server.Host.Transfer public string FilePath { get; } /// - /// If the file read stream should be allowed to share writes. + /// If the file read stream should be allowed to share writes. If this is set, the entire file will be buffered to avoid Content-Length mismatches. /// public bool ShareWrite { get; } @@ -29,11 +35,17 @@ namespace Tgstation.Server.Host.Transfer /// Initializes a new instance of the . /// /// The value of . + /// The optional value of . /// The value of . /// The value of . - public FileDownloadProvider(Func> activationCallback, string filePath, bool shareWrite) + public FileDownloadProvider( + Func activationCallback, + Func> fileStreamProvider, + string filePath, + bool shareWrite) { ActivationCallback = activationCallback ?? throw new ArgumentNullException(nameof(activationCallback)); + FileStreamProvider = fileStreamProvider; FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); ShareWrite = shareWrite; } diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index 9b520f9690..812e2c11f7 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -163,9 +163,9 @@ namespace Tgstation.Server.Host.Transfer QueueExpiry(() => { - logger.LogTrace("Expiring download ticket {0}...", ticketResult.FileTicket); lock (downloadTickets) - downloadTickets.Remove(ticketResult.FileTicket); + if(downloadTickets.Remove(ticketResult.FileTicket)) + logger.LogTrace("Expired download ticket {0}...", ticketResult.FileTicket); }); logger.LogTrace("Created download ticket {0}", ticketResult.FileTicket); @@ -174,19 +174,21 @@ namespace Tgstation.Server.Host.Transfer } /// - public IFileUploadTicket CreateUpload() + public IFileUploadTicket CreateUpload(bool requireSynchronousIO) { logger.LogDebug("Creating upload ticket..."); - var uploadTicket = new FileUploadProvider(CreateTicket()); + var uploadTicket = new FileUploadProvider(CreateTicket(), requireSynchronousIO); lock (uploadTickets) uploadTickets.Add(uploadTicket.Ticket.FileTicket, uploadTicket); QueueExpiry(() => { - logger.LogTrace("Expiring upload ticket {0}...", uploadTicket.Ticket.FileTicket); lock (uploadTickets) - uploadTickets.Remove(uploadTicket.Ticket.FileTicket); + if (uploadTickets.Remove(uploadTicket.Ticket.FileTicket)) + logger.LogTrace("Expired upload ticket {0}...", uploadTicket.Ticket.FileTicket); + else + return; uploadTicket.Expire(); }); @@ -197,7 +199,7 @@ namespace Tgstation.Server.Host.Transfer } /// - public async Task> RetrieveDownloadStream(FileTicketResult ticket, CancellationToken cancellationToken) + public async Task> RetrieveDownloadStream(FileTicketResult ticket, CancellationToken cancellationToken) { if (ticket == null) throw new ArgumentNullException(nameof(ticket)); @@ -208,27 +210,30 @@ namespace Tgstation.Server.Host.Transfer if (!downloadTickets.TryGetValue(ticket.FileTicket, out downloadProvider)) { logger.LogTrace("Download ticket {0} not found!", ticket.FileTicket); - return Tuple.Create(null, null); + return Tuple.Create(null, null); } downloadTickets.Remove(ticket.FileTicket); } - var errorCode = await downloadProvider.ActivationCallback(cancellationToken).ConfigureAwait(false); + var errorCode = downloadProvider.ActivationCallback(); if (errorCode.HasValue) { logger.LogDebug("Download ticket {0} failed activation!", ticket.FileTicket); - return Tuple.Create(null, new ErrorMessage(errorCode.Value)); + return Tuple.Create(null, new ErrorMessage(errorCode.Value)); } - Stream stream; + FileStream stream; try { - stream = ioManager.GetFileStream(downloadProvider.FilePath, downloadProvider.ShareWrite); + if (downloadProvider.FileStreamProvider != null) + stream = await downloadProvider.FileStreamProvider(cancellationToken).ConfigureAwait(false); + else + stream = ioManager.GetFileStream(downloadProvider.FilePath, downloadProvider.ShareWrite); } catch (IOException ex) { - return Tuple.Create( + return Tuple.Create( null, new ErrorMessage(ErrorCode.IOError) { @@ -239,7 +244,7 @@ namespace Tgstation.Server.Host.Transfer try { logger.LogTrace("Ticket {0} downloading...", ticket.FileTicket); - return Tuple.Create(stream, null); + return Tuple.Create(stream, null); } catch { diff --git a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs index 21c3f3f142..ea5e47a245 100644 --- a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs @@ -1,9 +1,11 @@ +using Microsoft.AspNetCore.WebUtilities; using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Transfer { @@ -28,6 +30,11 @@ namespace Tgstation.Server.Host.Transfer /// readonly TaskCompletionSource completionTcs; + /// + /// If synchronous IO is required. Uses a as a backend if set. + /// + readonly bool requireSynchronousIO; + /// /// The that occurred while processing the upload if any. /// @@ -37,13 +44,15 @@ namespace Tgstation.Server.Host.Transfer /// Initializes a new instance of the . /// /// The value of . - public FileUploadProvider(FileTicketResult ticket) + /// The value of + public FileUploadProvider(FileTicketResult ticket, bool requireSynchronousIO) { Ticket = ticket ?? throw new ArgumentNullException(nameof(ticket)); ticketExpiryCts = new CancellationTokenSource(); taskCompletionSource = new TaskCompletionSource(); completionTcs = new TaskCompletionSource(); + this.requireSynchronousIO = requireSynchronousIO; } /// @@ -84,10 +93,21 @@ namespace Tgstation.Server.Host.Transfer if (ticketExpiryCts.IsCancellationRequested) return new ErrorMessage(ErrorCode.ResourceNotPresent); - taskCompletionSource.TrySetResult(stream); + Stream bufferedStream = null; + if (requireSynchronousIO) + { + // big reads, we should buffer to disk + bufferedStream = new FileBufferingReadStream(stream, DefaultIOManager.DefaultBufferSize); + await bufferedStream.DrainAsync(cancellationToken).ConfigureAwait(false); + } - await completionTcs.Task.WithToken(cancellationToken).ConfigureAwait(false); - return errorMessage; + using (bufferedStream) + { + taskCompletionSource.TrySetResult(bufferedStream ?? stream); + + await completionTcs.Task.WithToken(cancellationToken).ConfigureAwait(false); + return errorMessage; + } } /// diff --git a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs index fdf16b455f..fd8a051332 100644 --- a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs +++ b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs @@ -26,6 +26,6 @@ namespace Tgstation.Server.Host.Transfer /// The . /// The for the operation. /// A containing either a containing the data to download or an to return. - Task> RetrieveDownloadStream(FileTicketResult ticket, CancellationToken cancellationToken); + Task> RetrieveDownloadStream(FileTicketResult ticket, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs b/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs index 7e697e841c..5edfb86cd7 100644 --- a/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/IFileTransferTicketProvider.cs @@ -17,7 +17,8 @@ namespace Tgstation.Server.Host.Transfer /// /// Create a . /// + /// If synchronous IO is required on the provided stream. /// A new . - IFileUploadTicket CreateUpload(); + IFileUploadTicket CreateUpload(bool requiresSynchronousIO); } } diff --git a/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs index 26abb403c5..a4aaaae401 100644 --- a/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs @@ -40,23 +40,37 @@ namespace Tgstation.Server.Tests.Instance await configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken).ConfigureAwait(false); //try to delete non-empty - using var uploadMs = new MemoryStream(Encoding.UTF8.GetBytes("Hello world!")); + const string TestString = "Hello world!"; + using var uploadMs = new MemoryStream(Encoding.UTF8.GetBytes(TestString)); var file = await configurationClient.Write(new ConfigurationFile { Path = TestDir.Path + "/test.txt" }, uploadMs, cancellationToken).ConfigureAwait(false); Assert.IsTrue(FileExists(file)); + Assert.IsNull(file.LastReadHash); - var updatedFile = await configurationClient.Read(file, cancellationToken).ConfigureAwait(false); - Assert.AreEqual(file.LastReadHash, updatedFile.Item1.LastReadHash); + var updatedFileTuple = await configurationClient.Read(file, cancellationToken).ConfigureAwait(false); + var updatedFile = updatedFileTuple.Item1; + Assert.IsNotNull(updatedFile.LastReadHash); + using (var downloadMemoryStream = new MemoryStream()) + { + using (var downloadStream = updatedFileTuple.Item2) + await downloadStream.CopyToAsync(downloadMemoryStream); + Assert.AreEqual(TestString, Encoding.UTF8.GetString(downloadMemoryStream.ToArray()).Trim()); + } await ApiAssert.ThrowsException(() => configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken), ErrorCode.ConfigurationDirectoryNotEmpty).ConfigureAwait(false); file.FileTicket = null; - await configurationClient.Write(file, null, cancellationToken).ConfigureAwait(false); + await configurationClient.Write(updatedFile, null, cancellationToken).ConfigureAwait(false); + Assert.IsFalse(FileExists(file)); await configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken).ConfigureAwait(false); + + var tmp = (TestDir.Path?.StartsWith('/') ?? false) ? '.' + TestDir.Path : TestDir.Path; + var path = Path.Combine(instance.Path, "Configuration", tmp); + Assert.IsFalse(Directory.Exists(path)); } public async Task Run(CancellationToken cancellationToken) diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index efd3e9a21e..f9cc75d330 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -79,7 +79,7 @@ namespace Tgstation.Server.Tests.Instance clone = await repositoryClient.Clone(initalRepo, cancellationToken).ConfigureAwait(false); - await WaitForJob(clone.ActiveJob, 180, false, null, cancellationToken).ConfigureAwait(false); + await WaitForJob(clone.ActiveJob, 600, false, null, cancellationToken).ConfigureAwait(false); var cloned = await repositoryClient.Read(cancellationToken); Assert.AreEqual(Origin, cloned.Origin); diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs index be2b87cb95..ae08bbc1c5 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RootTest.cs @@ -194,11 +194,81 @@ namespace Tgstation.Server.Tests Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); } } + async Task TestInvalidTransfers(IServerClient serverClient, CancellationToken cancellationToken) + { + var url = serverClient.Url; + var token = serverClient.Token.Bearer; + // check that 400s are returned appropriately + using var httpClient = new HttpClient(); + + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.Transfer.Substring(1))) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(ErrorCode.ModelValidationFailure, message.ErrorCode); + } + + using (var request = new HttpRequestMessage(HttpMethod.Put, url.ToString() + Routes.Transfer.Substring(1))) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(ErrorCode.ModelValidationFailure, message.ErrorCode); + } + + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.Transfer.Substring(1) + "?ticket=veryfaketicket")) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode); + } + + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.Transfer.Substring(1) + "?ticket=veryfaketicket")) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Octet)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.Gone, response.StatusCode); + } + + using (var request = new HttpRequestMessage(HttpMethod.Put, url.ToString() + Routes.Transfer.Substring(1) + "?ticket=veryfaketicket")) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.Gone, response.StatusCode); + } + } public Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken) => Task.WhenAll( TestRequestValidation(serverClient, cancellationToken), TestOAuthFails(serverClient, cancellationToken), - TestServerInformation(clientFactory, serverClient, cancellationToken)); + TestServerInformation(clientFactory, serverClient, cancellationToken), + TestInvalidTransfers(serverClient, cancellationToken)); } } From d811011c5d412a82a176e5569634cd61f2940e18 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 2 Dec 2020 23:40:55 -0500 Subject: [PATCH 015/154] Swagger fixups --- build/OpenApiValidationSettings.json | 14 ++- build/Version.props | 8 +- .../Controllers/TransferController.cs | 15 ++-- .../Core/SwaggerConfiguration.cs | 88 +++++++++++++++++-- 4 files changed, 105 insertions(+), 20 deletions(-) diff --git a/build/OpenApiValidationSettings.json b/build/OpenApiValidationSettings.json index aa6faf8c1d..2c14eb6066 100644 --- a/build/OpenApiValidationSettings.json +++ b/build/OpenApiValidationSettings.json @@ -6,6 +6,7 @@ "no_summary": "error", "no_array_responses": "off", "parameter_order": "error", + "undefined_tag": "off", "unused_tag": "error", "operation_id_naming_convention": "off" }, @@ -44,12 +45,15 @@ "no_property_description": "off", "description_mentions_json": "error", "array_of_arrays": "error", + "inconsistent_property_type": "error", "property_case_convention": "off", - "enum_case_convention": "error" + "property_case_collision": "error", + "enum_case_convention": "error", + "undefined_required_properties": "error" }, "walker": { "no_empty_descriptions": "error", - "has_circular_references": "off", + "has_circular_references": "error", "$ref_siblings": "error", "duplicate_sibling_description": "error", "incorrect_ref_pattern": "error" @@ -76,7 +80,11 @@ "responses": { "no_response_codes": "error", "no_success_response_codes": "error", + "no_response_body": "error", "ibm_status_code_guidelines": "off" + }, + "schemas": { + "json_or_param_binary_string": "error" } } -} \ No newline at end of file +} diff --git a/build/Version.props b/build/Version.props index 3b64b3ff67..5305d27cc4 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,10 +3,10 @@ - 4.6.0 - 2.1.1 - 7.4.0 - 8.4.0 + 4.7.0 + 2.2.0 + 8.0.0 + 9.0.0 5.2.9 1.1.0 1.2.0 diff --git a/src/Tgstation.Server.Host/Controllers/TransferController.cs b/src/Tgstation.Server.Host/Controllers/TransferController.cs index e4cd2fbf39..26521a7159 100644 --- a/src/Tgstation.Server.Host/Controllers/TransferController.cs +++ b/src/Tgstation.Server.Host/Controllers/TransferController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using System; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.Net.Mime; @@ -59,8 +60,9 @@ namespace Tgstation.Server.Host.Controllers /// The was no longer or was never valid. [TgsAuthorize] [HttpGet] - [Produces(MediaTypeNames.Application.Octet, MediaTypeNames.Application.Json)] - public async Task Download([FromQuery] string ticket, CancellationToken cancellationToken) + [ProducesResponseType(200, Type = typeof(LimitedFileStreamResult))] + [ProducesResponseType(410, Type = typeof(ErrorMessage))] + public async Task Download([Required, FromQuery] string ticket, CancellationToken cancellationToken) { if (ticket == null) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); @@ -102,11 +104,14 @@ namespace Tgstation.Server.Host.Controllers /// The for the upload. /// The for the operation. /// A resulting in the of the method. - /// Uploaded file successfully. + /// Uploaded file successfully. + /// An error occurred during the upload. /// The was no longer or was never valid. [TgsAuthorize] [HttpPut] - public async Task Upload([FromQuery] string ticket, CancellationToken cancellationToken) + [ProducesResponseType(204)] + [ProducesResponseType(410, Type = typeof(ErrorMessage))] + public async Task Upload([Required, FromQuery] string ticket, CancellationToken cancellationToken) { if (ticket == null) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); @@ -122,7 +127,7 @@ namespace Tgstation.Server.Host.Controllers ? Gone() : Conflict(result); - return Created(new object()); + return NoContent(); } } } diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index 2d5a07a408..67d2dccc61 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -25,6 +25,11 @@ namespace Tgstation.Server.Host.Core /// const string PasswordSecuritySchemeId = "Password_Login_Scheme"; + /// + /// The name for OAuth 2.0 authentication. + /// + const string OAuthSecuritySchemeId = "OAuth_Login_Scheme"; + /// /// The name for token authentication. /// @@ -170,6 +175,14 @@ namespace Tgstation.Server.Host.Core Scheme = ApiHeaders.BasicAuthenticationScheme }); + swaggerGenOptions.AddSecurityDefinition(OAuthSecuritySchemeId, new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Name = HeaderNames.Authorization, + Scheme = ApiHeaders.OAuthAuthenticationScheme + }); + swaggerGenOptions.AddSecurityDefinition(TokenSecuritySchemeId, new OpenApiSecurityScheme { BearerFormat = "JWT", @@ -236,10 +249,35 @@ namespace Tgstation.Server.Host.Core Id = ApiHeaders.InstanceIdHeader } }); + else if (typeof(TransferController).IsAssignableFrom(context.MethodInfo.DeclaringType)) + if (context.MethodInfo.Name == nameof(TransferController.Upload)) + operation.RequestBody = new OpenApiRequestBody + { + Content = new Dictionary + { + { + MediaTypeNames.Application.Octet, + new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + } + } + } + } + }; + else if (context.MethodInfo.Name == nameof(TransferController.Download)) + { + var twoHundredResponseContents = operation.Responses["200"].Content; + var fileContent = twoHundredResponseContents[MediaTypeNames.Application.Json]; + twoHundredResponseContents.Remove(MediaTypeNames.Application.Json); + twoHundredResponseContents.Add(MediaTypeNames.Application.Octet, fileContent); + } } - else + else if (context.MethodInfo.Name == nameof(HomeController.CreateToken)) { - // HomeController.CreateToken var passwordScheme = new OpenApiSecurityScheme { Reference = new OpenApiReference @@ -249,6 +287,28 @@ namespace Tgstation.Server.Host.Core } }; + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = OAuthSecuritySchemeId + } + }; + + operation.Parameters.Add(new OpenApiParameter + { + In = ParameterLocation.Header, + Name = ApiHeaders.OAuthProviderHeader, + Description = "The external OAuth service provider.", + Style = ParameterStyle.Simple, + Example = new OpenApiString("Discord"), + Schema = new OpenApiSchema + { + Type = "string" + } + }); + operation.Security = new List { new OpenApiSecurityRequirement @@ -256,6 +316,10 @@ namespace Tgstation.Server.Host.Core { passwordScheme, new List() + }, + { + oAuthScheme, + new List() } } }; @@ -311,17 +375,24 @@ namespace Tgstation.Server.Host.Core Schema = productHeaderSchema }); - string bridgeOperationPath = null; + var pathsToRemove = new List(); + var filteredControllers = new string[] + { + nameof(BridgeController), + nameof(ControlPanelController), + }; + foreach (var path in swaggerDoc.Paths) foreach (var operation in path.Value.Operations.Select(x => x.Value)) { - if (operation.OperationId.Equals($"{nameof(BridgeController)}.{nameof(BridgeController.Process)}", StringComparison.Ordinal)) + if (filteredControllers.Any( + x => operation.OperationId.StartsWith(x, StringComparison.Ordinal))) { - bridgeOperationPath = path.Key; + pathsToRemove.Add(path.Key); continue; } - operation.Parameters.Add(new OpenApiParameter + operation.Parameters.Insert(0, new OpenApiParameter { Reference = new OpenApiReference { @@ -330,7 +401,7 @@ namespace Tgstation.Server.Host.Core }, }); - operation.Parameters.Add(new OpenApiParameter + operation.Parameters.Insert(1, new OpenApiParameter { Reference = new OpenApiReference { @@ -340,7 +411,8 @@ namespace Tgstation.Server.Host.Core }); } - swaggerDoc.Paths.Remove(bridgeOperationPath); + foreach (var filteredPath in pathsToRemove) + swaggerDoc.Paths.Remove(filteredPath); AddDefaultResponses(swaggerDoc); } From bb0f2bc068ac46a95781e864c344f40d076233e7 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 3 Dec 2020 00:02:33 -0500 Subject: [PATCH 016/154] Additional testing --- src/Tgstation.Server.Client/ApiClient.cs | 2 +- src/Tgstation.Server.Client/ApiClientFactory.cs | 2 +- .../{HttpClient.cs => HttpClientImplementation.cs} | 14 +++++++------- .../Properties/AssemblyInfo.cs | 3 +++ .../Instance/ConfigurationTest.cs | 9 +++++++++ tests/Tgstation.Server.Tests/RootTest.cs | 5 +++++ 6 files changed, 26 insertions(+), 9 deletions(-) rename src/Tgstation.Server.Client/{HttpClient.cs => HttpClientImplementation.cs} (68%) create mode 100644 src/Tgstation.Server.Client/Properties/AssemblyInfo.cs diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 574006f475..3252782576 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -39,7 +39,7 @@ namespace Tgstation.Server.Client } /// - /// The for the + /// The for the /// readonly IHttpClient httpClient; diff --git a/src/Tgstation.Server.Client/ApiClientFactory.cs b/src/Tgstation.Server.Client/ApiClientFactory.cs index d3f137baeb..02ff485b3a 100644 --- a/src/Tgstation.Server.Client/ApiClientFactory.cs +++ b/src/Tgstation.Server.Client/ApiClientFactory.cs @@ -7,6 +7,6 @@ namespace Tgstation.Server.Client sealed class ApiClientFactory : IApiClientFactory { /// - public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders) => new ApiClient(new HttpClient(), url, apiHeaders, tokenRefreshHeaders); + public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders) => new ApiClient(new HttpClientImplementation(), url, apiHeaders, tokenRefreshHeaders); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Client/HttpClient.cs b/src/Tgstation.Server.Client/HttpClientImplementation.cs similarity index 68% rename from src/Tgstation.Server.Client/HttpClient.cs rename to src/Tgstation.Server.Client/HttpClientImplementation.cs index c01042ae2d..5a449b1441 100644 --- a/src/Tgstation.Server.Client/HttpClient.cs +++ b/src/Tgstation.Server.Client/HttpClientImplementation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Tgstation.Server.Client { /// - sealed class HttpClient : IHttpClient + sealed class HttpClientImplementation : IHttpClient { /// public TimeSpan Timeout @@ -16,16 +16,16 @@ namespace Tgstation.Server.Client } /// - /// The real + /// The real /// - readonly System.Net.Http.HttpClient httpClient; + readonly HttpClient httpClient; /// - /// Construct an + /// Construct an /// - public HttpClient() + public HttpClientImplementation() { - httpClient = new System.Net.Http.HttpClient(); + httpClient = new HttpClient(); } /// diff --git a/src/Tgstation.Server.Client/Properties/AssemblyInfo.cs b/src/Tgstation.Server.Client/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7f876c9048 --- /dev/null +++ b/src/Tgstation.Server.Client/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tgstation.Server.Tests")] diff --git a/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs index a4aaaae401..b9e353a632 100644 --- a/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ConfigurationTest.cs @@ -1,6 +1,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; +using System.Net.Http; +using System.Net.Mime; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -56,7 +59,13 @@ namespace Tgstation.Server.Tests.Instance using (var downloadMemoryStream = new MemoryStream()) { using (var downloadStream = updatedFileTuple.Item2) + { + var requestStream = downloadStream as CachedResponseStream; + Assert.IsNotNull(requestStream); + var response = (HttpResponseMessage)requestStream.GetType().GetField("response", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(requestStream); + Assert.AreEqual(response.Content.Headers.ContentType.MediaType, MediaTypeNames.Application.Octet); await downloadStream.CopyToAsync(downloadMemoryStream); + } Assert.AreEqual(TestString, Encoding.UTF8.GetString(downloadMemoryStream.ToArray()).Trim()); } diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs index ae08bbc1c5..eccf9c0889 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RootTest.cs @@ -212,6 +212,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(MediaTypeNames.Application.Json, response.Content.Headers.ContentType.MediaType); Assert.AreEqual(ErrorCode.ModelValidationFailure, message.ErrorCode); } @@ -226,6 +227,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); var message = JsonConvert.DeserializeObject(content); + Assert.AreEqual(MediaTypeNames.Application.Json, response.Content.Headers.ContentType.MediaType); Assert.AreEqual(ErrorCode.ModelValidationFailure, message.ErrorCode); } @@ -237,6 +239,7 @@ namespace Tgstation.Server.Tests request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(MediaTypeNames.Application.Json, response.Content.Headers.ContentType.MediaType); Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode); } @@ -249,6 +252,7 @@ namespace Tgstation.Server.Tests request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(MediaTypeNames.Application.Json, response.Content.Headers.ContentType.MediaType); Assert.AreEqual(HttpStatusCode.Gone, response.StatusCode); } @@ -260,6 +264,7 @@ namespace Tgstation.Server.Tests request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(MediaTypeNames.Application.Json, response.Content.Headers.ContentType.MediaType); Assert.AreEqual(HttpStatusCode.Gone, response.StatusCode); } } From a9149482adeaa84caaffa810695cdb59fa8261e7 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 3 Dec 2020 11:55:43 -0500 Subject: [PATCH 017/154] Documentation about configuration methods --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 512c58e203..922fbab65d 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,16 @@ The first time you run TGS4 you should be prompted with a configuration wizard w This wizard will, generally, run whenever the server is launched without detecting the config json. Follow the instructions below to perform this process manually. +#### Configuration Methods + +There are 3 primary supported ways to configure TGS: + +- Modify the `appsettings.Production.json` file (Recommended). +- Set environment variables in the form `Section__Subsection=value` or `Section__ArraySubsection__0=value` for arrays. +- Set command line arguments in the form `--Section:Subsection=value` or `--Section:ArraySubsection:0=value` for arrays. + +The latter two are not recommended as they cannot be dynamically changed at runtime. See more on ASP.NET core configuration [here](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1). + #### Manual Configuration Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon's running). Note these are all case-sensitive: From 1e528114342fe60b4d59024f7e4529af2d615184 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 4 Dec 2020 23:49:20 -0500 Subject: [PATCH 018/154] Abstract test merging per git provider --- build/Version.props | 2 +- .../Models/Internal/IGitRemoteInformation.cs | 23 +++ .../Models/RemoteGitProvider.cs | 18 ++ src/Tgstation.Server.Api/Models/Repository.cs | 22 +-- .../Components/Deployment/DmbFactory.cs | 15 +- .../Components/Deployment/DreamMaker.cs | 123 +----------- .../IRemoteDeploymentManager.cs} | 39 +++- .../RemoteDeploymentManager.cs} | 187 ++++++++++++++++-- .../Components/Instance.cs | 94 +-------- .../Components/InstanceFactory.cs | 17 +- .../Repository/DefaultGitRemoteFeatures.cs | 22 +++ .../Repository/GitHubRemoteFeatures.cs | 36 ++++ .../Repository/IGitRemoteFeatures.cs | 15 ++ .../Repository/ILibGit2RepositoryFactory.cs | 8 +- .../Components/Repository/IRepository.cs | 18 +- .../Repository/LibGit2RepositoryFactory.cs | 45 ++++- .../Components/Repository/Repository.cs | 43 ++-- .../Repository/RepositoryManager.cs | 36 ++-- .../Components/Watchdog/BasicWatchdog.cs | 7 +- .../Components/Watchdog/IWatchdogFactory.cs | 5 +- .../Components/Watchdog/PosixWatchdog.cs | 5 +- .../Watchdog/PosixWatchdogFactory.cs | 3 +- .../Components/Watchdog/WatchdogBase.cs | 13 +- .../Components/Watchdog/WatchdogFactory.cs | 5 +- .../Components/Watchdog/WindowsWatchdog.cs | 5 +- .../Watchdog/WindowsWatchdogFactory.cs | 3 +- .../Controllers/AdministrationController.cs | 14 +- .../Controllers/RepositoryController.cs | 29 +-- .../Core/IGitHubClientFactory.cs | 8 +- .../Repository/TestRepositoryFactory.cs | 9 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 1 + 31 files changed, 505 insertions(+), 365 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/Internal/IGitRemoteInformation.cs create mode 100644 src/Tgstation.Server.Api/Models/RemoteGitProvider.cs rename src/Tgstation.Server.Host/Components/Deployment/{IGitHubDeploymentManager.cs => Remote/IRemoteDeploymentManager.cs} (57%) rename src/Tgstation.Server.Host/Components/Deployment/{GitHubDeploymentManager.cs => Remote/RemoteDeploymentManager.cs} (53%) create mode 100644 src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs create mode 100644 src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs create mode 100644 src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs diff --git a/build/Version.props b/build/Version.props index 220cd98461..bb2c35a17a 100644 --- a/build/Version.props +++ b/build/Version.props @@ -7,7 +7,7 @@ 2.1.1 7.4.0 8.4.0 - 5.2.8 + 5.2.9 1.1.0 1.1.1 diff --git a/src/Tgstation.Server.Api/Models/Internal/IGitRemoteInformation.cs b/src/Tgstation.Server.Api/Models/Internal/IGitRemoteInformation.cs new file mode 100644 index 0000000000..f687c9908e --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/IGitRemoteInformation.cs @@ -0,0 +1,23 @@ +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Provides information about remote providers. + /// + public interface IGitRemoteInformation + { + /// + /// The in use by the repository. + /// + public RemoteGitProvider? RemoteGitProvider { get; } + + /// + /// If is not this will be set with the owner of the repository + /// + public string? RemoteRepositoryOwner { get; } + + /// + /// If is not this will be set with the name of the repository + /// + public string? RemoteRepositoryName { get; } + } +} diff --git a/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs b/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs new file mode 100644 index 0000000000..61a3f73d0a --- /dev/null +++ b/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// Indicates the remote git host. + /// + public enum RemoteGitProvider + { + /// + /// Unknown remote git provider. + /// + Unknown, + + /// + /// Remote provider is GitHub.com + /// + GitHub, + } +} diff --git a/src/Tgstation.Server.Api/Models/Repository.cs b/src/Tgstation.Server.Api/Models/Repository.cs index 88b53ab95c..33a0f07d89 100644 --- a/src/Tgstation.Server.Api/Models/Repository.cs +++ b/src/Tgstation.Server.Api/Models/Repository.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Api.Models { /// /// Represents a git repository /// - public sealed class Repository : Internal.RepositorySettings + public sealed class Repository : RepositorySettings, IGitRemoteInformation { /// /// The origin URL. If , the does not exist @@ -29,15 +30,14 @@ namespace Tgstation.Server.Api.Models /// public RevisionInformation? RevisionInformation { get; set; } - /// - /// If the repository was cloned from GitHub.com this will be set with the owner of the repository - /// - public string? GitHubOwner { get; set; } + /// + public RemoteGitProvider? RemoteGitProvider { get; set; } - /// - /// If the repository was cloned from GitHub.com this will be set with the name of the repository - /// - public string? GitHubName { get; set; } + /// + public string? RemoteRepositoryOwner { get; set; } + + /// + public string? RemoteRepositoryName { get; set; } /// /// The started by the if any @@ -60,4 +60,4 @@ namespace Tgstation.Server.Api.Models /// public ICollection? NewTestMerges { get; set; } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 4a9df3d206..e565f524f3 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; @@ -41,9 +42,9 @@ namespace Tgstation.Server.Host.Components.Deployment readonly IIOManager ioManager; /// - /// The for the . + /// The for the . /// - readonly IGitHubDeploymentManager gitHubDeploymentManager; + readonly IRemoteDeploymentManager remoteDeploymentManager; /// /// The for the @@ -90,19 +91,19 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The value of /// The value of - /// The value of . + /// The value of . /// The value of /// The value of public DmbFactory( IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager remoteDeploymentManager, ILogger logger, Api.Models.Instance instance) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.gitHubDeploymentManager = gitHubDeploymentManager ?? throw new ArgumentNullException(nameof(gitHubDeploymentManager)); + this.remoteDeploymentManager = remoteDeploymentManager ?? throw new ArgumentNullException(nameof(remoteDeploymentManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); @@ -126,7 +127,7 @@ namespace Tgstation.Server.Host.Components.Deployment var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token); // DCT: None available - var deploymentJob = gitHubDeploymentManager.MarkInactive(job, default); + var deploymentJob = remoteDeploymentManager.MarkInactive(job, default); var otherTask = cleanupTask; await Task.WhenAll(otherTask, deleteJob, deploymentJob).ConfigureAwait(false); } @@ -157,7 +158,7 @@ namespace Tgstation.Server.Host.Components.Deployment // Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied if (started) - await gitHubDeploymentManager.StageDeployment( + await remoteDeploymentManager.StageDeployment( newProvider.CompileJob, cancellationToken) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 9f178ea8c0..cbc81333fd 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -1,9 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Octokit; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Text; using System.Threading; @@ -12,10 +10,10 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; -using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; @@ -78,20 +76,15 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly IRepositoryManager repositoryManager; - /// - /// The for . - /// - readonly IGitHubClientFactory gitHubClientFactory; - /// /// The for . /// readonly ICompileJobSink compileJobConsumer; /// - /// The for . + /// The for . /// - readonly IGitHubDeploymentManager gitHubDeploymentManager; + readonly IRemoteDeploymentManager gitHubDeploymentManager; /// /// The for @@ -137,7 +130,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// The value of /// The value of /// The value of - /// The value of . /// The value of . /// The value of . /// The value of . @@ -151,10 +143,9 @@ namespace Tgstation.Server.Host.Components.Deployment IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, - IGitHubClientFactory gitHubClientFactory, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager gitHubDeploymentManager, ILogger logger, Api.Models.Instance metadata) { @@ -165,7 +156,6 @@ namespace Tgstation.Server.Host.Components.Deployment this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.gitHubDeploymentManager = gitHubDeploymentManager ?? throw new ArgumentNullException(nameof(gitHubDeploymentManager)); @@ -590,12 +580,6 @@ namespace Tgstation.Server.Host.Components.Deployment if (repo == null) throw new JobException(ErrorCode.RepoMissing); - if (repo.IsGitHubRepository) - { - repoOwner = repo.GitHubOwner; - repoName = repo.GitHubRepoName; - } - var repoSha = repo.Head; revInfo = await databaseContext .RevisionInformations @@ -693,7 +677,7 @@ namespace Tgstation.Server.Host.Components.Deployment throw; } - var commentsTask = PostDeploymentComments( + var commentsTask = gitHubDeploymentManager.PostDeploymentComments( compileJob, activeCompileJob?.RevisionInformation, repositorySettings, @@ -787,8 +771,8 @@ namespace Tgstation.Server.Host.Components.Deployment revisionInformation, byondLock.Version, DateTimeOffset.Now + estimatedDuration, - repository.GitHubOwner, - repository.GitHubRepoName, + repository.RemoteRepositoryOwner, + repository.RemoteRepositoryName, localCommitExistsOnRemote, cancellationToken) .ConfigureAwait(false); @@ -823,98 +807,5 @@ namespace Tgstation.Server.Host.Components.Deployment await progressTask.ConfigureAwait(false); } } - - /// - /// Post deployment GitHub comments. - /// - /// The deployed . - /// The of the previous deployment. - /// The . - /// The GitHub repostiory owner. - /// The GitHub repostiory name. - /// The for the operation. - /// A representing the running operation. - async Task PostDeploymentComments( - Models.CompileJob compileJob, - Models.RevisionInformation previousRevisionInformation, - Models.RepositorySettings repositorySettings, - string repoOwner, - string repoName, - CancellationToken cancellationToken) - { - if (repositorySettings?.AccessToken == null) - return; - - if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha) - || !repositorySettings.PostTestMergeComment.Value) - return; - - previousRevisionInformation = new Models.RevisionInformation - { - ActiveTestMerges = new List() - }; - - var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); - - async Task CommentOnPR(int prNumber, string comment) - { - try - { - await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment) - .WithToken(cancellationToken) - .ConfigureAwait(false); - } - catch (ApiException e) - { - logger.LogWarning(e, "Error posting GitHub comment!"); - } - } - - var tasks = new List(); - - var deployedRevisionInformation = compileJob.RevisionInformation; - string FormatTestMerge(Models.TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}", - Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty, - testMerge.PullRequestRevision, - testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty, - updated ? "Updated" : "Deployed", - metadata.Name, - deployedRevisionInformation.OriginCommitSha, - deployedRevisionInformation.CommitSha, - compileJob.GitHubDeploymentId.HasValue - ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{repoOwner}/{repoName}/deployments/activity_log?environment=TGS%3A%20{metadata.Name})" - : String.Empty); - - // added prs - foreach (var I in deployedRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !previousRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false))); - - // removed prs - foreach (var I in previousRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !deployedRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed")); - - // updated prs - foreach (var I in deployedRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => previousRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true))); - - if (tasks.Any()) - await Task.WhenAll(tasks).ConfigureAwait(false); - } } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/IGitHubDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs similarity index 57% rename from src/Tgstation.Server.Host/Components/Deployment/IGitHubDeploymentManager.cs rename to src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs index 9b3a955310..58cb4539f6 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IGitHubDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs @@ -1,14 +1,15 @@ +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Models; -namespace Tgstation.Server.Host.Components.Deployment +namespace Tgstation.Server.Host.Components.Deployment.Remote { /// - /// Creates and updates GitHub deployments. + /// Creates and updates remote deployments. /// - interface IGitHubDeploymentManager + interface IRemoteDeploymentManager { /// /// Start a deployment for a given . @@ -54,5 +55,37 @@ namespace Tgstation.Server.Host.Components.Deployment /// The for the operation. /// A representing the running operation. Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken); + + /// + /// Post deployment comments to the test merge ticket. + /// + /// The deployed . + /// The of the previous deployment. + /// The . + /// The GitHub repostiory owner. + /// The GitHub repostiory name. + /// The for the operation. + /// A representing the running operation. + Task PostDeploymentComments( + CompileJob compileJob, + RevisionInformation previousRevisionInformation, + RepositorySettings repositorySettings, + string repoOwner, + string repoName, + CancellationToken cancellationToken); + + /// + /// Get the updated list of s for an origin merge. + /// + /// The to use. + /// The . + /// The current . + /// The for the operation. + /// A resulting in the of s that should remain the new . + Task> RemoveMergedPullRequests( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/GitHubDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManager.cs similarity index 53% rename from src/Tgstation.Server.Host/Components/Deployment/GitHubDeploymentManager.cs rename to src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManager.cs index 7b931405d5..cd10dea7e0 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/GitHubDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManager.cs @@ -2,7 +2,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Octokit; using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,42 +14,42 @@ using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; -namespace Tgstation.Server.Host.Components.Deployment +namespace Tgstation.Server.Host.Components.Deployment.Remote { /// - sealed class GitHubDeploymentManager : IGitHubDeploymentManager + sealed class RemoteDeploymentManager : IRemoteDeploymentManager { /// - /// The for the . + /// The for the . /// readonly IDatabaseContextFactory databaseContextFactory; /// - /// The for the . + /// The for the . /// readonly IGitHubClientFactory gitHubClientFactory; /// - /// The for the . + /// The for the . /// - readonly ILogger logger; + readonly ILogger logger; /// - /// The for the . + /// The for the . /// readonly Api.Models.Instance metadata; /// - /// Initializes a new instance of the . + /// Initializes a new instance of the . /// /// The value of . /// The value of . /// The value of . /// The value of . - public GitHubDeploymentManager( + public RemoteDeploymentManager( IDatabaseContextFactory databaseContextFactory, IGitHubClientFactory gitHubClientFactory, - ILogger logger, + ILogger logger, Api.Models.Instance metadata) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -64,7 +66,7 @@ namespace Tgstation.Server.Host.Components.Deployment if (compileJob == null) throw new ArgumentNullException(nameof(compileJob)); - if (!repository.IsGitHubRepository) + if (repository.RemoteGitProvider != Api.Models.RemoteGitProvider.GitHub) { logger.LogTrace("Not managing deployment as this is not a GitHub repo"); return; @@ -83,6 +85,7 @@ namespace Tgstation.Server.Host.Components.Deployment .ConfigureAwait(false)) .ConfigureAwait(false); + var instanceAuthenticated = repositorySettings.AccessToken == null; var gitHubClient = repositorySettings.AccessToken == null ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(repositorySettings.AccessToken); @@ -90,18 +93,22 @@ namespace Tgstation.Server.Host.Components.Deployment var repositoryTask = gitHubClient .Repository .Get( - repository.GitHubOwner, - repository.GitHubRepoName); + repository.RemoteRepositoryOwner, + repository.RemoteRepositoryName); - if (repositorySettings.CreateGitHubDeployments.Value) + if (!repositorySettings.CreateGitHubDeployments.Value) + logger.LogTrace("Not creating deployment"); + else if (!instanceAuthenticated) + logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!"); + else { logger.LogTrace("Creating deployment..."); var deployment = await gitHubClient .Repository .Deployment .Create( - repository.GitHubOwner, - repository.GitHubRepoName, + repository.RemoteRepositoryOwner, + repository.RemoteRepositoryName, new NewDeployment(compileJob.RevisionInformation.CommitSha) { AutoMerge = false, @@ -121,8 +128,8 @@ namespace Tgstation.Server.Host.Components.Deployment .Deployment .Status .Create( - repository.GitHubOwner, - repository.GitHubRepoName, + repository.RemoteRepositoryOwner, + repository.RemoteRepositoryName, deployment.Id, new NewDeploymentStatus(DeploymentState.InProgress) { @@ -134,8 +141,6 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogTrace("In-progress deployment status created"); } - else - logger.LogTrace("Not creating deployment"); try { @@ -239,5 +244,147 @@ namespace Tgstation.Server.Host.Components.Deployment "The deployment has been superceeded.", DeploymentState.Inactive, cancellationToken); + + /// + public async Task PostDeploymentComments( + CompileJob compileJob, + RevisionInformation previousRevisionInformation, + RepositorySettings repositorySettings, + string repoOwner, + string repoName, + CancellationToken cancellationToken) + { + if (repositorySettings?.AccessToken == null) + return; + + if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha) + || !repositorySettings.PostTestMergeComment.Value) + return; + + previousRevisionInformation = new RevisionInformation + { + ActiveTestMerges = new List() + }; + + var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); + + async Task CommentOnPR(int prNumber, string comment) + { + try + { + await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment) + .WithToken(cancellationToken) + .ConfigureAwait(false); + } + catch (ApiException e) + { + logger.LogWarning(e, "Error posting GitHub comment!"); + } + } + + var tasks = new List(); + + var deployedRevisionInformation = compileJob.RevisionInformation; + string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}", + Environment.NewLine, + repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty, + testMerge.PullRequestRevision, + testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty, + updated ? "Updated" : "Deployed", + metadata.Name, + deployedRevisionInformation.OriginCommitSha, + deployedRevisionInformation.CommitSha, + compileJob.GitHubDeploymentId.HasValue + ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{repoOwner}/{repoName}/deployments/activity_log?environment=TGS%3A%20{metadata.Name})" + : String.Empty); + + // added prs + foreach (var I in deployedRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !previousRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false))); + + // removed prs + foreach (var I in previousRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !deployedRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed")); + + // updated prs + foreach (var I in deployedRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => previousRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true))); + + if (tasks.Any()) + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// + public async Task> RemoveMergedPullRequests( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken) + { + if (revisionInformation.ActiveTestMerges?.Any() != true) + { + logger.LogTrace("No test merges to remove."); + return Array.Empty(); + } + + var gitHubClient = repositorySettings.AccessToken != null + ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) + : gitHubClientFactory.CreateClient(); + + var tasks = revisionInformation + .ActiveTestMerges + .Select(x => gitHubClient + .PullRequest + .Get(repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, x.TestMerge.Number) + .WithToken(cancellationToken)); + try + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + logger.LogWarning(ex, "Pull requests update check failed!"); + } + + var newList = revisionInformation.ActiveTestMerges.ToList(); + + PullRequest lastMerged = null; + async Task CheckRemovePR(Task task) + { + var pr = await task.ConfigureAwait(false); + if (!pr.Merged) + return; + + // We don't just assume, actually check the repo contains the merge commit. + if (await repository.ShaIsParent(pr.MergeCommitSha, cancellationToken).ConfigureAwait(false)) + { + if (lastMerged == null || lastMerged.MergedAt < pr.MergedAt) + lastMerged = pr; + newList.Remove( + newList.First( + potential => potential.TestMerge.Number == pr.Number)); + } + } + + foreach (var prTask in tasks) + await CheckRemovePR(prTask).ConfigureAwait(false); + + return newList; + } } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index c4f0ee24ec..cbf9d2872e 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Octokit; using Serilog.Context; using System; using System.Collections.Generic; @@ -11,13 +10,11 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -66,9 +63,9 @@ namespace Tgstation.Server.Host.Components readonly IEventConsumer eventConsumer; /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IRemoteDeploymentManager remoteDeploymentManager; /// /// The for the @@ -80,11 +77,6 @@ namespace Tgstation.Server.Host.Components /// readonly Api.Models.Instance metadata; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// for and . /// @@ -113,9 +105,8 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of . + /// The value of . /// The value of - /// The value of . public Instance( Api.Models.Instance metadata, IRepositoryManager repositoryManager, @@ -128,9 +119,8 @@ namespace Tgstation.Server.Host.Components IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, - IGitHubClientFactory gitHubClientFactory, - ILogger logger, - GeneralConfiguration generalConfiguration) + IRemoteDeploymentManager remoteDeploymentManager, + ILogger logger) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); @@ -142,9 +132,8 @@ namespace Tgstation.Server.Host.Components this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.remoteDeploymentManager = remoteDeploymentManager ?? throw new ArgumentNullException(nameof(remoteDeploymentManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); timerLock = new object(); } @@ -279,7 +268,7 @@ namespace Tgstation.Server.Host.Components { currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); - var updatedTestMerges = await RemoveMergedPullRequests( + var updatedTestMerges = await remoteDeploymentManager.RemoveMergedPullRequests( repo, repositorySettings, currentRevInfo, @@ -456,73 +445,6 @@ namespace Tgstation.Server.Host.Components } #pragma warning restore CA1502 - /// - /// Get the updated list of s for an origin merge. - /// - /// The to use. - /// The . - /// The current . - /// The for the operation. - /// A resulting in the of s that should remain the new . - async Task> RemoveMergedPullRequests( - IRepository repository, - RepositorySettings repositorySettings, - RevisionInformation revisionInformation, - CancellationToken cancellationToken) - { - if (revisionInformation.ActiveTestMerges?.Any() != true) - { - logger.LogTrace("No test merges to remove."); - return Array.Empty(); - } - - var gitHubClient = repositorySettings.AccessToken != null - ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) - : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) - ? gitHubClientFactory.CreateClient() - : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); - - var tasks = revisionInformation - .ActiveTestMerges - .Select(x => gitHubClient - .PullRequest - .Get(repository.GitHubOwner, repository.GitHubRepoName, x.TestMerge.Number) - .WithToken(cancellationToken)); - try - { - await Task.WhenAll(tasks).ConfigureAwait(false); - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - logger.LogWarning(ex, "Pull requests update check failed!"); - } - - var newList = revisionInformation.ActiveTestMerges.ToList(); - - PullRequest lastMerged = null; - async Task CheckRemovePR(Task task) - { - var pr = await task.ConfigureAwait(false); - if (!pr.Merged) - return; - - // We don't just assume, actually check the repo contains the merge commit. - if (await repository.ShaIsParent(pr.MergeCommitSha, cancellationToken).ConfigureAwait(false)) - { - if (lastMerged == null || lastMerged.MergedAt < pr.MergedAt) - lastMerged = pr; - newList.Remove( - newList.First( - potential => potential.TestMerge.Number == pr.Number)); - } - } - - foreach (var prTask in tasks) - await CheckRemovePR(prTask).ConfigureAwait(false); - - return newList; - } - /// public Task InstanceRenamed(string newName, CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 548410eeef..cea3e83292 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -7,6 +7,7 @@ using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Repository; @@ -248,16 +249,16 @@ namespace Tgstation.Server.Host.Components loggerFactory.CreateLogger(), metadata.CloneMetadata()); - var gitHubDeploymentManager = new GitHubDeploymentManager( + var remoteDeploymentManager = new RemoteDeploymentManager( databaseContextFactory, gitHubClientFactory, - loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), metadata.CloneMetadata()); var dmbFactory = new DmbFactory( databaseContextFactory, gameIoManager, - gitHubDeploymentManager, + remoteDeploymentManager, loggerFactory.CreateLogger(), metadata.CloneMetadata()); try @@ -276,7 +277,7 @@ namespace Tgstation.Server.Host.Components gameIoManager, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManager, metadata.CloneMetadata(), metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); @@ -292,10 +293,9 @@ namespace Tgstation.Server.Host.Components eventConsumer, chatManager, processExecutor, - gitHubClientFactory, dmbFactory, repoManager, - gitHubDeploymentManager, + remoteDeploymentManager, loggerFactory.CreateLogger(), metadata.CloneMetadata()); @@ -310,9 +310,8 @@ namespace Tgstation.Server.Host.Components dmbFactory, jobManager, eventConsumer, - gitHubClientFactory, - loggerFactory.CreateLogger(), - generalConfiguration); + remoteDeploymentManager, + loggerFactory.CreateLogger()); return instance; } diff --git a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs new file mode 100644 index 0000000000..868ac3f789 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs @@ -0,0 +1,22 @@ +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// The used for unknown providers. + /// + sealed class DefaultGitRemoteFeatures : IGitRemoteFeatures + { + /// + public string TestMergeRefSpecFormatter => null; + + /// + public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.Unknown; + + /// + public string RemoteRepositoryOwner => null; + + /// + public string RemoteRepositoryName => null; + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs new file mode 100644 index 0000000000..b7cac504da --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -0,0 +1,36 @@ +using System; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// GitHub . + /// + sealed class GitHubRemoteFeatures : IGitRemoteFeatures + { + /// + public string TestMergeRefSpecFormatter => "pull/{0}/head:{1}"; + + /// + public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitHub; + + /// + public string RemoteRepositoryOwner { get; } + + /// + public string RemoteRepositoryName { get; } + + /// + /// Initializes a new instance of the . + /// + /// The remote repository . + public GitHubRemoteFeatures(Uri remoteUrl) + { + if (remoteUrl == null) + throw new ArgumentNullException(nameof(remoteUrl)); + + RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); + RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs new file mode 100644 index 0000000000..d26e9cd562 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs @@ -0,0 +1,15 @@ +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Provides features for remote git services + /// + interface IGitRemoteFeatures : IGitRemoteInformation + { + /// + /// Gets a formatter string which creates the remote refspec for fetching the HEAD of passed in pull request number. + /// + string TestMergeRefSpecFormatter { get; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs index ac5ee07b91..79e20a64fb 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using System; using System.Threading; using System.Threading.Tasks; @@ -21,8 +21,8 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The full path to the . /// The for the operation. - /// A resulting in, the loaded . - Task CreateFromPath(string path, CancellationToken cancellationToken); + /// A resulting in a containing the loaded and the associated . + Task> CreateFromPath(string path, CancellationToken cancellationToken); /// /// Clone a remote . @@ -34,4 +34,4 @@ namespace Tgstation.Server.Host.Components.Repository /// A representing the running operation. Task Clone(Uri url, CloneOptions cloneOptions, string path, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 02cc7f7175..41dd80e15e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -2,29 +2,15 @@ using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Host.Components.Repository { /// /// Represents an on-disk git repository /// - public interface IRepository : IDisposable + public interface IRepository : IGitRemoteInformation, IDisposable { - /// - /// If the was cloned from GitHub.com - /// - bool IsGitHubRepository { get; } - - /// - /// The if this - /// - string GitHubOwner { get; } - - /// - /// The if this - /// - string GitHubRepoName { get; } - /// /// If tracks an upstream branch /// diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index b09fbc9c45..bd1f7de84c 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -2,6 +2,7 @@ using LibGit2Sharp; using LibGit2Sharp.Handlers; using Microsoft.Extensions.Logging; using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -35,12 +36,12 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public Task CreateFromPath(string path, CancellationToken cancellationToken) + public async Task> CreateFromPath(string path, CancellationToken cancellationToken) { if (path == null) throw new ArgumentNullException(nameof(path)); - return Task.Factory.StartNew( + var repo = await Task.Factory.StartNew( () => { logger.LogTrace("Creating libgit2 repostory at {0}...", path); @@ -48,7 +49,45 @@ namespace Tgstation.Server.Host.Components.Repository }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, - TaskScheduler.Current); + TaskScheduler.Current) + .ConfigureAwait(false); + + try + { + var remoteFeatures = CreateGitRemoteFeatures(repo); + return Tuple.Create(repo, remoteFeatures); + } + catch + { + repo.Dispose(); + throw; + } + } + + IGitRemoteFeatures CreateGitRemoteFeatures(LibGit2Sharp.IRepository repo) + { + var primaryRemote = repo.Network.Remotes.First(); + var primaryRemoteUrl = new Uri(primaryRemote.Url); + + try + { + switch (primaryRemoteUrl.Host.ToUpperInvariant()) + { + case "GITHUB.COM": + case "WWW.GITHUB.COM": + case "GIT.GITHUB.COM": + return new GitHubRemoteFeatures(primaryRemoteUrl); + default: + logger.LogTrace("Unknown git remote: {0}", primaryRemoteUrl); + break; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error parsing remote git provider."); + } + + return new DefaultGitRemoteFeatures(); } /// diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 53e7d5b50b..6c752c0cf6 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -17,11 +17,6 @@ namespace Tgstation.Server.Host.Components.Repository /// sealed class Repository : IRepository { - /// - /// Indication of a GitHub repository - /// - public const string GitHubUrl = "://github.com/"; - /// /// The default username for committers. /// @@ -42,16 +37,19 @@ namespace Tgstation.Server.Host.Components.Repository /// public const string RemoteTemporaryBranchName = "___TGSTempBranch"; + /// + /// Used when a reference cannot be determined + /// const string UnknownReference = ""; /// - public bool IsGitHubRepository { get; } + public RemoteGitProvider? RemoteGitProvider => gitRemoteFeatures.RemoteGitProvider; /// - public string GitHubOwner { get; } + public string RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner; /// - public string GitHubRepoName { get; } + public string RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName; /// public bool Tracking => Reference != null && libGitRepo.Head.IsTracking; @@ -90,6 +88,11 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly ICredentialsProvider credentialsProvider; + /// + /// The for the . + /// + readonly IGitRemoteFeatures gitRemoteFeatures; + /// /// The for the /// @@ -120,6 +123,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of /// The value of /// The value of + /// The value of . /// The value of /// The value if public Repository( @@ -128,6 +132,7 @@ namespace Tgstation.Server.Host.Components.Repository IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, + IGitRemoteFeatures gitRemoteFeatures, ILogger logger, Action onDispose) { @@ -136,17 +141,9 @@ namespace Tgstation.Server.Host.Components.Repository this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); + this.gitRemoteFeatures = gitRemoteFeatures ?? throw new ArgumentNullException(nameof(gitRemoteFeatures)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); - - IsGitHubRepository = Origin.Contains(GitHubUrl, StringComparison.InvariantCultureIgnoreCase); - - if (IsGitHubRepository) - { - GetRepositoryOwnerName(Origin, out var owner, out var name); - GitHubOwner = owner; - GitHubRepoName = name; - } } /// @@ -165,9 +162,15 @@ namespace Tgstation.Server.Host.Components.Repository onDispose(); } + /// + /// Parses the and for a given git . + /// + /// The full remote URL. + /// The parsed owner. + /// The parsed name. void GetRepositoryOwnerName(string remote, out string owner, out string name) { - // Assume standard gh format: [(git)|(https)]://github.com/owner/repo(.git)[0-1] + // Assume standard gh format: [(git)|(https)]://[].com/owner/repo(.git)[0-1] // Yes use .git twice in case it was weird var toRemove = new string[] { ".git", "/", ".git" }; foreach (string item in toRemove) @@ -283,7 +286,7 @@ namespace Tgstation.Server.Host.Components.Repository committerName, committerEmail); - if (!IsGitHubRepository) + if (RemoteGitProvider == Api.Models.RemoteGitProvider.Unknown) throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); var commitMessage = String.Format( @@ -298,7 +301,7 @@ namespace Tgstation.Server.Host.Components.Repository var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", testMergeParameters.Number); var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", testMergeParameters.Number, prBranchName); - var refSpec = String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", testMergeParameters.Number, prBranchName); + var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, prBranchName); var refSpecList = new List { refSpec }; var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", testMergeParameters.Number); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index b3a1b858b1..c82b56e22a 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -188,18 +188,32 @@ namespace Tgstation.Server.Host.Components.Repository { try { - var libGitRepo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false); - return new Repository( - libGitRepo, - commands, - ioManager, - eventConsumer, - repositoryFactory, - repositoryLogger, () => + var repoTuple = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false); + + try { - logger.LogTrace("Releasing semaphore due to Repository disposal..."); - semaphore.Release(); - }); + var libGit2Repo = repoTuple.Item1; + var gitRemoteFeatures = repoTuple.Item2; + + return new Repository( + libGit2Repo, + commands, + ioManager, + eventConsumer, + repositoryFactory, + gitRemoteFeatures, + repositoryLogger, + () => + { + logger.LogTrace("Releasing semaphore due to Repository disposal..."); + semaphore.Release(); + }); + } + catch + { + repoTuple.Item1.Dispose(); + throw; + } } catch { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index faf935a4e1..421ca862ba 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; @@ -47,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . + /// The for the . /// The for the . /// The for the . /// The for the . @@ -62,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager remoteDeploymentManager, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, @@ -77,7 +78,7 @@ namespace Tgstation.Server.Host.Components.Watchdog asyncDelayer, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManager, logger, initialLaunchParameters, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs index 128361d841..fa3b9b016d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs @@ -1,6 +1,7 @@ using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.IO; @@ -22,7 +23,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The pointing to the Game directory for the . /// The pointing to the Diagnostics directory for the . /// The for the . - /// The for the . + /// The for the . /// The for the /// The initial for the /// A new @@ -34,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager remoteDeploymentManager, Api.Models.Instance instance, DreamDaemonSettings settings); } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 9259b15253..849c27c27d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; @@ -35,7 +36,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . + /// The for the . /// The pointing to the game directory for the .. /// The for the . /// The for the . @@ -52,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager gitHubDeploymentManager, IIOManager gameIOManager, ISymlinkFactory symlinkFactory, ILogger logger, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 8a1575f89c..df8e4d2351 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -4,6 +4,7 @@ using System; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Configuration; @@ -52,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager gitHubDeploymentManager, Api.Models.Instance instance, DreamDaemonSettings settings) => new PosixWatchdog( diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index e9843b2294..836209a670 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.Components.Session; @@ -123,9 +124,9 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly IEventConsumer eventConsumer; /// - /// The for the . + /// The for the . /// - readonly IGitHubDeploymentManager gitHubDeploymentManager; + readonly IRemoteDeploymentManager remoteDeploymentManager; /// /// If the should in @@ -179,7 +180,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of . /// The value of . /// The value of . - /// The value of . + /// The value of . /// The value of /// The initial value of . May be modified /// The value of @@ -194,7 +195,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager remoteDeploymentManager, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, @@ -208,7 +209,7 @@ namespace Tgstation.Server.Host.Components.Watchdog AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); - this.gitHubDeploymentManager = gitHubDeploymentManager ?? throw new ArgumentNullException(nameof(gitHubDeploymentManager)); + this.remoteDeploymentManager = remoteDeploymentManager ?? throw new ArgumentNullException(nameof(remoteDeploymentManager)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); @@ -536,7 +537,7 @@ namespace Tgstation.Server.Host.Components.Watchdog return Task.CompletedTask; } - return gitHubDeploymentManager.ApplyDeployment(newCompileJob, ActiveCompileJob, cancellationToken); + return remoteDeploymentManager.ApplyDeployment(newCompileJob, ActiveCompileJob, cancellationToken); } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index 3592b413cf..d08a666e3a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -4,6 +4,7 @@ using System; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Configuration; @@ -72,7 +73,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager remoteDeploymentManager, Api.Models.Instance instance, DreamDaemonSettings settings) => new BasicWatchdog( @@ -85,7 +86,7 @@ namespace Tgstation.Server.Host.Components.Watchdog AsyncDelayer, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManager, LoggerFactory.CreateLogger(), settings, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index a59fa95ff0..1f40899f7b 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; @@ -55,7 +56,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . + /// The for the . /// The value of . /// The value of . /// The for the . @@ -72,7 +73,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager gitHubDeploymentManager, IIOManager gameIOManager, ISymlinkFactory symlinkFactory, ILogger logger, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 98c7d1c2c2..99a18670f3 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -4,6 +4,7 @@ using System; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Configuration; @@ -58,7 +59,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IGitHubDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManager gitHubDeploymentManager, Api.Models.Instance instance, DreamDaemonSettings settings) => new WindowsWatchdog( diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index c35de94da2..dfedfdb9ec 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -63,11 +63,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly UpdatesConfiguration updatesConfiguration; - /// - /// The for the - /// - readonly GeneralConfiguration generalConfiguration; - /// /// The for the /// @@ -85,7 +80,6 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The for the /// The containing value of - /// The containing value of /// The containing value of public AdministrationController( IDatabaseContext databaseContext, @@ -97,7 +91,6 @@ namespace Tgstation.Server.Host.Controllers IPlatformIdentifier platformIdentifier, ILogger logger, IOptions updatesConfigurationOptions, - IOptions generalConfigurationOptions, IOptions fileLoggingConfigurationOptions) : base( databaseContext, @@ -111,7 +104,6 @@ namespace Tgstation.Server.Host.Controllers this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } @@ -135,7 +127,7 @@ namespace Tgstation.Server.Host.Controllers IEnumerable releases; try { - var gitHubClient = GetGitHubClient(); + var gitHubClient = gitHubClientFactory.CreateClient(); releases = await gitHubClient .Repository .Release @@ -180,8 +172,6 @@ namespace Tgstation.Server.Host.Controllers return Gone(); } - IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken); - /// /// Get server information. /// @@ -203,7 +193,7 @@ namespace Tgstation.Server.Host.Controllers Uri repoUrl = null; try { - var gitHubClient = GetGitHubClient(); + var gitHubClient = gitHubClientFactory.CreateClient(); var repositoryTask = gitHubClient .Repository .Get(updatesConfiguration.GitHubRepositoryId) diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 88de87bbce..e1c43c6bcc 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -123,11 +123,9 @@ namespace Tgstation.Server.Host.Controllers async Task PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken) { - if (repository.IsGitHubRepository) - { - model.GitHubOwner = repository.GitHubOwner; - model.GitHubName = repository.GitHubRepoName; - } + model.RemoteGitProvider = repository.RemoteGitProvider; + model.RemoteRepositoryOwner = repository.RemoteRepositoryOwner; + model.RemoteRepositoryName = repository.RemoteRepositoryName; model.Origin = repository.Origin; model.Reference = repository.Reference; @@ -172,14 +170,6 @@ namespace Tgstation.Server.Host.Controllers if (currentModel == default) return Gone(); - // normalize github urls - const string BadGitHubUrl = "://www.github.com/"; - var uiOrigin = model.Origin.ToUpperInvariant(); - var uiBad = BadGitHubUrl.ToUpperInvariant(); - var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant(); - if (uiOrigin.Contains(uiBad, StringComparison.Ordinal)) - model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal); - currentModel.AccessToken = model.AccessToken; currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm var cloneBranch = model.Reference; @@ -507,7 +497,7 @@ namespace Tgstation.Server.Host.Controllers var startSha = repo.Head; string postUpdateSha = null; - if (newTestMerges && !repo.IsGitHubRepository) + if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown) throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); var committerName = currentModel.ShowTestMergeCommitters.Value @@ -655,18 +645,19 @@ namespace Tgstation.Server.Host.Controllers Dictionary prMap = null; if (newTestMerges) { + if (repo.RemoteGitProvider == RemoteGitProvider.Unknown) + throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); + // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision))) I.PullRequestRevision = null; var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) - : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) - ? gitHubClientFactory.CreateClient() - : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); + : gitHubClientFactory.CreateClient(); - var repoOwner = repo.GitHubOwner; - var repoName = repo.GitHubRepoName; + var repoOwner = repo.RemoteRepositoryOwner; + var repoName = repo.RemoteRepositoryName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; diff --git a/src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs b/src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs index 7066ed1d29..68cf07d9e7 100644 --- a/src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs @@ -1,4 +1,4 @@ -using Octokit; +using Octokit; namespace Tgstation.Server.Host.Core { @@ -8,9 +8,9 @@ namespace Tgstation.Server.Host.Core public interface IGitHubClientFactory { /// - /// Create a client with anonymous authentication. Low rate limit + /// Create a client with anonymous authentication or general authentica. Low rate limit. Attempts to use the server's token to bypass this. /// - /// A new + /// A new . IGitHubClient CreateClient(); /// @@ -20,4 +20,4 @@ namespace Tgstation.Server.Host.Core /// A new IGitHubClient CreateClient(string accessToken); } -} \ No newline at end of file +} diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index 0b49bbc5d6..3fcb973507 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -16,11 +16,12 @@ namespace Tgstation.Server.Host.Components.Repository.Tests { static ILibGit2RepositoryFactory CreateFactory() => new LibGit2RepositoryFactory(Mock.Of>()); - static Task TestRepoLoading( + static async Task TestRepoLoading( string path, ILibGit2RepositoryFactory repositoryFactory = null) => - (repositoryFactory ?? CreateFactory()) - .CreateFromPath(path, default); + (await (repositoryFactory ?? CreateFactory()) + .CreateFromPath(path, default)) + .Item1; [TestMethod] public void TestConstructionThrows() => Assert.ThrowsException(() => new LibGit2RepositoryFactory(null)); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 5fcb6e5a42..91b272d01c 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -459,6 +459,7 @@ namespace Tgstation.Server.Tests Mock.Of(), Mock.Of(), Mock.Of(), + Mock.Of(), Mock.Of>(), () => { }); From a231a776c89e7ff0b747b50049688757e0975ba9 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 4 Dec 2020 23:52:04 -0500 Subject: [PATCH 019/154] Do not give /world/Topic TGS topics - Return TRUE and log a warning if it's too early for us to handle it --- build/Version.props | 2 +- src/DMAPI/tgs.dm | 2 +- src/DMAPI/tgs/v5/api.dm | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/build/Version.props b/build/Version.props index bb2c35a17a..1a3715b698 100644 --- a/build/Version.props +++ b/build/Version.props @@ -7,7 +7,7 @@ 2.1.1 7.4.0 8.4.0 - 5.2.9 + 5.2.10 1.1.0 1.1.1 diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index e70955845c..2a024cb450 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "5.2.8" +#define TGS_DMAPI_VERSION "5.2.10" // All functions and datums outside this document are subject to change with any version and should not be relied on. diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 466a986237..7a2ff694e0 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -98,18 +98,19 @@ return json_encode(response) /datum/tgs_api/v5/OnTopic(T) - if(!initialized) - return FALSE //continue world/Topic - var/list/params = params2list(T) var/json = params[DMAPI5_TOPIC_DATA] if(!json) - return FALSE + return FALSE // continue to /world/Topic var/list/topic_parameters = json_decode(json) if(!topic_parameters) return TopicResponse("Invalid topic parameters json!"); + if(!initialized) + TGS_WARNING_LOG("Missed topic due to not being initialized: [T]") + return TRUE // too early to handle, but it's still our responsibility + var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] if(their_sCK != access_identifier) return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!"); From bc505c244616f960aae5356bfef59fad5314dee5 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 6 Dec 2020 19:37:20 -0500 Subject: [PATCH 020/154] Diverse test merging big commit --- .../Models/RemoteGitProvider.cs | 5 + .../Components/Deployment/DreamMaker.cs | 4 +- .../Components/InstanceFactory.cs | 9 ++ .../Repository/DefaultGitRemoteFeatures.cs | 14 ++- .../Repository/GitHubRemoteFeatures.cs | 93 ++++++++++++++++-- .../Repository/GitLabRemoteFeatures.cs | 97 +++++++++++++++++++ .../Repository/GitRemoteFeaturesBase.cs | 95 ++++++++++++++++++ .../Repository/GitRemoteFeaturesFactory.cs | 80 +++++++++++++++ .../IGitRemoteAdditionalInformation.cs | 25 +++++ .../Repository/IGitRemoteFeatures.cs | 9 +- .../Repository/IGitRemoteFeaturesFactory.cs | 15 +++ .../Repository/ILibGit2RepositoryFactory.cs | 4 +- .../Components/Repository/IRepository.cs | 3 +- .../Repository/LibGit2RepositoryFactory.cs | 40 +------- .../Components/Repository/Repository.cs | 22 ++++- .../Repository/RepositoryManager.cs | 47 +++++---- .../Controllers/RepositoryController.cs | 92 ++++++------------ src/Tgstation.Server.Host/Core/Application.cs | 15 ++- src/Tgstation.Server.Host/Models/Job.cs | 4 +- .../Tgstation.Server.Host.csproj | 1 + .../Repository/TestRepositoryFactory.cs | 3 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 2 +- 22 files changed, 521 insertions(+), 158 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs create mode 100644 src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs create mode 100644 src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Repository/IGitRemoteAdditionalInformation.cs create mode 100644 src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs diff --git a/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs b/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs index 61a3f73d0a..df0da1312b 100644 --- a/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs +++ b/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs @@ -14,5 +14,10 @@ namespace Tgstation.Server.Api.Models /// Remote provider is GitHub.com /// GitHub, + + /// + /// Remote provider is GitLab.com + /// + GitLab, } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index cbc81333fd..62b62aa652 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -495,7 +495,7 @@ namespace Tgstation.Server.Host.Components.Deployment } /// - #pragma warning disable CA1506 + #pragma warning disable CA1506, CA1508 public async Task DeploymentProcess( Models.Job job, IDatabaseContextFactory databaseContextFactory, @@ -714,7 +714,7 @@ namespace Tgstation.Server.Host.Components.Deployment deploying = false; } } - #pragma warning restore CA1506 + #pragma warning restore CA1506, CA1508 /// /// Calculate the average length of a deployment using a given . diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index c2f1587ca4..12e308ccb0 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -132,6 +132,11 @@ namespace Tgstation.Server.Host.Components /// readonly IFileTransferTicketProvider fileTransferService; + /// + /// The for the . + /// + readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + /// /// The for the . /// @@ -161,6 +166,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The value of . /// The containing the value of . public InstanceFactory( IIOManager ioManager, @@ -184,6 +190,7 @@ namespace Tgstation.Server.Host.Components ILibGit2Commands repositoryCommands, IServerPortProvider serverPortProvider, IFileTransferTicketProvider fileTransferService, + IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, IOptions generalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -207,6 +214,7 @@ namespace Tgstation.Server.Host.Components this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); + this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } @@ -239,6 +247,7 @@ namespace Tgstation.Server.Host.Components repositoryCommands, repoIoManager, eventConsumer, + gitRemoteFeaturesFactory, loggerFactory.CreateLogger(), loggerFactory.CreateLogger()); try diff --git a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs index 868ac3f789..3bdc876709 100644 --- a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading; +using System.Threading.Tasks; using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Components.Repository @@ -8,7 +11,10 @@ namespace Tgstation.Server.Host.Components.Repository sealed class DefaultGitRemoteFeatures : IGitRemoteFeatures { /// - public string TestMergeRefSpecFormatter => null; + public string TestMergeRefSpecFormatter => throw new NotSupportedException(); + + /// + public string TestMergeLocalBranchNameFormatter => throw new NotSupportedException(); /// public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.Unknown; @@ -18,5 +24,11 @@ namespace Tgstation.Server.Host.Components.Repository /// public string RemoteRepositoryName => null; + + /// + public Task GetTestMerge( + TestMergeParameters parameters, + Api.Models.Internal.RepositorySettings repositorySettings, + CancellationToken cancellationToken) => throw new NotSupportedException(); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index b7cac504da..1878171196 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -1,36 +1,117 @@ +using Microsoft.Extensions.Logging; +using Octokit; using System; +using System.Threading; +using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Extensions; namespace Tgstation.Server.Host.Components.Repository { /// /// GitHub . /// - sealed class GitHubRemoteFeatures : IGitRemoteFeatures + sealed class GitHubRemoteFeatures : GitRemoteFeaturesBase { /// - public string TestMergeRefSpecFormatter => "pull/{0}/head:{1}"; + public override string TestMergeRefSpecFormatter => "pull/{0}/head:{1}"; /// - public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitHub; + public override string TestMergeLocalBranchNameFormatter => "pull/{0}/headrefs/heads/{1}"; /// - public string RemoteRepositoryOwner { get; } + public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitHub; /// - public string RemoteRepositoryName { get; } + public override string RemoteRepositoryOwner { get; } + + /// + public override string RemoteRepositoryName { get; } + + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; /// /// Initializes a new instance of the . /// + /// The value of . + /// The for the . /// The remote repository . - public GitHubRemoteFeatures(Uri remoteUrl) + public GitHubRemoteFeatures(IGitHubClientFactory gitHubClientFactory, ILogger logger, Uri remoteUrl) + : base(logger, remoteUrl) { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + if (remoteUrl == null) throw new ArgumentNullException(nameof(remoteUrl)); RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); } + + /// + protected override async Task GetTestMergeImpl( + TestMergeParameters parameters, + RepositorySettings repositorySettings, + CancellationToken cancellationToken) + { + var gitHubClient = repositorySettings.AccessToken != null + ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) + : gitHubClientFactory.CreateClient(); + + PullRequest pr = null; + ApiException exception = null; + string errorMessage = null; + try + { + pr = await gitHubClient + .PullRequest + .Get(RemoteRepositoryOwner, RemoteRepositoryName, parameters.Number) + .WithToken(cancellationToken) + .ConfigureAwait(false); + } + catch (RateLimitExceededException ex) + { + // you look at your anonymous access and sigh + errorMessage = "GITHUB API ERROR: RATE LIMITED"; + exception = ex; + } + catch (AuthorizationException ex) + { + errorMessage = "GITHUB API ERROR: BAD CREDENTIALS"; + exception = ex; + } + catch (NotFoundException ex) + { + // you look at your shithub and sigh + errorMessage = "GITHUB API ERROR: PULL REQUEST NOT FOUND"; + exception = ex; + } + + if (exception != null) + Logger.LogWarning(exception, "Error retrieving pull request metadata!"); + + var revisionToUse = parameters.PullRequestRevision == null + || pr?.Head.Sha.StartsWith(parameters.PullRequestRevision, StringComparison.OrdinalIgnoreCase) == true + ? pr?.Head.Sha + : parameters.PullRequestRevision; + + var testMerge = new Models.TestMerge + { + Author = pr?.User.Login ?? errorMessage, + BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty, + TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, + Comment = parameters.Comment, + Number = parameters.Number, + PullRequestRevision = revisionToUse, + Url = pr?.HtmlUrl ?? errorMessage + }; + + return testMerge; + } } } diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs new file mode 100644 index 0000000000..aa5b932fd5 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -0,0 +1,97 @@ +using GitLabApiClient; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Extensions; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// GitLab . + /// + sealed class GitLabRemoteFeatures : GitRemoteFeaturesBase + { + /// + public override string TestMergeRefSpecFormatter => "merge-requests/{0}/head:{1}"; + + /// + public override string TestMergeLocalBranchNameFormatter => "merge-requests/{0}/headrefs/heads/{1}"; + + /// + public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitLab; + + /// + public override string RemoteRepositoryOwner { get; } + + /// + public override string RemoteRepositoryName { get; } + + /// + /// Initializes a new instance of the . + /// + /// The for the . + /// The remote repository . + public GitLabRemoteFeatures(ILogger logger, Uri remoteUrl) + : base(logger, remoteUrl) + { + RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); + RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); + } + + /// + protected override async Task GetTestMergeImpl( + TestMergeParameters parameters, + RepositorySettings repositorySettings, + CancellationToken cancellationToken) + { + const string GitLabUrl = "https://gitlab.com"; + + var client = repositorySettings.AccessToken != null + ? new GitLabClient(GitLabUrl, repositorySettings.AccessToken) + : new GitLabClient(GitLabUrl); + + try + { + var mr = await client + .MergeRequests + .GetAsync($"{RemoteRepositoryOwner}/{RemoteRepositoryName}", parameters.Number) + .WithToken(cancellationToken) + .ConfigureAwait(false); + + var revisionToUse = parameters.PullRequestRevision == null + || mr.Sha.StartsWith(parameters.PullRequestRevision, StringComparison.OrdinalIgnoreCase) + ? mr.Sha + : parameters.PullRequestRevision; + + return new Models.TestMerge + { + Author = mr.Author.Username, + BodyAtMerge = mr.Description, + TitleAtMerge = mr.Title, + Comment = parameters.Comment, + Number = parameters.Number, + PullRequestRevision = mr.Sha, + Url = mr.WebUrl + }; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error retrieving merge request metadata!"); + + return new Models.TestMerge + { + Author = ex.Message, + BodyAtMerge = ex.Message, + TitleAtMerge = ex.Message, + Comment = parameters.Comment, + Number = parameters.Number, + PullRequestRevision = parameters.PullRequestRevision, + Url = ex.Message + }; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs new file mode 100644 index 0000000000..92835dd7ad --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Base for implementing . + /// + abstract class GitRemoteFeaturesBase : IGitRemoteFeatures + { + /// + public abstract string TestMergeRefSpecFormatter { get; } + + /// + public abstract string TestMergeLocalBranchNameFormatter { get; } + + /// + public abstract RemoteGitProvider? RemoteGitProvider { get; } + + /// + public abstract string RemoteRepositoryOwner { get; } + + /// + public abstract string RemoteRepositoryName { get; } + + /// + /// The for the . + /// + protected ILogger Logger { get; } + + /// + /// Cache of created s. + /// + readonly Dictionary cachedLookups; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The remote repository . + public GitRemoteFeaturesBase(ILogger logger, Uri remoteUrl) + { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + if (remoteUrl == null) + throw new ArgumentNullException(nameof(remoteUrl)); + + cachedLookups = new Dictionary(); + } + + /// + public async Task GetTestMerge( + TestMergeParameters parameters, + RepositorySettings repositorySettings, + CancellationToken cancellationToken) + { + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + if (repositorySettings == null) + throw new ArgumentNullException(nameof(repositorySettings)); + + Models.TestMerge result; + lock (cachedLookups) + if (cachedLookups.TryGetValue(parameters, out result)) + Logger.LogTrace("Using cache for test merge #{0}", parameters.Number); + + if (result == null) + { + Logger.LogTrace("Retrieving metadata for test merge #{0}...", parameters.Number); + result = await GetTestMergeImpl(parameters, repositorySettings, cancellationToken).ConfigureAwait(false); + lock (cachedLookups) + if (!cachedLookups.TryAdd(parameters, result)) + Logger.LogError("Race condition on adding test merge #{0}!", parameters.Number); + } + + return result; + } + + /// + /// Implementation of + /// + /// The . + /// The . + /// The for the operation. + /// A resulting in the of the . + protected abstract Task GetTestMergeImpl( + TestMergeParameters parameters, + RepositorySettings repositorySettings, + CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs new file mode 100644 index 0000000000..1c061adc6e --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.Logging; +using System; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + sealed class GitRemoteFeaturesFactory : IGitRemoteFeaturesFactory + { + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + public GitRemoteFeaturesFactory( + IGitHubClientFactory gitHubClientFactory, + ILoggerFactory loggerFactory, + ILogger logger) + { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public IGitRemoteFeatures CreateGitRemoteFeatures(IRepository repository) + { + if (repository == null) + throw new ArgumentNullException(nameof(repository)); + + var primaryRemote = repository.Origin; + try + { + var primaryRemoteUrl = new Uri(primaryRemote); + + switch (primaryRemoteUrl.Host.ToUpperInvariant()) + { + case "GITHUB.COM": + case "WWW.GITHUB.COM": + case "GIT.GITHUB.COM": + return new GitHubRemoteFeatures( + gitHubClientFactory, + loggerFactory.CreateLogger(), + primaryRemoteUrl); + case "GITLAB.COM": + case "WWW.GITLAB.COM": + case "GIT.GITLAB.COM": + return new GitLabRemoteFeatures( + loggerFactory.CreateLogger(), + primaryRemoteUrl); + default: + logger.LogTrace("Unknown git remote: {0}", primaryRemoteUrl); + break; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error parsing remote git provider."); + } + + return new DefaultGitRemoteFeatures(); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteAdditionalInformation.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteAdditionalInformation.cs new file mode 100644 index 0000000000..7b4de75b0e --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteAdditionalInformation.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Additional information from git remotes. + /// + public interface IGitRemoteAdditionalInformation : IGitRemoteInformation + { + /// + /// Retrieve the representation of given test merge . + /// + /// The . + /// The . + /// The for the operation. + /// A resulting in the of the . + Task GetTestMerge( + TestMergeParameters parameters, + RepositorySettings repositorySettings, + CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs index d26e9cd562..7dc7502ba7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs @@ -1,15 +1,18 @@ -using Tgstation.Server.Api.Models.Internal; - namespace Tgstation.Server.Host.Components.Repository { /// /// Provides features for remote git services /// - interface IGitRemoteFeatures : IGitRemoteInformation + interface IGitRemoteFeatures : IGitRemoteAdditionalInformation { /// /// Gets a formatter string which creates the remote refspec for fetching the HEAD of passed in pull request number. /// string TestMergeRefSpecFormatter { get; } + + /// + /// Get + /// + string TestMergeLocalBranchNameFormatter { get; } } } diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs new file mode 100644 index 0000000000..31085a16d2 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs @@ -0,0 +1,15 @@ +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Factory for creating . + /// + interface IGitRemoteFeaturesFactory + { + /// + /// Create the for a given . + /// + /// The to create for. + /// A new instance. + IGitRemoteFeatures CreateGitRemoteFeatures(IRepository repository); + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs index 79e20a64fb..086ca8d93e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs @@ -21,8 +21,8 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The full path to the . /// The for the operation. - /// A resulting in a containing the loaded and the associated . - Task> CreateFromPath(string path, CancellationToken cancellationToken); + /// A resulting in the loaded . + Task CreateFromPath(string path, CancellationToken cancellationToken); /// /// Clone a remote . diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 41dd80e15e..be9d6b0b0d 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -2,14 +2,13 @@ using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Host.Components.Repository { /// /// Represents an on-disk git repository /// - public interface IRepository : IGitRemoteInformation, IDisposable + public interface IRepository : IGitRemoteAdditionalInformation, IDisposable { /// /// If tracks an upstream branch diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index bd1f7de84c..137d6be054 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -2,7 +2,6 @@ using LibGit2Sharp; using LibGit2Sharp.Handlers; using Microsoft.Extensions.Logging; using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -36,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public async Task> CreateFromPath(string path, CancellationToken cancellationToken) + public async Task CreateFromPath(string path, CancellationToken cancellationToken) { if (path == null) throw new ArgumentNullException(nameof(path)); @@ -52,42 +51,7 @@ namespace Tgstation.Server.Host.Components.Repository TaskScheduler.Current) .ConfigureAwait(false); - try - { - var remoteFeatures = CreateGitRemoteFeatures(repo); - return Tuple.Create(repo, remoteFeatures); - } - catch - { - repo.Dispose(); - throw; - } - } - - IGitRemoteFeatures CreateGitRemoteFeatures(LibGit2Sharp.IRepository repo) - { - var primaryRemote = repo.Network.Remotes.First(); - var primaryRemoteUrl = new Uri(primaryRemote.Url); - - try - { - switch (primaryRemoteUrl.Host.ToUpperInvariant()) - { - case "GITHUB.COM": - case "WWW.GITHUB.COM": - case "GIT.GITHUB.COM": - return new GitHubRemoteFeatures(primaryRemoteUrl); - default: - logger.LogTrace("Unknown git remote: {0}", primaryRemoteUrl); - break; - } - } - catch (Exception ex) - { - logger.LogWarning(ex, "Error parsing remote git provider."); - } - - return new DefaultGitRemoteFeatures(); + return repo; } /// diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 6c752c0cf6..3d83a36dbb 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -123,7 +124,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of /// The value of /// The value of - /// The value of . + /// The to provide the value of . /// The value of /// The value if public Repository( @@ -132,7 +133,7 @@ namespace Tgstation.Server.Host.Components.Repository IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, - IGitRemoteFeatures gitRemoteFeatures, + IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILogger logger, Action onDispose) { @@ -141,9 +142,13 @@ namespace Tgstation.Server.Host.Components.Repository this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); - this.gitRemoteFeatures = gitRemoteFeatures ?? throw new ArgumentNullException(nameof(gitRemoteFeatures)); + if (gitRemoteFeaturesFactory == null) + throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); + + gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this); } /// @@ -299,7 +304,7 @@ namespace Tgstation.Server.Host.Components.Repository testMergeParameters.Comment ?? String.Empty); var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", testMergeParameters.Number); - var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", testMergeParameters.Number, prBranchName); + var localBranchName = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeLocalBranchNameFormatter, testMergeParameters.Number, prBranchName); var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, prBranchName); var refSpecList = new List { refSpec }; @@ -774,5 +779,14 @@ namespace Tgstation.Server.Host.Components.Repository return false; }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); + + /// + public Task GetTestMerge( + TestMergeParameters parameters, + RepositorySettings repositorySettings, + CancellationToken cancellationToken) => gitRemoteFeatures.GetTestMerge( + parameters, + repositorySettings, + cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index c82b56e22a..6e025f8b6f 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -40,6 +40,11 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IEventConsumer eventConsumer; + /// + /// The for the + /// + readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + /// /// The created s /// @@ -62,6 +67,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of . /// The value of /// The value of + /// The value of . /// The value of /// The value of public RepositoryManager( @@ -69,6 +75,7 @@ namespace Tgstation.Server.Host.Components.Repository ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, + IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILogger repositoryLogger, ILogger logger) { @@ -76,6 +83,7 @@ namespace Tgstation.Server.Host.Components.Repository this.commands = commands ?? throw new ArgumentNullException(nameof(commands)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); semaphore = new SemaphoreSlim(1); @@ -188,32 +196,21 @@ namespace Tgstation.Server.Host.Components.Repository { try { - var repoTuple = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false); + var libGit2Repo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false); - try - { - var libGit2Repo = repoTuple.Item1; - var gitRemoteFeatures = repoTuple.Item2; - - return new Repository( - libGit2Repo, - commands, - ioManager, - eventConsumer, - repositoryFactory, - gitRemoteFeatures, - repositoryLogger, - () => - { - logger.LogTrace("Releasing semaphore due to Repository disposal..."); - semaphore.Release(); - }); - } - catch - { - repoTuple.Item1.Dispose(); - throw; - } + return new Repository( + libGit2Repo, + commands, + ioManager, + eventConsumer, + repositoryFactory, + gitRemoteFeaturesFactory, + repositoryLogger, + () => + { + logger.LogTrace("Releasing semaphore due to Repository disposal..."); + semaphore.Release(); + }); } catch { diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 1b77f22bf6..c83393aa98 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -17,7 +17,6 @@ using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -644,7 +643,6 @@ namespace Tgstation.Server.Host.Controllers } // test merging - Dictionary prMap = null; if (newTestMerges) { if (repo.RemoteGitProvider == RemoteGitProvider.Unknown) @@ -666,10 +664,6 @@ namespace Tgstation.Server.Host.Controllers bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { - // In order for this to work though we need the shas of all the commits - if (model.NewTestMerges.Any(x => x.PullRequestRevision == null)) - prMap = new Dictionary(); - bool cantSearch = false; foreach (var I in model.NewTestMerges) { @@ -681,11 +675,10 @@ namespace Tgstation.Server.Host.Controllers try { // retrieve the latest sha - var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number) - .WithToken(ct) - .ConfigureAwait(false); - prMap.Add(I.Number, pr); - I.PullRequestRevision = pr.Head.Sha; + var pr = await repo.GetTestMerge(I, currentModel, ct).ConfigureAwait(false); + + // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it + I.PullRequestRevision = pr.PullRequestRevision; } catch { @@ -798,47 +791,10 @@ namespace Tgstation.Server.Host.Controllers { foreach (var I in model.NewTestMerges) { - Octokit.PullRequest pr = null; - string errorMessage = null; - if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number)) throw new JobException(ErrorCode.RepoDuplicateTestMerge); - Exception exception = null; - try - { - // load from cache if possible - if (prMap == null || !prMap.TryGetValue(I.Number, out pr)) - pr = await gitHubClient - .PullRequest - .Get(repoOwner, repoName, I.Number) - .WithToken(ct) - .ConfigureAwait(false); - } - catch (Octokit.RateLimitExceededException ex) - { - // you look at your anonymous access and sigh - errorMessage = "REMOTE API ERROR: RATE LIMITED"; - exception = ex; - } - catch (Octokit.AuthorizationException ex) - { - errorMessage = "REMOTE API ERROR: BAD CREDENTIALS"; - exception = ex; - } - catch (Octokit.NotFoundException ex) - { - // you look at your shithub and sigh - errorMessage = "REMOTE API ERROR: PULL REQUEST NOT FOUND"; - exception = ex; - } - - if (exception != null) - Logger.LogWarning(exception, "Error retrieving pull request metadata!"); - - // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it - if (I.PullRequestRevision == null && pr != null) - I.PullRequestRevision = pr.Head.Sha; + var fullTestMergeTask = repo.GetTestMerge(I, currentModel, ct); var mergeResult = await repo.AddTestMerge( I, @@ -849,28 +805,38 @@ namespace Tgstation.Server.Host.Controllers NextProgressReporter(), ct).ConfigureAwait(false); - if (!mergeResult.HasValue) + if (mergeResult == null) throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!")); - ++doneSteps; + Models.TestMerge fullTestMerge; + try + { + fullTestMerge = await fullTestMergeTask.ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning("Error retrieving metadata for test merge #{0}!", I.Number); + + fullTestMerge = new Models.TestMerge + { + Author = ex.Message, + BodyAtMerge = ex.Message, + MergedAt = DateTimeOffset.Now, + TitleAtMerge = ex.Message, + Comment = I.Comment, + Number = I.Number, + PullRequestRevision = I.PullRequestRevision, + Url = ex.Message + }; + } // MergedBy will be set later - var tm = new Models.TestMerge - { - Author = pr?.User.Login ?? errorMessage, - BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty, - MergedAt = DateTimeOffset.Now, - TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, - Comment = I.Comment, - Number = I.Number, - PullRequestRevision = I.PullRequestRevision, - Url = pr?.HtmlUrl ?? errorMessage - }; + ++doneSteps; - await UpdateRevInfo(tm).ConfigureAwait(false); + await UpdateRevInfo(fullTestMerge).ConfigureAwait(false); } } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index cf80be7bfd..071380e041 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -306,24 +306,23 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); } - // configure misc services + // configure component/misc services services.AddScoped(); services.AddTransient, LimitedFileStreamResultExecutor>(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(x => x.GetRequiredService()); - services.AddSingleton(x => x.GetRequiredService()); - - // configure component services + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); // configure root services services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index 134993f14c..d2ba919450 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -1,9 +1,11 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { /// + #pragma warning disable CA1724 // naming conflict with gitlab package public sealed class Job : Api.Models.Internal.Job + #pragma warning restore CA1724 { /// /// See diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index f8d5296d9d..f2de7240e3 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -65,6 +65,7 @@ + diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index 3fcb973507..43b4c66124 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -20,8 +20,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests string path, ILibGit2RepositoryFactory repositoryFactory = null) => (await (repositoryFactory ?? CreateFactory()) - .CreateFromPath(path, default)) - .Item1; + .CreateFromPath(path, default)); [TestMethod] public void TestConstructionThrows() => Assert.ThrowsException(() => new LibGit2RepositoryFactory(null)); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 9aa2f223a2..d0d50482eb 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -480,7 +480,7 @@ namespace Tgstation.Server.Tests Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of(), + Mock.Of(), Mock.Of>(), () => { }); From 3b4bd2d703adbd4fc97a9b17476b4a497b05002a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 12:34:09 -0500 Subject: [PATCH 021/154] Adds URL setting to OAuthConfiguration --- README.md | 7 +++- .../Configuration/OAuthConfiguration.cs | 32 ++-------------- .../Configuration/OAuthConfigurationBase.cs | 37 +++++++++++++++++++ .../Security/OAuth/DiscordTokenRequest.cs | 4 +- .../Security/OAuth/OAuthTokenRequest.cs | 4 +- .../Security/OAuth/TGForumsOAuthValidator.cs | 2 +- 6 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs diff --git a/README.md b/README.md index 922fbab65d..c2cb3d211d 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,16 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: ```json "GitHubOAuth":{ - "ClientId": "... (Note for `TGForums`, this is the redirect_uri used)", + "Url": "...", (Used with certain providers) + "ClientId": "...", "ClientSecret": "..." } ``` +The following providers use the `Url` setting: + +- `TGForums`: Used as the OAuth redirect url. + ### Database Configuration If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk. diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs index b0dc12fc21..5d865c76ed 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -1,37 +1,13 @@ -using System; - namespace Tgstation.Server.Host.Configuration { /// - /// OAuth options. + /// OAuth configuration options. /// - class OAuthConfiguration + sealed class OAuthConfiguration : OAuthConfigurationBase { /// - /// The client ID. + /// The redirect or server URL. Not used by all providers. /// - public string ClientId { get; set; } - - /// - /// The client secret. - /// - public string ClientSecret { get; set; } - - /// - /// Initializes a new instance of the . - /// - public OAuthConfiguration() { } - - /// - /// Initializes a new instance of the . - /// - /// The to copy settings from. - public OAuthConfiguration(OAuthConfiguration oAuthConfiguration) - { - if (oAuthConfiguration == null) - throw new ArgumentNullException(nameof(oAuthConfiguration)); - ClientId = oAuthConfiguration.ClientId; - ClientSecret = oAuthConfiguration.ClientSecret; - } + public string Url { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs new file mode 100644 index 0000000000..6fb4fda5fb --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -0,0 +1,37 @@ +using System; + +namespace Tgstation.Server.Host.Configuration +{ + /// + /// Base OAuth options. + /// + abstract class OAuthConfigurationBase + { + /// + /// The client ID. + /// + public string ClientId { get; set; } + + /// + /// The client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Initializes a new instance of the . + /// + public OAuthConfigurationBase() { } + + /// + /// Initializes a new instance of the . + /// + /// The to copy settings from. + public OAuthConfigurationBase(OAuthConfigurationBase oAuthConfiguration) + { + if (oAuthConfiguration == null) + throw new ArgumentNullException(nameof(oAuthConfiguration)); + ClientId = oAuthConfiguration.ClientId; + ClientSecret = oAuthConfiguration.ClientSecret; + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs index fdd6dcc393..a3ee43c704 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs @@ -21,9 +21,9 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Initializes a new instance of the . /// - /// The for the . + /// The for the . /// The OAuth code for the . - public DiscordTokenRequest(OAuthConfiguration oAuthConfiguration, string code) + public DiscordTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code) : base(oAuthConfiguration, code) { GrantType = "authorization_code"; diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs index 7f03023625..1db7a8f692 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Generic OAuth token request. /// - class OAuthTokenRequest : OAuthConfiguration + class OAuthTokenRequest : OAuthConfigurationBase { /// /// The OAuth code. @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// The to build from. /// The OAuth code received from the browser. - public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code) + public OAuthTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code) : base(oAuthConfiguration) { Code = code ?? throw new ArgumentNullException(nameof(code)); diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index afdd635974..775551af97 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Security.OAuth { UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_create_session.php") { - Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.ClientId)}" + Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.Url)}" }; using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); From 3168eded05065353d3bcd6e1347ad2e6174ff176 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 12:40:04 -0500 Subject: [PATCH 022/154] Refactors Scope into base OAuthTokenRequest --- .../Security/OAuth/DiscordTokenRequest.cs | 8 +------- .../Security/OAuth/OAuthTokenRequest.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs index a3ee43c704..cf477ba156 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs @@ -13,21 +13,15 @@ namespace Tgstation.Server.Host.Security.OAuth /// public string GrantType { get; } - /// - /// The 'scope' field. - /// - public string Scope { get; } - /// /// Initializes a new instance of the . /// /// The for the . /// The OAuth code for the . public DiscordTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code) - : base(oAuthConfiguration, code) + : base(oAuthConfiguration, code, "identify") { GrantType = "authorization_code"; - Scope = "identify"; } } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs index 1db7a8f692..cad2885746 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -9,19 +9,26 @@ namespace Tgstation.Server.Host.Security.OAuth class OAuthTokenRequest : OAuthConfigurationBase { /// - /// The OAuth code. + /// The OAuth code received from the browser. /// public string Code { get; } /// - /// Initializes a new instance of the + /// The scopes being requested. + /// + public string Scope { get; } + + /// + /// Initializes a new instance of the . /// /// The to build from. - /// The OAuth code received from the browser. - public OAuthTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code) + /// The value of . + /// The value of + public OAuthTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code, string scope) : base(oAuthConfiguration) { Code = code ?? throw new ArgumentNullException(nameof(code)); + Scope = scope ?? throw new ArgumentNullException(nameof(scope)); } } } From d524d06b5a932c2cd45b5fef07ead3aa2f44091b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 12:52:33 -0500 Subject: [PATCH 023/154] Add URL OAuth config to integration tests --- tests/Tgstation.Server.Tests/TestingServer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 7eee0642a1..3459c2a11a 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -93,6 +93,7 @@ namespace Tgstation.Server.Tests { args.Add($"Security:OAuth:{I}:ClientId=Fake"); args.Add($"Security:OAuth:{I}:ClientSecret=Faker"); + args.Add($"Security:OAuth:{I}:Url=https://fakest.com"); } // SPECIFICALLY DELETE THE DEV APPSETTINGS, WE DON'T WANT IT IN THE WAY From a7026df1956e852a8210350d40272d2337043cff Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 13:13:35 -0500 Subject: [PATCH 024/154] Fix being unable to read current OAuthConnections --- src/Tgstation.Server.Host/Models/User.cs | 4 +++- .../Security/AuthenticationContextFactory.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 5d4783dcf0..547df857df 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; namespace Tgstation.Server.Host.Models { @@ -75,7 +76,8 @@ namespace Tgstation.Server.Host.Models Id = Id, InstanceManagerRights = showDetails ? InstanceManagerRights : null, Name = Name, - SystemIdentifier = showDetails ? SystemIdentifier : null + SystemIdentifier = showDetails ? SystemIdentifier : null, + OAuthConnections = OAuthConnections?.Select(x => x.ToApi()).ToList(), }; /// diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index d74ef23d4a..04bf2e2062 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -60,6 +60,7 @@ namespace Tgstation.Server.Host.Security .AsQueryable() .Where(x => x.Id == userId) .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (user == default) From 36044c826c3b3f4aa207a1395d82295ef7329273 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 13:14:29 -0500 Subject: [PATCH 025/154] Use string enums for OAuthProviders in the API --- docs/API.dox | 6 +++--- src/Tgstation.Server.Api/Models/OAuthConnection.cs | 3 +++ src/Tgstation.Server.Api/Tgstation.Server.Api.csproj | 1 + src/Tgstation.Server.Client/Tgstation.Server.Client.csproj | 1 - 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/API.dox b/docs/API.dox index f33f901cb2..c4cec21c15 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -144,9 +144,9 @@ You will be granted a bearer token as in basic auth. This will have an extended @subsubsection api_auth_o_providers Supported Providers -- ID: 0, Name: GitHub, Documentation: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ -- ID: 1, Name: Discord, Documentation: https://discord.com/developers/docs/topics/oauth2 -- ID: 2, Name: TGForums, Documentation: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 +- GitHub: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ +- Discord: https://discord.com/developers/docs/topics/oauth2 +- TGForums: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 @section api_perms Permissions diff --git a/src/Tgstation.Server.Api/Models/OAuthConnection.cs b/src/Tgstation.Server.Api/Models/OAuthConnection.cs index d684a04e2e..f68f7d2f16 100644 --- a/src/Tgstation.Server.Api/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Api/Models/OAuthConnection.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models @@ -10,6 +12,7 @@ namespace Tgstation.Server.Api.Models /// /// The of the . /// ] + [JsonConverter(typeof(StringEnumConverter))] [EnumDataType(typeof(OAuthProvider))] public OAuthProvider Provider { get; set; } diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 7b61d94fec..f971e5b548 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -37,6 +37,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 3070b75011..4ceb1f0844 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -31,7 +31,6 @@ - all runtime; build; native; contentfiles; analyzers From 3147ffd2bfe7cd7bde8a23c673b0e4912b482d63 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 14:06:43 -0500 Subject: [PATCH 026/154] Oauth cleanups --- README.md | 13 ++++++--- docs/API.dox | 8 +++--- .../Models/OAuthProvider.cs | 5 ++++ .../Models/OAuthProviderInfo.cs | 20 ++++++++++++++ .../Models/ServerInformation.cs | 4 +-- .../Configuration/OAuthConfiguration.cs | 11 ++++++-- .../Controllers/HomeController.cs | 2 +- .../Security/OAuth/BaseOAuthValidator.cs | 2 +- .../Security/OAuth/DiscordOAuthValidator.cs | 2 +- .../Security/OAuth/DiscordTokenRequest.cs | 27 ------------------- .../Security/OAuth/GenericOAuthValidator.cs | 8 +++++- .../Security/OAuth/GitHubOAuthValidator.cs | 12 +++++++-- .../Security/OAuth/IOAuthProviders.cs | 4 +-- .../Security/OAuth/IOAuthValidator.cs | 4 +-- .../Security/OAuth/OAuthProviders.cs | 6 ++--- .../Security/OAuth/OAuthTokenRequest.cs | 17 ++++++++++-- .../Security/OAuth/TGForumsOAuthValidator.cs | 15 ++++++++--- 17 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs delete mode 100644 src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs diff --git a/README.md b/README.md index c2cb3d211d..26a84e81dd 100644 --- a/README.md +++ b/README.md @@ -133,15 +133,20 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: ```json "GitHubOAuth":{ - "Url": "...", (Used with certain providers) "ClientId": "...", - "ClientSecret": "..." + "ClientSecret": "...", + "RedirectUrl": "...", (Used with certain providers) + "ServerUrl": "...", (Used with certain providers) } ``` +The following providers use the `RedirectUrl` setting: -The following providers use the `Url` setting: +- GitHub +- TGForums -- `TGForums`: Used as the OAuth redirect url. +The following providers use the `ServerUrl` setting: + +- None so far ### Database Configuration diff --git a/docs/API.dox b/docs/API.dox index c4cec21c15..5507fa7f83 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -63,7 +63,7 @@ TGS will only every return the response codes listed here - 204: No Content. Identical to 200 with no response body. - 400: Bad Request. The response body will contain an @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error - 401: Unauthorized. Invalid or expired credentials were provided. Check rights APIs for updates. See @ref api_auth for details -- 403: Forbidden. User tried to make a request they were not allowed to perform. +- 403: Forbidden. User tried to make a request they were not allowed to perform. - 404: Not found. A resource was requested that had never existed. In the case of retrieving a resource by ID, it could potentially exist in the future - 406: Not Acceptable. Consequence of failing to provide an Accept header - 408: Request Timeout. The client took to long to continue a request @@ -97,7 +97,7 @@ Other fields may be present in the Version model but should be ignored. See a de @section api_auth Authentication -Every request made to TGS requires authentication. It is provided in the form of the Authorization header. +Every request made to TGS requires authentication. It is provided in the form of the Authorization header. The first request made to TGS must be to login the user @@ -128,7 +128,7 @@ TGS4 supports OAuth 2.0 with select providers for authentication. The flow for this is as follows: -- Retrieve the @ref api_ver to find out available OAuth providers and their respective client IDs. +- Retrieve the @ref api_ver to find out available OAuth providers and their respective client ID and redirect URIs. - Send the user to the Authorization Request endpoint for the provider using the client ID from above. See https://tools.ietf.org/html/rfc6749#section-4.1.1. DO NOT specify a redirect URI, this should be configured in the provider. - Retrieve the authorization response code after successfully completing the authorize step above. - Perform the following request: @@ -391,7 +391,7 @@ If the server detects a set of @ref Tgstation.Server.Api.Models.TestMergeParamet @subsubsection api_repounsetauth Unsetting Authentication -The repository uses the @ref Tgstation.Server.Api.Models.Repository.AccessUser and @ref Tgstation.Server.Api.Models.Repository.AccessToken credentials to access the remote repository if these fields are set. To unset them you must set both of them to an empty string like so +The repository uses the @ref Tgstation.Server.Api.Models.Repository.AccessUser and @ref Tgstation.Server.Api.Models.Repository.AccessToken credentials to access the remote repository if these fields are set. To unset them you must set both of them to an empty string like so @code{.json} { diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs index 8d5a9eb354..0031ed0f79 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProvider.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -19,5 +19,10 @@ namespace Tgstation.Server.Api.Models /// https://tgstation13.org /// TGForums, + + /// + /// https://www.keycloak.org + /// + Keycloak, } } diff --git a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs new file mode 100644 index 0000000000..10466c3204 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs @@ -0,0 +1,20 @@ +using System; + +namespace Tgstation.Server.Api.Models +{ + /// + /// Public information about a given . + /// + public sealed class OAuthProviderInfo + { + /// + /// The client ID. + /// + public string? ClientId { get; set; } + + /// + /// The redirect URL. + /// + public Uri? RedirectUri { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs index a2d8958845..2111302237 100644 --- a/src/Tgstation.Server.Api/Models/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs @@ -24,8 +24,8 @@ namespace Tgstation.Server.Api.Models public Version? DMApiVersion { get; set; } /// - /// Map of to the server's associated client IDs for them. + /// Map of to the for them. /// - public IDictionary? OAuthProviderClientIds { get; set; } + public IDictionary? OAuthProviderInfos { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs index 5d865c76ed..b12e239911 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -1,3 +1,5 @@ +using System; + namespace Tgstation.Server.Host.Configuration { /// @@ -6,8 +8,13 @@ namespace Tgstation.Server.Host.Configuration sealed class OAuthConfiguration : OAuthConfigurationBase { /// - /// The redirect or server URL. Not used by all providers. + /// The client redirect URL. Not used by all providers. /// - public string Url { get; set; } + public Uri ServerUrl { get; set; } + + /// + /// The authentication server URL. Not used by all providers. + /// + public Uri RedirectUrl { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 9d09033105..fd4dd4d7f8 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -167,7 +167,7 @@ namespace Tgstation.Server.Host.Controllers InstanceLimit = generalConfiguration.InstanceLimit, UserLimit = generalConfiguration.UserLimit, ValidInstancePaths = generalConfiguration.ValidInstancePaths, - OAuthProviderClientIds = await oAuthProviders.ClientIds(cancellationToken).ConfigureAwait(false) + OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken).ConfigureAwait(false) }); } diff --git a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs index 68cc17cf23..4cb1995f59 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public abstract Task GetClientId(CancellationToken cancellationToken); + public abstract Task GetProviderInfo(CancellationToken cancellationToken); /// public abstract Task ValidateResponseCode(string code, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs index f0fe8591f5..37d055d08b 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.Security.OAuth protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me"); /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new DiscordTokenRequest(OAuthConfiguration, code); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "identify"); /// protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs deleted file mode 100644 index cf477ba156..0000000000 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Tgstation.Server.Host.Configuration; - -namespace Tgstation.Server.Host.Security.OAuth -{ - /// - /// for Discord. - /// - /// See https://discord.com/developers/docs/topics/oauth2 - sealed class DiscordTokenRequest : OAuthTokenRequest - { - /// - /// The 'grant_type' field. - /// - public string GrantType { get; } - - /// - /// Initializes a new instance of the . - /// - /// The for the . - /// The OAuth code for the . - public DiscordTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code) - : base(oAuthConfiguration, code, "identify") - { - GrantType = "authorization_code"; - } - } -} diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index 8deff22537..98294d46d6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; @@ -119,6 +120,11 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public override Task GetClientId(CancellationToken cancellationToken) => Task.FromResult(OAuthConfiguration.ClientId); + public override Task GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult( + new OAuthProviderInfo + { + ClientId = OAuthConfiguration.ClientId, + RedirectUri = OAuthConfiguration.RedirectUrl + }); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index b4ee1354e6..c678568ebe 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -65,7 +65,10 @@ namespace Tgstation.Server.Host.Security.OAuth new OauthTokenRequest( oAuthConfiguration.ClientId, oAuthConfiguration.ClientSecret, - code)) + code) + { + RedirectUri = oAuthConfiguration.RedirectUrl + }) .ConfigureAwait(false); var token = response.AccessToken; @@ -94,6 +97,11 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public Task GetClientId(CancellationToken cancellationToken) => Task.FromResult(oAuthConfiguration.ClientId); + public Task GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult( + new OAuthProviderInfo + { + ClientId = oAuthConfiguration.ClientId, + RedirectUri = oAuthConfiguration.RedirectUrl + }); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index 66af75b5af..06cae4e96e 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// Gets a of the provider client IDs. /// /// The for the operation. - /// A resulting in a anew of the active provider client IDs. - Task> ClientIds(CancellationToken cancellationToken); + /// A resulting in a anew of the active s. + Task> ProviderInfos(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index be33dcb992..be8df49788 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -15,11 +15,11 @@ namespace Tgstation.Server.Host.Security.OAuth OAuthProvider Provider { get; } /// - /// Gets the OAuth client ID of validator. + /// Gets the of validator. /// /// The for the operation. /// A resulting in the client ID of the validator on success, on failure. - Task GetClientId(CancellationToken cancellationToken); + Task GetProviderInfo(CancellationToken cancellationToken); /// /// Validate a given OAuth response . diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 91be97f0e2..9087aad25b 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), discordConfig)); - if(securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig)) + if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig)) validatorsBuilder.Add( new TGForumsOAuthValidator( httpClientFactory, @@ -73,11 +73,11 @@ namespace Tgstation.Server.Host.Security.OAuth public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); /// - public async Task> ClientIds(CancellationToken cancellationToken) + public async Task> ProviderInfos(CancellationToken cancellationToken) { var providersAndTasks = validators.ToDictionary( x => x.Provider, - x => x.GetClientId(cancellationToken)); + x => x.GetProviderInfo(cancellationToken)); await Task.WhenAll(providersAndTasks.Values).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs index cad2885746..eb715953c6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Generic OAuth token request. /// - class OAuthTokenRequest : OAuthConfigurationBase + sealed class OAuthTokenRequest : OAuthConfigurationBase { /// /// The OAuth code received from the browser. @@ -18,17 +18,30 @@ namespace Tgstation.Server.Host.Security.OAuth /// public string Scope { get; } + /// + /// The OAuth redirect URI. + /// + public Uri RedirectUri { get; } + + /// + /// The OAuth grant type. + /// + public string GrantType { get; } + /// /// Initializes a new instance of the . /// /// The to build from. /// The value of . /// The value of - public OAuthTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code, string scope) + public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code, string scope) : base(oAuthConfiguration) { Code = code ?? throw new ArgumentNullException(nameof(code)); Scope = scope ?? throw new ArgumentNullException(nameof(scope)); + + RedirectUri = oAuthConfiguration.RedirectUrl; + GrantType = "authorization_code"; } } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index 775551af97..4e6028987e 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public override async Task GetClientId(CancellationToken cancellationToken) + public override async Task GetProviderInfo(CancellationToken cancellationToken) { var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.Now); if (expiredSessions > 0) @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Security.OAuth { UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_create_session.php") { - Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.Url)}" + Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.RedirectUrl.ToString())}" }; using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); @@ -83,8 +83,15 @@ namespace Tgstation.Server.Host.Security.OAuth return null; } - sessions.Add(Tuple.Create(newSession, DateTimeOffset.Now)); - return newSession.SessionPublicToken; + sessions.Add( + Tuple.Create( + newSession, + DateTimeOffset.Now)); + return new OAuthProviderInfo + { + ClientId = newSession.SessionPublicToken, + RedirectUri = OAuthConfiguration.RedirectUrl + }; } catch (Exception ex) { From 4a070e39a7fe4c1c3208bce71d526cd7997b4f89 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 14:13:09 -0500 Subject: [PATCH 027/154] Adds a single log line --- .../Security/OAuth/GenericOAuthValidator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index 98294d46d6..ad0de1e980 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -96,7 +96,10 @@ namespace Tgstation.Server.Host.Security.OAuth var accessToken = DecodeTokenPayload(tokenResponseJson); if (accessToken == null) + { + Logger.LogTrace("No token from DecodeTokenPayload!"); return null; + } Logger.LogTrace("Getting user details..."); using var userInformationRequest = new HttpRequestMessage(HttpMethod.Get, UserInformationUrl); From 73a88245b17605c9d9cb49d6592e417876f30f2b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 14:14:11 -0500 Subject: [PATCH 028/154] Adds Keycloak OAuth support --- README.md | 3 +- docs/API.dox | 1 + .../Security/OAuth/KeycloakOAuthValidator.cs | 54 +++++++++++++++++++ .../Security/OAuth/OAuthProviders.cs | 8 +++ src/Tgstation.Server.Host/appsettings.json | 3 +- 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs diff --git a/README.md b/README.md index 26a84e81dd..841fdb4e12 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,11 @@ The following providers use the `RedirectUrl` setting: - GitHub - TGForums +- Keycloak The following providers use the `ServerUrl` setting: -- None so far +- Keycloak ### Database Configuration diff --git a/docs/API.dox b/docs/API.dox index 5507fa7f83..927d3cd0c5 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -147,6 +147,7 @@ You will be granted a bearer token as in basic auth. This will have an extended - GitHub: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ - Discord: https://discord.com/developers/docs/topics/oauth2 - TGForums: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 +- Keycloak: https://plugins.miniorange.com/keycloak-single-sign-on-wordpress-sso-oauth-openid-connect @section api_perms Permissions diff --git a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs new file mode 100644 index 0000000000..61e08a56ab --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// OAuth validator for Keycloak. + /// + sealed class KeycloakOAuthValidator : GenericOAuthValidator + { + /// + public override OAuthProvider Provider => OAuthProvider.Keycloak; + + /// + protected override Uri TokenUrl => new Uri($"{BaseProtocolPath}/token"); + + /// + protected override Uri UserInformationUrl => new Uri($"{BaseProtocolPath}/userinfo"); + + /// + /// Base path to the server's OAuth endpoint. + /// + string BaseProtocolPath => $"{OAuthConfiguration.ServerUrl}/protocol/openid-connect"; + + /// + /// Initializes a new instance of the . + /// + /// The for the . + /// The for the . + /// The for the . + /// The for the . + public KeycloakOAuthValidator( + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + OAuthConfiguration oAuthConfiguration) + : base(httpClientFactory, assemblyInformationProvider, logger, oAuthConfiguration) + { + } + + /// + protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "openid"); + + /// + protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; + + /// + protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.sub; + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 9087aad25b..e8133706b2 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -66,6 +66,14 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), tgConfig)); + if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Keycloak, out var keyCloakConfig)) + validatorsBuilder.Add( + new KeycloakOAuthValidator( + httpClientFactory, + assemblyInformationProvider, + loggerFactory.CreateLogger(), + keyCloakConfig)); + validators = validatorsBuilder; } diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 00506b5762..b0df570838 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -58,7 +58,8 @@ "OAuth": { "GitHub": null, "Discord": null, - "TGForums": null + "TGForums": null, + "Keycloak": null } } } From 20f55160bd307f42e896940a8d868be1c384d6ce Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 14:19:23 -0500 Subject: [PATCH 029/154] Removes an annoying trailing `/` --- docs/API.dox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.dox b/docs/API.dox index 927d3cd0c5..0a27032d8e 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -144,7 +144,7 @@ You will be granted a bearer token as in basic auth. This will have an extended @subsubsection api_auth_o_providers Supported Providers -- GitHub: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ +- GitHub: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps - Discord: https://discord.com/developers/docs/topics/oauth2 - TGForums: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 - Keycloak: https://plugins.miniorange.com/keycloak-single-sign-on-wordpress-sso-oauth-openid-connect From e2da8b53716722eda165524d9febc55c487291e4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 15:34:05 -0500 Subject: [PATCH 030/154] Fix setting TGS4_GITHUB_REF for PRs --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 15c75bb15d..c316b5b649 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -203,7 +203,7 @@ jobs: - name: Set TGS4_GITHUB_REF for PR if: ${{ github.event_name == 'pull_request' }} - run: echo "TGS4_GITHUB_REF=${{ github.event.base_ref }}" >> $env:GITHUB_ENV + run: echo "TGS4_GITHUB_REF=${{ github.base_ref }}" >> $env:GITHUB_ENV - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} From 3716c84664d0e50cb464b785e60e4442c023fa70 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 15:46:18 -0500 Subject: [PATCH 031/154] Fix a potential test error dropping --- tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs index 3bf72f06e1..32c94dc8ae 100644 --- a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs @@ -22,7 +22,6 @@ namespace Tgstation.Server.Tests.Instance public async Task Run(Task repositoryTask, CancellationToken cancellationToken) { - Assert.IsFalse(repositoryTask.IsCompleted); var deployJob = await dreamMakerClient.Compile(cancellationToken); deployJob = await WaitForJob(deployJob, 30, true, null, cancellationToken); Assert.IsTrue(deployJob.ErrorCode == ErrorCode.RepoCloning || deployJob.ErrorCode == ErrorCode.RepoMissing); From d21143da4a3df99b6e9063ef859f4f152578cb87 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 16:12:14 -0500 Subject: [PATCH 032/154] Fix test branch parsing --- .../Instance/RepositoryTest.cs | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index 02b3251622..1bdb2bb7fb 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -22,28 +22,13 @@ namespace Tgstation.Server.Tests.Instance public async Task RunPreWatchdog(CancellationToken cancellationToken) { - const string GitHubRef = "TGS4_GITHUB_REF"; - var branchSourceEnvVars = new List - { - "TGS4_TEST_BRANCH", - "APPVEYOR_REPO_BRANCH", - "TRAVIS_BRANCH", - GitHubRef - }; - + const string TestRefEnvVar = "TGS4_GITHUB_REF"; + var envVar = Environment.GetEnvironmentVariable(TestRefEnvVar); string workingBranch = null; - foreach (var envVarName in branchSourceEnvVars) + if (!String.IsNullOrWhiteSpace(envVar)) { - var envVar = Environment.GetEnvironmentVariable(envVarName); - if (!String.IsNullOrWhiteSpace(envVar)) - { - if(envVarName == GitHubRef) - envVar = envVar.Substring("refs/heads/".Length); - - workingBranch = envVar; - Console.WriteLine($"TEST: Set working branch to '{workingBranch}' from env var '{envVarName}'"); - break; - } + workingBranch = envVar; + Console.WriteLine($"TEST: Set working branch to '{workingBranch}' from env var '{TestRefEnvVar}'"); } if (workingBranch == null) From daebe5eac893d79f067690f6c99bda1c1b027fab Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 16:48:39 -0500 Subject: [PATCH 033/154] War on TGS4_GITHUB_REF EP4 --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index c316b5b649..474aabc0f1 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -345,7 +345,7 @@ jobs: - name: Set TGS4_GITHUB_REF for PR if: ${{ github.event_name == 'pull_request' }} - run: echo "TGS4_GITHUB_REF=${{ github.event.base_ref }}" >> $GITHUB_ENV + run: echo "TGS4_GITHUB_REF=${{ github.base_ref }}" >> $GITHUB_ENV - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} From c5b654bf9b21d141621a004fae255447fce2331f Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 22:00:35 -0500 Subject: [PATCH 034/154] Fix OAuthProvider not being spec'd as strings --- src/Tgstation.Server.Api/Models/OAuthConnection.cs | 2 -- src/Tgstation.Server.Api/Models/OAuthProvider.cs | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/OAuthConnection.cs b/src/Tgstation.Server.Api/Models/OAuthConnection.cs index f68f7d2f16..99f5e4d05e 100644 --- a/src/Tgstation.Server.Api/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Api/Models/OAuthConnection.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models @@ -12,7 +11,6 @@ namespace Tgstation.Server.Api.Models /// /// The of the . /// ] - [JsonConverter(typeof(StringEnumConverter))] [EnumDataType(typeof(OAuthProvider))] public OAuthProvider Provider { get; set; } diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs index 0031ed0f79..cf9adbd91a 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProvider.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -1,8 +1,12 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + namespace Tgstation.Server.Api.Models { /// /// List of OAuth providers supported by TGS /// + [JsonConverter(typeof(StringEnumConverter))] public enum OAuthProvider { /// From 778ebc91b07df10a80a4d6bd8e2c9b94258b34a3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 7 Dec 2020 23:46:52 -0500 Subject: [PATCH 035/154] Fix unauth'd / requests failing on non-browsers --- src/Tgstation.Server.Host/Controllers/HomeController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index fd4dd4d7f8..69ed0cc0d7 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -109,8 +109,7 @@ namespace Tgstation.Server.Host.Controllers databaseContext, authenticationContextFactory, logger, - (browserResolver ?? throw new ArgumentNullException(nameof(browserResolver))).Browser.Type != BrowserType.Generic - && !(controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions))).Enable) + false) { this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); From f87f609e380067dfe373e0b77478e72cb019a63b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 8 Dec 2020 17:44:40 -0500 Subject: [PATCH 036/154] Add serverUrl to OAuthProviderInfo --- docs/API.dox | 2 +- src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs | 5 +++++ .../Security/OAuth/GenericOAuthValidator.cs | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/API.dox b/docs/API.dox index 0a27032d8e..66f79a3174 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -128,7 +128,7 @@ TGS4 supports OAuth 2.0 with select providers for authentication. The flow for this is as follows: -- Retrieve the @ref api_ver to find out available OAuth providers and their respective client ID and redirect URIs. +- Retrieve the @ref api_ver to find out which @ref Tgstation.Server.Api.Models.OAuthProvider are enabled and their respective information. - Send the user to the Authorization Request endpoint for the provider using the client ID from above. See https://tools.ietf.org/html/rfc6749#section-4.1.1. DO NOT specify a redirect URI, this should be configured in the provider. - Retrieve the authorization response code after successfully completing the authorize step above. - Perform the following request: diff --git a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs index 10466c3204..6163e445f4 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs @@ -16,5 +16,10 @@ namespace Tgstation.Server.Api.Models /// The redirect URL. /// public Uri? RedirectUri { get; set; } + + /// + /// The server URL. + /// + public Uri? ServerUrl { get; set; } } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index ad0de1e980..4a6beb15e6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -127,7 +127,8 @@ namespace Tgstation.Server.Host.Security.OAuth new OAuthProviderInfo { ClientId = OAuthConfiguration.ClientId, - RedirectUri = OAuthConfiguration.RedirectUrl + RedirectUri = OAuthConfiguration.RedirectUrl, + ServerUrl = OAuthConfiguration.ServerUrl }); } } From 83aec7ef71a7d071f217e07474b4a6a3f1efea9d Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 8 Dec 2020 17:53:39 -0500 Subject: [PATCH 037/154] Fix branch build's TGS4_GITHUB_REF --- .github/workflows/ci-suite.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 474aabc0f1..6d20bbfda5 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -207,7 +207,10 @@ jobs: - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} - run: echo "TGS4_GITHUB_REF=${{ github.event.ref }}" >> $env:GITHUB_ENV + shell: bash + run: | + TEMP_GITHUB_REF="${{ github.event.ref }}" + echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV - name: Run Integration Test run: | @@ -349,7 +352,9 @@ jobs: - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} - run: echo "TGS4_GITHUB_REF=${{ github.event.ref }}" >> $GITHUB_ENV + run: | + TEMP_GITHUB_REF="${{ github.event.ref }}" + echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV - name: Run Integration Test run: | From 71060ee6af7606dc9666e2f74bdea5f18b8775e0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 8 Dec 2020 17:55:28 -0500 Subject: [PATCH 038/154] Remove travis CI badge We use actions now --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 841fdb4e12..a3b24aee2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tgstation-server v4: -![CI](https://github.com/tgstation/tgstation-server/workflows/CI/badge.svg) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) +![CI](https://github.com/tgstation/tgstation-server/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client) From 0c83bcbd0092a6763c712e0123a519911e03a2e1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 8 Dec 2020 19:21:01 -0500 Subject: [PATCH 039/154] Disable enum_case_convention OpenAPI lint Broken by OAuthProvider types. --- build/OpenApiValidationSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/OpenApiValidationSettings.json b/build/OpenApiValidationSettings.json index 2c14eb6066..731082653b 100644 --- a/build/OpenApiValidationSettings.json +++ b/build/OpenApiValidationSettings.json @@ -48,7 +48,7 @@ "inconsistent_property_type": "error", "property_case_convention": "off", "property_case_collision": "error", - "enum_case_convention": "error", + "enum_case_convention": "off", "undefined_required_properties": "error" }, "walker": { From 8466fe31a3462f59a8add3c1c282dd240d262b98 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 15:15:01 -0500 Subject: [PATCH 040/154] Oh god oh fuck this commit is too big - Disable faulty CA1508 - Add CompileJob.RepositoryOrigin - Rename TestMergeParameters.PulRequestRevision to TargetCommitSha - Ensure test merges aren't referred to as pull requests anywhere - Make remote deployment git host agnostic - Make Repository.Origin a Uri --- src/Tgstation.Server.Api/Models/CompileJob.cs | 7 +- .../Models/Internal/CompileJob.cs | 2 +- .../Models/Internal/TestMergeBase.cs | 4 +- src/Tgstation.Server.Api/Models/Repository.cs | 3 +- .../Models/TestMergeParameters.cs | 8 +- .../Chat/Commands/PullRequestsCommand.cs | 6 +- .../Chat/Providers/DiscordProvider.cs | 2 +- .../Components/Chat/Providers/IrcProvider.cs | 2 +- .../Components/Deployment/DmbFactory.cs | 36 +- .../Components/Deployment/DreamMaker.cs | 66 +- .../Remote/BaseRemoteDeploymentManager.cs | 185 +++ ...er.cs => GitHubRemoteDeploymentManager.cs} | 250 ++-- .../Remote/GitLabRemoteDeploymentManager.cs | 175 +++ .../Remote/IRemoteDeploymentManager.cs | 9 +- .../Remote/IRemoteDeploymentManagerFactory.cs | 26 + .../Remote/NoOpRemoteDeploymentManager.cs | 57 + .../Remote/RemoteDeploymentManagerFactory.cs | 101 ++ .../Components/Instance.cs | 16 +- .../Components/InstanceFactory.cs | 30 +- .../Repository/GitHubRemoteFeatures.cs | 8 +- .../Repository/GitLabRemoteFeatures.cs | 18 +- .../Repository/GitRemoteFeaturesFactory.cs | 58 +- .../Repository/IGitRemoteFeaturesFactory.cs | 10 + .../Components/Repository/IRepository.cs | 2 +- .../Components/Repository/Repository.cs | 53 +- .../Components/Watchdog/BasicWatchdog.cs | 6 +- .../Components/Watchdog/IWatchdogFactory.cs | 4 +- .../Components/Watchdog/PosixWatchdog.cs | 6 +- .../Watchdog/PosixWatchdogFactory.cs | 4 +- .../Components/Watchdog/WatchdogBase.cs | 23 +- .../Components/Watchdog/WatchdogFactory.cs | 4 +- .../Components/Watchdog/WindowsWatchdog.cs | 6 +- .../Watchdog/WindowsWatchdogFactory.cs | 4 +- .../Controllers/InstanceController.cs | 2 - .../Controllers/RepositoryController.cs | 40 +- .../Controllers/UserController.cs | 2 - src/Tgstation.Server.Host/Core/Application.cs | 2 + ...250_MSGenericTestMergingUpdate.Designer.cs | 825 ++++++++++++ ...201209194250_MSGenericTestMergingUpdate.cs | 44 + ...348_MYGenericTestMergingUpdate.Designer.cs | 814 ++++++++++++ ...201209194348_MYGenericTestMergingUpdate.cs | 44 + ...500_PGGenericTestMergingUpdate.Designer.cs | 822 ++++++++++++ ...201209194500_PGGenericTestMergingUpdate.cs | 44 + ...554_SLGenericTestMergingUpdate.Designer.cs | 813 ++++++++++++ ...201209194554_SLGenericTestMergingUpdate.cs | 44 + .../MySqlDatabaseContextModelSnapshot.cs | 1117 ++++++++-------- ...PostgresSqlDatabaseContextModelSnapshot.cs | 1129 ++++++++-------- .../SqlServerDatabaseContextModelSnapshot.cs | 1135 +++++++++-------- .../SqliteDatabaseContextModelSnapshot.cs | 1117 ++++++++-------- .../Models/CompileJob.cs | 11 +- src/Tgstation.Server.Host/Models/TestMerge.cs | 2 +- .../Tgstation.Server.Host.csproj | 2 +- .../Instance/RepositoryTest.cs | 46 +- 53 files changed, 6635 insertions(+), 2611 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs rename src/Tgstation.Server.Host/Components/Deployment/Remote/{RemoteDeploymentManager.cs => GitHubRemoteDeploymentManager.cs} (56%) create mode 100644 src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs create mode 100644 src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs create mode 100644 src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs diff --git a/src/Tgstation.Server.Api/Models/CompileJob.cs b/src/Tgstation.Server.Api/Models/CompileJob.cs index 964bc9dad7..d5a2c12d09 100644 --- a/src/Tgstation.Server.Api/Models/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/CompileJob.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Tgstation.Server.Api.Models { @@ -19,5 +19,10 @@ namespace Tgstation.Server.Api.Models /// The the was made with /// public Version? ByondVersion { get; set; } + + /// + /// The origin of the repository the compile job was built from. + /// + public Uri? RepositoryOrigin { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index 06583edaf0..e57bc17784 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs b/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs index c105e643f7..d8cc971b5b 100644 --- a/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models.Internal @@ -51,7 +51,7 @@ namespace Tgstation.Server.Api.Models.Internal BodyAtMerge = copy.BodyAtMerge; Comment = copy.Comment; Number = copy.Number; - PullRequestRevision = copy.PullRequestRevision; + TargetCommitSha = copy.TargetCommitSha; TitleAtMerge = copy.TitleAtMerge; Url = copy.Url; } diff --git a/src/Tgstation.Server.Api/Models/Repository.cs b/src/Tgstation.Server.Api/Models/Repository.cs index 33a0f07d89..a88dfe1c54 100644 --- a/src/Tgstation.Server.Api/Models/Repository.cs +++ b/src/Tgstation.Server.Api/Models/Repository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Tgstation.Server.Api.Models.Internal; @@ -12,7 +13,7 @@ namespace Tgstation.Server.Api.Models /// /// The origin URL. If , the does not exist /// - public string? Origin { get; set; } + public Uri? Origin { get; set; } /// /// If submodules should be recursively cloned. diff --git a/src/Tgstation.Server.Api/Models/TestMergeParameters.cs b/src/Tgstation.Server.Api/Models/TestMergeParameters.cs index ba68c72e7e..0697522f6f 100644 --- a/src/Tgstation.Server.Api/Models/TestMergeParameters.cs +++ b/src/Tgstation.Server.Api/Models/TestMergeParameters.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models { @@ -13,11 +13,11 @@ namespace Tgstation.Server.Api.Models public int Number { get; set; } /// - /// The sha of the pull request revision to merge. If not specified, the latest commit shall be used (semi-unsafe) + /// The sha of the test merge revision to merge. If not specified, the latest commit shall be used (semi-unsafe) /// [Required] [StringLength(40)] - public string? PullRequestRevision { get; set; } + public string? TargetCommitSha { get; set; } /// /// Optional comment about the test @@ -25,4 +25,4 @@ namespace Tgstation.Server.Api.Models [StringLength(Limits.MaximumStringLength)] public string? Comment { get; set; } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs index 2c645130b4..56cce4cc1e 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Globalization; @@ -87,7 +87,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands .Select(x => new Models.TestMerge { Number = x.Number, - PullRequestRevision = x.PullRequestRevision + TargetCommitSha = x.TargetCommitSha }) .ToListAsync(cancellationToken) .ConfigureAwait(false)) @@ -100,7 +100,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List(); } - return !results.Any() ? "None!" : String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)))); + return !results.Any() ? "None!" : String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha.Substring(0, 7)))); } #pragma warning restore CA1506 } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index e82c35b9d5..629b39382c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -417,7 +417,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers .Select(x => new EmbedFieldBuilder { Name = $"#{x.Number}", - Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.PullRequestRevision.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.PullRequestRevision}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}" + Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}" })); var builder = new EmbedBuilder diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 911f95dc23..2767163710 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -601,7 +601,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})", String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => { - var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)); + var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha.Substring(0, 7)); if (x.Comment != null) result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); return result; diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index e565f524f3..ac78dd6fe4 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -42,9 +42,9 @@ namespace Tgstation.Server.Host.Components.Deployment readonly IIOManager ioManager; /// - /// The for the . + /// The for the . /// - readonly IRemoteDeploymentManager remoteDeploymentManager; + readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory; /// /// The for the @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The for the /// - readonly Api.Models.Instance instance; + readonly Api.Models.Instance metadata; /// /// The for @@ -91,21 +91,21 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The value of /// The value of - /// The value of . + /// The value of . /// The value of - /// The value of + /// The value of public DmbFactory( IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, - IRemoteDeploymentManager remoteDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, ILogger logger, - Api.Models.Instance instance) + Api.Models.Instance metadata) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.remoteDeploymentManager = remoteDeploymentManager ?? throw new ArgumentNullException(nameof(remoteDeploymentManager)); + this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); cleanupTask = Task.CompletedTask; newerDmbTcs = new TaskCompletionSource(); @@ -125,6 +125,9 @@ namespace Tgstation.Server.Host.Components.Deployment async Task HandleCleanup() { var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token); + var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager( + metadata, + job); // DCT: None available var deploymentJob = remoteDeploymentManager.MarkInactive(job, default); @@ -158,10 +161,15 @@ namespace Tgstation.Server.Host.Components.Deployment // Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied if (started) + { + var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager( + metadata, + job); await remoteDeploymentManager.StageDeployment( - newProvider.CompileJob, - cancellationToken) - .ConfigureAwait(false); + newProvider.CompileJob, + cancellationToken) + .ConfigureAwait(false); + } lock (jobLockCounts) { @@ -200,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Deployment cj = await db .CompileJobs .AsQueryable() - .Where(x => x.Job.Instance.Id == instance.Id) + .Where(x => x.Job.Instance.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -357,7 +365,7 @@ namespace Tgstation.Server.Host.Components.Deployment .CompileJobs .AsQueryable() .Where( - x => x.Job.Instance.Id == instance.Id + x => x.Job.Instance.Id == metadata.Id && jobIdsToSkip.Contains(x.Id)) .Select(x => x.DirectoryName.Value) .ToListAsync(cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 62b62aa652..48079534c7 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -82,9 +82,9 @@ namespace Tgstation.Server.Host.Components.Deployment readonly ICompileJobSink compileJobConsumer; /// - /// The for . + /// The for . /// - readonly IRemoteDeploymentManager gitHubDeploymentManager; + readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory; /// /// The for @@ -132,7 +132,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// The value of /// The value of . /// The value of . - /// The value of . + /// The value of . /// The value of /// The value of . public DreamMaker( @@ -145,7 +145,7 @@ namespace Tgstation.Server.Host.Components.Deployment IProcessExecutor processExecutor, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, - IRemoteDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, ILogger logger, Api.Models.Instance metadata) { @@ -158,7 +158,7 @@ namespace Tgstation.Server.Host.Components.Deployment this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); - this.gitHubDeploymentManager = gitHubDeploymentManager ?? throw new ArgumentNullException(nameof(gitHubDeploymentManager)); + this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); @@ -360,9 +360,10 @@ namespace Tgstation.Server.Host.Components.Deployment /// Cleans up a failed compile . /// /// The running . + /// The associated with the . /// The that was thrown. /// A representing the running operation - async Task CleanupFailedCompile(Models.CompileJob job, Exception exception) + async Task CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception) { async Task CleanDir() { @@ -382,7 +383,7 @@ namespace Tgstation.Server.Host.Components.Deployment // DCT: None available await Task.WhenAll( CleanDir(), - gitHubDeploymentManager.FailDeployment( + remoteDeploymentManager.FailDeployment( job, FormatExceptionForUsers(exception), default)) @@ -396,10 +397,18 @@ namespace Tgstation.Server.Host.Components.Deployment /// The settings to use /// The to use /// The to use + /// The to use. /// The timeout for validating the DMAPI /// The for the operation /// A representing the running operation - async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken) + async Task RunCompileJob( + Models.CompileJob job, + Api.Models.DreamMaker dreamMakerSettings, + IByondExecutableLock byondLock, + IRepository repository, + IRemoteDeploymentManager remoteDeploymentManager, + uint apiValidateTimeout, + CancellationToken cancellationToken) { var outputDirectory = job.DirectoryName.ToString(); logger.LogTrace("Compile output GUID: {0}", outputDirectory); @@ -416,7 +425,15 @@ namespace Tgstation.Server.Host.Components.Deployment // repository closed now // run precompile scripts - await eventConsumer.HandleEvent(EventType.CompileStart, new List { resolvedOutputDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); + await eventConsumer.HandleEvent( + EventType.CompileStart, + new List + { + resolvedOutputDirectory, + repoOrigin.ToString() + }, + cancellationToken) + .ConfigureAwait(false); // determine the dme if (job.DmeName == null) @@ -489,13 +506,13 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception ex) { - await CleanupFailedCompile(job, ex).ConfigureAwait(false); + await CleanupFailedCompile(job, remoteDeploymentManager, ex).ConfigureAwait(false); throw; } } /// - #pragma warning disable CA1506, CA1508 + #pragma warning disable CA1506 public async Task DeploymentProcess( Models.Job job, IDatabaseContextFactory databaseContextFactory, @@ -530,6 +547,7 @@ namespace Tgstation.Server.Host.Components.Deployment Models.DreamDaemonSettings ddSettings = null; DreamMakerSettings dreamMakerSettings = null; IRepository repo = null; + IRemoteDeploymentManager remoteDeploymentManager = null; Models.RevisionInformation revInfo = null; await databaseContextFactory.UseContext( async databaseContext => @@ -580,6 +598,9 @@ namespace Tgstation.Server.Host.Components.Deployment if (repo == null) throw new JobException(ErrorCode.RepoMissing); + remoteDeploymentManager = remoteDeploymentManagerFactory + .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider.Value); + var repoSha = repo.Head; revInfo = await databaseContext .RevisionInformations @@ -627,6 +648,7 @@ namespace Tgstation.Server.Host.Components.Deployment dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, + remoteDeploymentManager, progressReporter, averageSpan, likelyPushedTestMergeCommit, @@ -673,11 +695,11 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception ex) { - await CleanupFailedCompile(compileJob, ex).ConfigureAwait(false); + await CleanupFailedCompile(compileJob, remoteDeploymentManager, ex).ConfigureAwait(false); throw; } - var commentsTask = gitHubDeploymentManager.PostDeploymentComments( + var commentsTask = remoteDeploymentManager.PostDeploymentComments( compileJob, activeCompileJob?.RevisionInformation, repositorySettings, @@ -714,7 +736,7 @@ namespace Tgstation.Server.Host.Components.Deployment deploying = false; } } - #pragma warning restore CA1506, CA1508 + #pragma warning restore CA1506 /// /// Calculate the average length of a deployment using a given . @@ -755,6 +777,7 @@ namespace Tgstation.Server.Host.Components.Deployment Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, + IRemoteDeploymentManager remoteDeploymentManager, Action progressReporter, TimeSpan? estimatedDuration, bool localCommitExistsOnRemote, @@ -782,16 +805,25 @@ namespace Tgstation.Server.Host.Components.Deployment DirectoryName = Guid.NewGuid(), DmeName = dreamMakerSettings.ProjectName, RevisionInformation = revisionInformation, - ByondVersion = byondLock.Version.ToString() + ByondVersion = byondLock.Version.ToString(), + RepositoryOrigin = repository.Origin.ToString(), }; - await gitHubDeploymentManager.StartDeployment( + await remoteDeploymentManager.StartDeployment( repository, job, cancellationToken) .ConfigureAwait(false); - await RunCompileJob(job, dreamMakerSettings, byondLock, repository, apiValidateTimeout, cancellationToken).ConfigureAwait(false); + await RunCompileJob( + job, + dreamMakerSettings, + byondLock, + repository, + remoteDeploymentManager, + apiValidateTimeout, + cancellationToken) + .ConfigureAwait(false); return job; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs new file mode 100644 index 0000000000..6b80ec9cb4 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs @@ -0,0 +1,185 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components.Deployment.Remote +{ + /// + /// Base class for implementing s. + /// + abstract class BaseRemoteDeploymentManager : IRemoteDeploymentManager + { + /// + /// The for the . + /// + protected Api.Models.Instance Metadata { get; } + + /// + /// The for the . + /// + protected ILogger Logger { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + protected BaseRemoteDeploymentManager(ILogger logger, Api.Models.Instance metadata) + { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + } + + /// + public async Task PostDeploymentComments( + CompileJob compileJob, + RevisionInformation previousRevisionInformation, + RepositorySettings repositorySettings, + string repoOwner, + string repoName, + CancellationToken cancellationToken) + { + if (repositorySettings?.AccessToken == null) + return; + + if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha) + || !repositorySettings.PostTestMergeComment.Value) + return; + + previousRevisionInformation ??= new RevisionInformation(); + previousRevisionInformation.ActiveTestMerges ??= new List(); + + var deployedRevisionInformation = compileJob.RevisionInformation; + var tasks = new List(); + + // added prs + foreach (var I in deployedRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !previousRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add( + CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + FormatTestMerge( + repositorySettings, + compileJob, + I, + repoOwner, + repoName, + false), + I.Number, + cancellationToken)); + + // removed prs + foreach (var I in previousRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !deployedRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add( + CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + "#### Test Merge Removed", + I.Number, + cancellationToken)); + + // updated prs + foreach (var I in deployedRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => previousRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add( + CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + FormatTestMerge( + repositorySettings, + compileJob, + I, + repoOwner, + repoName, + true), + I.Number, + cancellationToken)); + + if (tasks.Any()) + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// + /// Create a comment of a given 's source. + /// + /// The to use. + /// The . + /// The . + /// The comment to post. + /// The . + /// The for the operation. + /// A representing the running operation. + protected abstract Task CommentOnTestMergeSource( + RepositorySettings repositorySettings, + string remoteRepositoryOwner, + string remoteRepositoryName, + string comment, + int testMergeNumber, + CancellationToken cancellationToken); + + /// + /// Formats a comment for a given . + /// + /// The to use. + /// The test merge's . + /// The . + /// The . + /// The . + /// If is new, otherwise it has been updated to a different . + /// A representing the running operation. + protected abstract string FormatTestMerge( + RepositorySettings repositorySettings, + CompileJob compileJob, + TestMerge testMerge, + string remoteRepositoryOwner, + string remoteRepositoryName, + bool updated); + + /// + public abstract Task ApplyDeployment(CompileJob compileJob, CompileJob oldCompileJob, CancellationToken cancellationToken); + + /// + public abstract Task FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken); + + /// + public abstract Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken); + + /// + public abstract Task> RemoveMergedTestMerges( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken); + + /// + public abstract Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken); + + /// + public abstract Task StartDeployment( + Api.Models.Internal.IGitRemoteInformation remoteInformation, + CompileJob compileJob, + CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs similarity index 56% rename from src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManager.cs rename to src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index cd10dea7e0..4ef0a40b6b 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -16,63 +16,51 @@ using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Deployment.Remote { - /// - sealed class RemoteDeploymentManager : IRemoteDeploymentManager + /// + /// for GitHub.com + /// + sealed class GitHubRemoteDeploymentManager : BaseRemoteDeploymentManager { /// - /// The for the . + /// The for the . /// readonly IDatabaseContextFactory databaseContextFactory; /// - /// The for the . + /// The for the . /// readonly IGitHubClientFactory gitHubClientFactory; /// - /// The for the . - /// - readonly ILogger logger; - - /// - /// The for the . - /// - readonly Api.Models.Instance metadata; - - /// - /// Initializes a new instance of the . + /// Initializes a new instance of the . /// /// The value of . /// The value of . - /// The value of . - /// The value of . - public RemoteDeploymentManager( + /// The for the . + /// The for the . + public GitHubRemoteDeploymentManager( IDatabaseContextFactory databaseContextFactory, IGitHubClientFactory gitHubClientFactory, - ILogger logger, + ILogger logger, Api.Models.Instance metadata) + : base(logger, metadata) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); } /// - public async Task StartDeployment(IRepository repository, CompileJob compileJob, CancellationToken cancellationToken) + public override async Task StartDeployment( + Api.Models.Internal.IGitRemoteInformation remoteInformation, + CompileJob compileJob, + CancellationToken cancellationToken) { - if (repository == null) - throw new ArgumentNullException(nameof(repository)); + if (remoteInformation == null) + throw new ArgumentNullException(nameof(remoteInformation)); if (compileJob == null) throw new ArgumentNullException(nameof(compileJob)); - if (repository.RemoteGitProvider != Api.Models.RemoteGitProvider.GitHub) - { - logger.LogTrace("Not managing deployment as this is not a GitHub repo"); - return; - } - - logger.LogTrace("Starting deployment..."); + Logger.LogTrace("Starting deployment..."); RepositorySettings repositorySettings = null; await databaseContextFactory.UseContext( @@ -80,7 +68,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote repositorySettings = await databaseContext .RepositorySettings .AsQueryable() - .Where(x => x.InstanceId == metadata.Id) + .Where(x => x.InstanceId == Metadata.Id) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); @@ -93,27 +81,27 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote var repositoryTask = gitHubClient .Repository .Get( - repository.RemoteRepositoryOwner, - repository.RemoteRepositoryName); + remoteInformation.RemoteRepositoryOwner, + remoteInformation.RemoteRepositoryName); if (!repositorySettings.CreateGitHubDeployments.Value) - logger.LogTrace("Not creating deployment"); + Logger.LogTrace("Not creating deployment"); else if (!instanceAuthenticated) - logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!"); + Logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!"); else { - logger.LogTrace("Creating deployment..."); + Logger.LogTrace("Creating deployment..."); var deployment = await gitHubClient .Repository .Deployment .Create( - repository.RemoteRepositoryOwner, - repository.RemoteRepositoryName, + remoteInformation.RemoteRepositoryOwner, + remoteInformation.RemoteRepositoryName, new NewDeployment(compileJob.RevisionInformation.CommitSha) { AutoMerge = false, Description = "TGS Game Deployment", - Environment = $"TGS: {metadata.Name}", + Environment = $"TGS: {Metadata.Name}", ProductionEnvironment = true, RequiredContexts = new Collection() }) @@ -121,15 +109,15 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote .ConfigureAwait(false); compileJob.GitHubDeploymentId = deployment.Id; - logger.LogDebug("Created deployment ID {0}", deployment.Id); + Logger.LogDebug("Created deployment ID {0}", deployment.Id); await gitHubClient .Repository .Deployment .Status .Create( - repository.RemoteRepositoryOwner, - repository.RemoteRepositoryName, + remoteInformation.RemoteRepositoryOwner, + remoteInformation.RemoteRepositoryName, deployment.Id, new NewDeploymentStatus(DeploymentState.InProgress) { @@ -139,7 +127,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote .WithToken(cancellationToken) .ConfigureAwait(false); - logger.LogTrace("In-progress deployment status created"); + Logger.LogTrace("In-progress deployment status created"); } try @@ -149,11 +137,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote .ConfigureAwait(false); compileJob.GitHubRepoId = gitHubRepo.Id; - logger.LogTrace("Set GitHub ID as {0}", compileJob.GitHubRepoId); + Logger.LogTrace("Set GitHub ID as {0}", compileJob.GitHubRepoId); } catch (RateLimitExceededException ex) when (!repositorySettings.CreateGitHubDeployments.Value) { - logger.LogWarning(ex, "Unable to set compile job repository ID!"); + Logger.LogWarning(ex, "Unable to set compile job repository ID!"); } } @@ -168,11 +156,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote if (!compileJob.GitHubRepoId.HasValue || !compileJob.GitHubDeploymentId.HasValue) { - logger.LogTrace("Not updating deployment as it is missing a repo ID or deployment ID."); + Logger.LogTrace("Not updating deployment as it is missing a repo ID or deployment ID."); return; } - logger.LogTrace("Updating deployment {0} to {1}...", compileJob.GitHubDeploymentId.Value, deploymentState); + Logger.LogTrace("Updating deployment {0} to {1}...", compileJob.GitHubDeploymentId.Value, deploymentState); string gitHubAccessToken = null; await databaseContextFactory.UseContext( @@ -180,7 +168,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote gitHubAccessToken = await databaseContext .RepositorySettings .AsQueryable() - .Where(x => x.InstanceId == metadata.Id) + .Where(x => x.InstanceId == Metadata.Id) .Select(x => x.AccessToken) .FirstAsync(cancellationToken) .ConfigureAwait(false)) @@ -188,7 +176,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote if (gitHubAccessToken == null) { - logger.LogWarning( + Logger.LogWarning( "GitHub access token disappeared during deployment, can't update to {0}!", deploymentState); return; @@ -212,7 +200,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote } /// - public Task StageDeployment( + public override Task StageDeployment( CompileJob compileJob, CancellationToken cancellationToken) => UpdateDeployment( @@ -222,7 +210,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote cancellationToken); /// - public Task ApplyDeployment(CompileJob compileJob, CompileJob oldCompileJob, CancellationToken cancellationToken) + public override Task ApplyDeployment(CompileJob compileJob, CompileJob oldCompileJob, CancellationToken cancellationToken) => UpdateDeployment( compileJob, "The deployment is now live on the server.", @@ -230,7 +218,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote cancellationToken); /// - public Task FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken) + public override Task FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken) => UpdateDeployment( compileJob, errorMessage, @@ -238,7 +226,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote cancellationToken); /// - public Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken) + public override Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken) => UpdateDeployment( compileJob, "The deployment has been superceeded.", @@ -246,99 +234,22 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote cancellationToken); /// - public async Task PostDeploymentComments( - CompileJob compileJob, - RevisionInformation previousRevisionInformation, - RepositorySettings repositorySettings, - string repoOwner, - string repoName, - CancellationToken cancellationToken) - { - if (repositorySettings?.AccessToken == null) - return; - - if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha) - || !repositorySettings.PostTestMergeComment.Value) - return; - - previousRevisionInformation = new RevisionInformation - { - ActiveTestMerges = new List() - }; - - var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); - - async Task CommentOnPR(int prNumber, string comment) - { - try - { - await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment) - .WithToken(cancellationToken) - .ConfigureAwait(false); - } - catch (ApiException e) - { - logger.LogWarning(e, "Error posting GitHub comment!"); - } - } - - var tasks = new List(); - - var deployedRevisionInformation = compileJob.RevisionInformation; - string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}", - Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty, - testMerge.PullRequestRevision, - testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty, - updated ? "Updated" : "Deployed", - metadata.Name, - deployedRevisionInformation.OriginCommitSha, - deployedRevisionInformation.CommitSha, - compileJob.GitHubDeploymentId.HasValue - ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{repoOwner}/{repoName}/deployments/activity_log?environment=TGS%3A%20{metadata.Name})" - : String.Empty); - - // added prs - foreach (var I in deployedRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !previousRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false))); - - // removed prs - foreach (var I in previousRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !deployedRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed")); - - // updated prs - foreach (var I in deployedRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => previousRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true))); - - if (tasks.Any()) - await Task.WhenAll(tasks).ConfigureAwait(false); - } - - /// - public async Task> RemoveMergedPullRequests( + public override async Task> RemoveMergedTestMerges( IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken) { + if (repository == null) + throw new ArgumentNullException(nameof(repository)); + if (repositorySettings == null) + throw new ArgumentNullException(nameof(repositorySettings)); + if (revisionInformation == null) + throw new ArgumentNullException(nameof(revisionInformation)); + if (revisionInformation.ActiveTestMerges?.Any() != true) { - logger.LogTrace("No test merges to remove."); + Logger.LogTrace("No test merges to remove."); return Array.Empty(); } @@ -358,7 +269,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote } catch (Exception ex) when (!(ex is OperationCanceledException)) { - logger.LogWarning(ex, "Pull requests update check failed!"); + Logger.LogWarning(ex, "Pull requests update check failed!"); } var newList = revisionInformation.ActiveTestMerges.ToList(); @@ -386,5 +297,62 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return newList; } + + /// + protected override async Task CommentOnTestMergeSource( + RepositorySettings repositorySettings, + string remoteRepositoryOwner, + string remoteRepositoryName, + string comment, + int testMergeNumber, + CancellationToken cancellationToken) + { + var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); + + try + { + await gitHubClient.Issue.Comment.Create(remoteRepositoryOwner, remoteRepositoryName, testMergeNumber, comment) + .WithToken(cancellationToken) + .ConfigureAwait(false); + } + catch (ApiException e) + { + Logger.LogWarning(e, "Error posting GitHub comment!"); + } + } + + /// + protected override string FormatTestMerge( + RepositorySettings repositorySettings, + CompileJob compileJob, + TestMerge testMerge, + string remoteRepositoryOwner, + string remoteRepositoryName, + bool updated) => String.Format( + CultureInfo.InvariantCulture, + "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}", + Environment.NewLine, + repositorySettings.ShowTestMergeCommitters.Value + ? String.Format( + CultureInfo.InvariantCulture, + "{0}{0}##### Merged By{0}{1}", + Environment.NewLine, + testMerge.MergedBy.Name) + : String.Empty, + testMerge.TargetCommitSha, + testMerge.Comment != null + ? String.Format( + CultureInfo.InvariantCulture, + "{0}{0}##### Comment{0}{1}", + Environment.NewLine, + testMerge.Comment) + : String.Empty, + updated ? "Updated" : "Deployed", + Metadata.Name, + compileJob.RevisionInformation.OriginCommitSha, + compileJob.RevisionInformation.CommitSha, + compileJob.GitHubDeploymentId.HasValue + ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A%20{Metadata.Name})" + : String.Empty); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs new file mode 100644 index 0000000000..a81972fc87 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs @@ -0,0 +1,175 @@ +using GitLabApiClient; +using GitLabApiClient.Models.MergeRequests.Responses; +using GitLabApiClient.Models.Notes.Requests; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components.Deployment.Remote +{ + /// + /// for GitLab.com + /// + sealed class GitLabRemoteDeploymentManager : BaseRemoteDeploymentManager + { + /// + /// Initializes a new instance of the . + /// + /// The for the . + /// The for the . + public GitLabRemoteDeploymentManager(ILogger logger, Api.Models.Instance metadata) + : base(logger, metadata) + { + } + + /// + protected override Task CommentOnTestMergeSource( + RepositorySettings repositorySettings, + string remoteRepositoryOwner, + string remoteRepositoryName, + string comment, + int testMergeNumber, + CancellationToken cancellationToken) + { + var client = repositorySettings.AccessToken != null + ? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken) + : new GitLabClient(GitLabRemoteFeatures.GitLabUrl); + + return client + .MergeRequests + .CreateNoteAsync( + $"{remoteRepositoryOwner}/{remoteRepositoryName}", + testMergeNumber, + new CreateMergeRequestNoteRequest(comment)) + .WithToken(cancellationToken); + } + + /// + protected override string FormatTestMerge( + RepositorySettings repositorySettings, + CompileJob compileJob, + TestMerge testMerge, + string remoteRepositoryOwner, + string remoteRepositoryName, + bool updated) => String.Format( + CultureInfo.InvariantCulture, + "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}", + Environment.NewLine, + repositorySettings.ShowTestMergeCommitters.Value + ? String.Format( + CultureInfo.InvariantCulture, + "{0}{0}##### Merged By{0}{1}", + Environment.NewLine, + testMerge.MergedBy.Name) + : String.Empty, + testMerge.TargetCommitSha, + testMerge.Comment != null + ? String.Format( + CultureInfo.InvariantCulture, + "{0}{0}##### Comment{0}{1}", + Environment.NewLine, + testMerge.Comment) + : String.Empty, + updated ? "Updated" : "Deployed", + Metadata.Name, + compileJob.RevisionInformation.OriginCommitSha, + compileJob.RevisionInformation.CommitSha); + + /// + public override async Task> RemoveMergedTestMerges( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken) + { + if (repository == null) + throw new ArgumentNullException(nameof(repository)); + if (repositorySettings == null) + throw new ArgumentNullException(nameof(repositorySettings)); + if (revisionInformation == null) + throw new ArgumentNullException(nameof(revisionInformation)); + + if (revisionInformation.ActiveTestMerges?.Any() != true) + { + Logger.LogTrace("No test merges to remove."); + return Array.Empty(); + } + + var client = repositorySettings.AccessToken != null + ? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken) + : new GitLabClient(GitLabRemoteFeatures.GitLabUrl); + + var tasks = revisionInformation + .ActiveTestMerges + .Select(x => client + .MergeRequests + .GetAsync( + $"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}", + x.TestMerge.Number) + .WithToken(cancellationToken)); + try + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + Logger.LogWarning(ex, "Merge requests update check failed!"); + } + + var newList = revisionInformation.ActiveTestMerges.ToList(); + + MergeRequest lastMerged = null; + async Task CheckRemoveMR(Task task) + { + var mergeRequest = await task.ConfigureAwait(false); + if (mergeRequest.State != MergeRequestState.Merged) + return; + + // We don't just assume, actually check the repo contains the merge commit. + if (await repository.ShaIsParent(mergeRequest.MergeCommitSha, cancellationToken).ConfigureAwait(false)) + { + if (lastMerged == null || lastMerged.ClosedAt < mergeRequest.ClosedAt) + lastMerged = mergeRequest; + newList.Remove( + newList.First( + potential => potential.TestMerge.Number == mergeRequest.Id)); + } + } + + foreach (var prTask in tasks) + await CheckRemoveMR(prTask).ConfigureAwait(false); + + return newList; + } + + /// + public override Task ApplyDeployment( + CompileJob compileJob, + CompileJob oldCompileJob, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public override Task FailDeployment( + CompileJob compileJob, + string errorMessage, + CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public override Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public override Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public override Task StartDeployment( + Api.Models.Internal.IGitRemoteInformation remoteInformation, + CompileJob compileJob, + CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs index 58cb4539f6..26947bb659 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs @@ -14,11 +14,14 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// /// Start a deployment for a given . /// - /// The being deployed. + /// The of the repository being deployed. /// The active . /// The for the operation. /// A representing the running operation. - Task StartDeployment(IRepository repository, CompileJob compileJob, CancellationToken cancellationToken); + Task StartDeployment( + Api.Models.Internal.IGitRemoteInformation remoteInformation, + CompileJob compileJob, + CancellationToken cancellationToken); /// /// Stage a given 's deployment. @@ -82,7 +85,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// The current . /// The for the operation. /// A resulting in the of s that should remain the new . - Task> RemoveMergedPullRequests( + Task> RemoveMergedTestMerges( IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs new file mode 100644 index 0000000000..9ab84ef261 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManagerFactory.cs @@ -0,0 +1,26 @@ +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Deployment.Remote +{ + /// + /// Factory for creating s. + /// + interface IRemoteDeploymentManagerFactory + { + /// + /// Creates a for a given . + /// + /// Current metadata. + /// The in use. + /// A new based on the . + IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider); + + /// + /// Create a for a given . + /// + /// Current metadata. + /// The containing the to use. + /// A new . + IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, Models.CompileJob compileJob); + } +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs new file mode 100644 index 0000000000..4c84dc4a6f --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/NoOpRemoteDeploymentManager.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components.Deployment.Remote +{ + /// + /// No-op implementation of . + /// + sealed class NoOpRemoteDeploymentManager : IRemoteDeploymentManager + { + /// + public Task ApplyDeployment( + CompileJob compileJob, + CompileJob oldCompileJob, + CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task FailDeployment( + CompileJob compileJob, + string errorMessage, + CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task MarkInactive( + CompileJob compileJob, + CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task PostDeploymentComments( + CompileJob compileJob, + RevisionInformation previousRevisionInformation, + RepositorySettings repositorySettings, + string repoOwner, + string repoName, + CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task> RemoveMergedTestMerges( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken) => Task.FromResult>(Array.Empty()); + + /// + public Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task StartDeployment( + Api.Models.Internal.IGitRemoteInformation remoteInformation, + CompileJob compileJob, + CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs new file mode 100644 index 0000000000..6bc60ab1fa --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using System; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Database; + +namespace Tgstation.Server.Host.Components.Deployment.Remote +{ + /// + sealed class RemoteDeploymentManagerFactory : IRemoteDeploymentManagerFactory + { + /// + /// The for the . + /// + readonly IDatabaseContextFactory databaseContextFactory; + + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + + /// + /// The for the . + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public RemoteDeploymentManagerFactory( + IDatabaseContextFactory databaseContextFactory, + IGitHubClientFactory gitHubClientFactory, + IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, + ILoggerFactory loggerFactory, + ILogger logger) + { + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider) + { + if (metadata == null) + throw new ArgumentNullException(nameof(metadata)); + + logger.LogTrace("Creating remote deployment manager for remote git provider {0}...", remoteGitProvider); + return remoteGitProvider switch + { + RemoteGitProvider.GitHub => new GitHubRemoteDeploymentManager( + databaseContextFactory, + gitHubClientFactory, + loggerFactory.CreateLogger(), + metadata), + RemoteGitProvider.GitLab => new GitLabRemoteDeploymentManager( + loggerFactory.CreateLogger(), + metadata), + RemoteGitProvider.Unknown => new NoOpRemoteDeploymentManager(), + _ => throw new InvalidOperationException($"Invalid RemoteGitProvider: {remoteGitProvider}!"), + }; + } + + /// + public IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, Models.CompileJob compileJob) + { + if (compileJob == null) + throw new ArgumentNullException(nameof(compileJob)); + + RemoteGitProvider remoteGitProvider; + + // Pre 4.7.X + if (compileJob.RepositoryOrigin == null) + remoteGitProvider = RemoteGitProvider.Unknown; + else + remoteGitProvider = gitRemoteFeaturesFactory.ParseRemoteGitProviderFromOrigin( + new Uri( + compileJob.RepositoryOrigin)); + + return CreateRemoteDeploymentManager(metadata, remoteGitProvider); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index cbf9d2872e..3c4b3f437f 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -63,9 +63,9 @@ namespace Tgstation.Server.Host.Components readonly IEventConsumer eventConsumer; /// - /// The for the . + /// The for the . /// - readonly IRemoteDeploymentManager remoteDeploymentManager; + readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory; /// /// The for the @@ -105,7 +105,7 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of . + /// The value of . /// The value of public Instance( Api.Models.Instance metadata, @@ -119,7 +119,7 @@ namespace Tgstation.Server.Host.Components IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager remoteDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, ILogger logger) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); @@ -132,7 +132,7 @@ namespace Tgstation.Server.Host.Components this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); - this.remoteDeploymentManager = remoteDeploymentManager ?? throw new ArgumentNullException(nameof(remoteDeploymentManager)); + this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); timerLock = new object(); @@ -263,12 +263,14 @@ namespace Tgstation.Server.Host.Components } var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges.Value; - + var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager( + metadata, + repo.RemoteGitProvider.Value); if (result.HasValue) { currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); - var updatedTestMerges = await remoteDeploymentManager.RemoveMergedPullRequests( + var updatedTestMerges = await remoteDeploymentManager.RemoveMergedTestMerges( repo, repositorySettings, currentRevInfo, diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 12e308ccb0..14a641696c 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -102,11 +102,6 @@ namespace Tgstation.Server.Host.Components /// readonly INetworkPromptReaper networkPromptReaper; - /// - /// The for the - /// - readonly IGitHubClientFactory gitHubClientFactory; - /// /// The for the /// @@ -137,6 +132,11 @@ namespace Tgstation.Server.Host.Components /// readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + /// + /// The for the . + /// + readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory; + /// /// The for the . /// @@ -160,13 +160,13 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of . /// The value of . /// The value of . /// The value of . /// The value of . + /// The value of . /// The containing the value of . public InstanceFactory( IIOManager ioManager, @@ -184,13 +184,13 @@ namespace Tgstation.Server.Host.Components IWatchdogFactory watchdogFactory, IJobManager jobManager, INetworkPromptReaper networkPromptReaper, - IGitHubClientFactory gitHubClientFactory, IPlatformIdentifier platformIdentifier, ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands repositoryCommands, IServerPortProvider serverPortProvider, IFileTransferTicketProvider fileTransferService, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IOptions generalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -208,13 +208,13 @@ namespace Tgstation.Server.Host.Components this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper)); - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); + this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } @@ -275,16 +275,10 @@ namespace Tgstation.Server.Host.Components loggerFactory.CreateLogger(), metadata.CloneMetadata()); - var remoteDeploymentManager = new RemoteDeploymentManager( - databaseContextFactory, - gitHubClientFactory, - loggerFactory.CreateLogger(), - metadata.CloneMetadata()); - var dmbFactory = new DmbFactory( databaseContextFactory, gameIoManager, - remoteDeploymentManager, + remoteDeploymentManagerFactory, loggerFactory.CreateLogger(), metadata.CloneMetadata()); try @@ -303,7 +297,7 @@ namespace Tgstation.Server.Host.Components gameIoManager, diagnosticsIOManager, eventConsumer, - remoteDeploymentManager, + remoteDeploymentManagerFactory, metadata.CloneMetadata(), metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); @@ -321,7 +315,7 @@ namespace Tgstation.Server.Host.Components processExecutor, dmbFactory, repoManager, - remoteDeploymentManager, + remoteDeploymentManagerFactory, loggerFactory.CreateLogger(), metadata.CloneMetadata()); @@ -336,7 +330,7 @@ namespace Tgstation.Server.Host.Components dmbFactory, jobManager, eventConsumer, - remoteDeploymentManager, + remoteDeploymentManagerFactory, loggerFactory.CreateLogger()); return instance; diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 1878171196..260e64fa7e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -95,10 +95,10 @@ namespace Tgstation.Server.Host.Components.Repository if (exception != null) Logger.LogWarning(exception, "Error retrieving pull request metadata!"); - var revisionToUse = parameters.PullRequestRevision == null - || pr?.Head.Sha.StartsWith(parameters.PullRequestRevision, StringComparison.OrdinalIgnoreCase) == true + var revisionToUse = parameters.TargetCommitSha == null + || pr?.Head.Sha.StartsWith(parameters.TargetCommitSha, StringComparison.OrdinalIgnoreCase) == true ? pr?.Head.Sha - : parameters.PullRequestRevision; + : parameters.TargetCommitSha; var testMerge = new Models.TestMerge { @@ -107,7 +107,7 @@ namespace Tgstation.Server.Host.Components.Repository TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, Comment = parameters.Comment, Number = parameters.Number, - PullRequestRevision = revisionToUse, + TargetCommitSha = revisionToUse, Url = pr?.HtmlUrl ?? errorMessage }; diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index aa5b932fd5..cf5fd71149 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -14,6 +14,12 @@ namespace Tgstation.Server.Host.Components.Repository /// sealed class GitLabRemoteFeatures : GitRemoteFeaturesBase { + /// + /// Url for main GitLab site. + /// + /// Eventually we'll derive this from a repo's origin when someone request custom GitHub/GitLab installation support. + public const string GitLabUrl = "https://gitlab.com"; + /// public override string TestMergeRefSpecFormatter => "merge-requests/{0}/head:{1}"; @@ -47,8 +53,6 @@ namespace Tgstation.Server.Host.Components.Repository RepositorySettings repositorySettings, CancellationToken cancellationToken) { - const string GitLabUrl = "https://gitlab.com"; - var client = repositorySettings.AccessToken != null ? new GitLabClient(GitLabUrl, repositorySettings.AccessToken) : new GitLabClient(GitLabUrl); @@ -61,10 +65,10 @@ namespace Tgstation.Server.Host.Components.Repository .WithToken(cancellationToken) .ConfigureAwait(false); - var revisionToUse = parameters.PullRequestRevision == null - || mr.Sha.StartsWith(parameters.PullRequestRevision, StringComparison.OrdinalIgnoreCase) + var revisionToUse = parameters.TargetCommitSha == null + || mr.Sha.StartsWith(parameters.TargetCommitSha, StringComparison.OrdinalIgnoreCase) ? mr.Sha - : parameters.PullRequestRevision; + : parameters.TargetCommitSha; return new Models.TestMerge { @@ -73,7 +77,7 @@ namespace Tgstation.Server.Host.Components.Repository TitleAtMerge = mr.Title, Comment = parameters.Comment, Number = parameters.Number, - PullRequestRevision = mr.Sha, + TargetCommitSha = mr.Sha, Url = mr.WebUrl }; } @@ -88,7 +92,7 @@ namespace Tgstation.Server.Host.Components.Repository TitleAtMerge = ex.Message, Comment = parameters.Comment, Number = parameters.Number, - PullRequestRevision = parameters.PullRequestRevision, + TargetCommitSha = parameters.TargetCommitSha, Url = ex.Message }; } diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs index 1c061adc6e..2f864b258e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using System; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Repository @@ -45,36 +46,41 @@ namespace Tgstation.Server.Host.Components.Repository throw new ArgumentNullException(nameof(repository)); var primaryRemote = repository.Origin; - try + var remoteGitProvider = ParseRemoteGitProviderFromOrigin(primaryRemote); + return remoteGitProvider switch { - var primaryRemoteUrl = new Uri(primaryRemote); + RemoteGitProvider.GitHub => new GitHubRemoteFeatures( + gitHubClientFactory, + loggerFactory.CreateLogger(), + primaryRemote), + RemoteGitProvider.GitLab => new GitLabRemoteFeatures( + loggerFactory.CreateLogger(), + primaryRemote), + RemoteGitProvider.Unknown => new DefaultGitRemoteFeatures(), + _ => throw new InvalidOperationException($"Unknown RemoteGitProvider: {remoteGitProvider}!"), + }; + } - switch (primaryRemoteUrl.Host.ToUpperInvariant()) - { - case "GITHUB.COM": - case "WWW.GITHUB.COM": - case "GIT.GITHUB.COM": - return new GitHubRemoteFeatures( - gitHubClientFactory, - loggerFactory.CreateLogger(), - primaryRemoteUrl); - case "GITLAB.COM": - case "WWW.GITLAB.COM": - case "GIT.GITLAB.COM": - return new GitLabRemoteFeatures( - loggerFactory.CreateLogger(), - primaryRemoteUrl); - default: - logger.LogTrace("Unknown git remote: {0}", primaryRemoteUrl); - break; - } - } - catch (Exception ex) + /// + public RemoteGitProvider ParseRemoteGitProviderFromOrigin(Uri origin) + { + if (origin == null) + throw new ArgumentNullException(nameof(origin)); + + switch (origin.Host.ToUpperInvariant()) { - logger.LogWarning(ex, "Error parsing remote git provider."); + case "GITHUB.COM": + case "WWW.GITHUB.COM": + case "GIT.GITHUB.COM": + return RemoteGitProvider.GitHub; + case "GITLAB.COM": + case "WWW.GITLAB.COM": + case "GIT.GITLAB.COM": + return RemoteGitProvider.GitLab; + default: + logger.LogTrace("Unknown git remote: {0}", origin); + return RemoteGitProvider.Unknown; } - - return new DefaultGitRemoteFeatures(); } } } diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs index 31085a16d2..b2c00aac6d 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeaturesFactory.cs @@ -1,3 +1,6 @@ +using System; +using Tgstation.Server.Api.Models; + namespace Tgstation.Server.Host.Components.Repository { /// @@ -11,5 +14,12 @@ namespace Tgstation.Server.Host.Components.Repository /// The to create for. /// A new instance. IGitRemoteFeatures CreateGitRemoteFeatures(IRepository repository); + + /// + /// Gets the for a given . + /// + /// The of the origin. + /// The of the . + RemoteGitProvider ParseRemoteGitProviderFromOrigin(Uri origin); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index be9d6b0b0d..84dc366a55 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The current origin remote the is using /// - string Origin { get; } + Uri Origin { get; } /// /// Checks if a given is a sha diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 3d83a36dbb..d8be8805fb 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Components.Repository public string Reference => libGitRepo.Head.FriendlyName; /// - public string Origin => libGitRepo.Network.Remotes.First().Url; + public Uri Origin => new Uri(libGitRepo.Network.Remotes.First().Url); /// /// The for the @@ -167,27 +167,6 @@ namespace Tgstation.Server.Host.Components.Repository onDispose(); } - /// - /// Parses the and for a given git . - /// - /// The full remote URL. - /// The parsed owner. - /// The parsed name. - void GetRepositoryOwnerName(string remote, out string owner, out string name) - { - // Assume standard gh format: [(git)|(https)]://[].com/owner/repo(.git)[0-1] - // Yes use .git twice in case it was weird - var toRemove = new string[] { ".git", "/", ".git" }; - foreach (string item in toRemove) - if (remote.EndsWith(item, StringComparison.OrdinalIgnoreCase)) - remote = remote.Substring(0, remote.LastIndexOf(item, StringComparison.OrdinalIgnoreCase)); - var splits = remote.Split('/'); - name = splits.Last(); - owner = splits[^2].Split('.').First(); - - logger.LogTrace("GetRepositoryOwnerName({0}) => {1} / {2}", remote, owner, name); - } - /// /// Generate a standard set of /// @@ -286,7 +265,7 @@ namespace Tgstation.Server.Host.Components.Repository logger.LogDebug("Begin AddTestMerge: #{0} at {1} ({2}) by <{3} ({4})>", testMergeParameters.Number, - testMergeParameters.PullRequestRevision?.Substring(0, 7), + testMergeParameters.TargetCommitSha?.Substring(0, 7), testMergeParameters.Comment, committerName, committerEmail); @@ -303,12 +282,12 @@ namespace Tgstation.Server.Host.Components.Repository : String.Empty, testMergeParameters.Comment ?? String.Empty); - var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", testMergeParameters.Number); - var localBranchName = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeLocalBranchNameFormatter, testMergeParameters.Number, prBranchName); + var testMergeBranchName = String.Format(CultureInfo.InvariantCulture, "tm-{0}", testMergeParameters.Number); + var localBranchName = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeLocalBranchNameFormatter, testMergeParameters.Number, testMergeBranchName); - var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, prBranchName); + var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, testMergeBranchName); var refSpecList = new List { refSpec }; - var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", testMergeParameters.Number); + var logMessage = String.Format(CultureInfo.InvariantCulture, "Test merge #{0}", testMergeParameters.Number); var originalCommit = libGitRepo.Head; @@ -352,13 +331,13 @@ namespace Tgstation.Server.Host.Components.Repository cancellationToken.ThrowIfCancellationRequested(); - testMergeParameters.PullRequestRevision = libGitRepo.Lookup(testMergeParameters.PullRequestRevision ?? localBranchName).Sha; + testMergeParameters.TargetCommitSha = libGitRepo.Lookup(testMergeParameters.TargetCommitSha ?? localBranchName).Sha; cancellationToken.ThrowIfCancellationRequested(); - logger.LogTrace("Merging {0} into {1}...", testMergeParameters.PullRequestRevision.Substring(0, 7), Reference); + logger.LogTrace("Merging {0} into {1}...", testMergeParameters.TargetCommitSha.Substring(0, 7), Reference); - result = libGitRepo.Merge(testMergeParameters.PullRequestRevision, sig, new MergeOptions + result = libGitRepo.Merge(testMergeParameters.TargetCommitSha, sig, new MergeOptions { CommitOnSuccess = commitMessage == null, FailOnConflict = true, @@ -387,7 +366,17 @@ namespace Tgstation.Server.Host.Components.Repository if (result.Status == MergeStatus.Conflicts) { - await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List { originalCommit.Tip.Sha, testMergeParameters.PullRequestRevision, originalCommit.FriendlyName ?? UnknownReference, prBranchName }, cancellationToken).ConfigureAwait(false); + await eventConsumer.HandleEvent( + EventType.RepoMergeConflict, + new List + { + originalCommit.Tip.Sha, + testMergeParameters.TargetCommitSha, + originalCommit.FriendlyName ?? UnknownReference, + testMergeBranchName + }, + cancellationToken) + .ConfigureAwait(false); return null; } @@ -405,7 +394,7 @@ namespace Tgstation.Server.Host.Components.Repository new List { testMergeParameters.Number.ToString(CultureInfo.InvariantCulture), - testMergeParameters.PullRequestRevision, + testMergeParameters.TargetCommitSha, testMergeParameters.Comment }, cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 421ca862ba..48098d98de 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . + /// The for the . /// The for the . /// The for the . /// The for the . @@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager remoteDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, @@ -78,7 +78,7 @@ namespace Tgstation.Server.Host.Components.Watchdog asyncDelayer, diagnosticsIOManager, eventConsumer, - remoteDeploymentManager, + remoteDeploymentManagerFactory, logger, initialLaunchParameters, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs index fa3b9b016d..b833c7812a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The pointing to the Game directory for the . /// The pointing to the Diagnostics directory for the . /// The for the . - /// The for the . + /// The for the . /// The for the /// The initial for the /// A new @@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager remoteDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, Api.Models.Instance instance, DreamDaemonSettings settings); } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 849c27c27d..a0ef483b82 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -36,7 +36,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . + /// The for the . /// The pointing to the game directory for the .. /// The for the . /// The for the . @@ -53,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IIOManager gameIOManager, ISymlinkFactory symlinkFactory, ILogger logger, @@ -70,7 +70,7 @@ namespace Tgstation.Server.Host.Components.Watchdog asyncDelayer, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManagerFactory, gameIOManager, symlinkFactory, logger, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index df8e4d2351..92aa8ed26b 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -53,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, Api.Models.Instance instance, DreamDaemonSettings settings) => new PosixWatchdog( @@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Components.Watchdog AsyncDelayer, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManagerFactory, gameIOManager, SymlinkFactory, LoggerFactory.CreateLogger(), diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 168bbf959b..8275f6df83 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the . /// - readonly Api.Models.Instance instance; + readonly Api.Models.Instance metadata; /// /// The for the . @@ -124,9 +124,9 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly IEventConsumer eventConsumer; /// - /// The for the . + /// The for the . /// - readonly IRemoteDeploymentManager remoteDeploymentManager; + readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory; /// /// If the should in @@ -180,10 +180,10 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of . /// The value of . /// The value of . - /// The value of . + /// The value of . /// The value of /// The initial value of . May be modified - /// The value of + /// The value of /// The value of protected WatchdogBase( IChatManager chat, @@ -195,10 +195,10 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager remoteDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, - Api.Models.Instance instance, + Api.Models.Instance metadata, bool autoStart) { Chat = chat ?? throw new ArgumentNullException(nameof(chat)); @@ -209,10 +209,10 @@ namespace Tgstation.Server.Host.Components.Watchdog AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); - this.remoteDeploymentManager = remoteDeploymentManager ?? throw new ArgumentNullException(nameof(remoteDeploymentManager)); + this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters)); - this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.autoStart = autoStart; if (serverControl == null) @@ -539,6 +539,9 @@ namespace Tgstation.Server.Host.Components.Watchdog return Task.CompletedTask; } + var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager( + metadata, + newCompileJob); return remoteDeploymentManager.ApplyDeployment(newCompileJob, ActiveCompileJob, cancellationToken); } @@ -910,7 +913,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { Instance = new Models.Instance { - Id = instance.Id + Id = metadata.Id }, Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}", CancelRight = (ulong)DreamDaemonRights.Shutdown, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index d08a666e3a..2185a78586 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager remoteDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerfactory, Api.Models.Instance instance, DreamDaemonSettings settings) => new BasicWatchdog( @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Components.Watchdog AsyncDelayer, diagnosticsIOManager, eventConsumer, - remoteDeploymentManager, + remoteDeploymentManagerfactory, LoggerFactory.CreateLogger(), settings, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 1f40899f7b..1f14c243fc 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -56,7 +56,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . + /// The for the . /// The value of . /// The value of . /// The for the . @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IIOManager gameIOManager, ISymlinkFactory symlinkFactory, ILogger logger, @@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Components.Watchdog asyncDelayer, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManagerFactory, logger, initialLaunchParameters, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 99a18670f3..0f7f140f6f 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IIOManager gameIOManager, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, - IRemoteDeploymentManager gitHubDeploymentManager, + IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, Api.Models.Instance instance, DreamDaemonSettings settings) => new WindowsWatchdog( @@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Components.Watchdog AsyncDelayer, diagnosticsIOManager, eventConsumer, - gitHubDeploymentManager, + remoteDeploymentManagerFactory, gameIOManager, SymlinkFactory, LoggerFactory.CreateLogger(), diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 15c325389e..cf475e6836 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -281,9 +281,7 @@ namespace Tgstation.Server.Host.Controllers } } -#pragma warning disable CA1508 // Avoid dead conditional code if (earlyOut != null) -#pragma warning restore CA1508 // Avoid dead conditional code return earlyOut; // Last test, ensure it's in the list of valid paths diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index c83393aa98..cfd4135299 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Globalization; @@ -14,7 +13,6 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; -using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Jobs; @@ -40,11 +38,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly IJobManager jobManager; - /// - /// The for the - /// - readonly GeneralConfiguration generalConfiguration; - /// /// Construct a /// @@ -54,15 +47,13 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// 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) + ILogger logger) : base( instanceManager, databaseContext, @@ -71,7 +62,6 @@ namespace Tgstation.Server.Host.Controllers { this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } async Task LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action revInfoSink, CancellationToken cancellationToken) @@ -204,7 +194,7 @@ namespace Tgstation.Server.Host.Controllers { var repoManager = core.RepositoryManager; using var repos = await repoManager.CloneRepository( - new Uri(origin), + origin, cloneBranch, currentModel.AccessUser, currentModel.AccessToken, @@ -415,9 +405,7 @@ namespace Tgstation.Server.Host.Controllers || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) return Forbid(); -#pragma warning disable CA1508 // Avoid dead conditional code if (model.AccessToken?.Length == 0 && model.AccessUser?.Length == 0) -#pragma warning restore CA1508 // Avoid dead conditional code { // setting an empty string clears everything currentModel.AccessUser = null; @@ -471,11 +459,11 @@ namespace Tgstation.Server.Host.Controllers description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); if (newTestMerges) - description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}", + description = String.Format(CultureInfo.InvariantCulture, "{0}est merge(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, - x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))), + x.TargetCommitSha != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.TargetCommitSha.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); if (description == null) @@ -649,8 +637,8 @@ namespace Tgstation.Server.Host.Controllers throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); // bit of sanitization - foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision))) - I.PullRequestRevision = null; + foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) + I.TargetCommitSha = null; var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) @@ -667,9 +655,9 @@ namespace Tgstation.Server.Host.Controllers bool cantSearch = false; foreach (var I in model.NewTestMerges) { - if (I.PullRequestRevision != null) + if (I.TargetCommitSha != null) #pragma warning disable CA1308 // Normalize strings to uppercase - I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2 + I.TargetCommitSha = I.TargetCommitSha?.ToLowerInvariant(); // ala libgit2 #pragma warning restore CA1308 // Normalize strings to uppercase else try @@ -678,7 +666,7 @@ namespace Tgstation.Server.Host.Controllers var pr = await repo.GetTestMerge(I, currentModel, ct).ConfigureAwait(false); // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it - I.PullRequestRevision = pr.PullRequestRevision; + I.TargetCommitSha = pr.TargetCommitSha; } catch { @@ -711,7 +699,7 @@ namespace Tgstation.Server.Host.Controllers && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number - && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) + && y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); @@ -736,8 +724,8 @@ namespace Tgstation.Server.Host.Controllers if (!numberMatch) return false; - var shaMatch = testRevInfo.PrimaryTestMerge.PullRequestRevision.StartsWith( - testTestMerge.PullRequestRevision, + var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith( + testTestMerge.TargetCommitSha, StringComparison.Ordinal); if (!shaMatch) return false; @@ -809,7 +797,7 @@ namespace Tgstation.Server.Host.Controllers throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( - $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!")); + $"Test Merge #{I.Number} at {I.TargetCommitSha.Substring(0, 7)} conflicted!")); Models.TestMerge fullTestMerge; try @@ -828,7 +816,7 @@ namespace Tgstation.Server.Host.Controllers TitleAtMerge = ex.Message, Comment = I.Comment, Number = I.Number, - PullRequestRevision = I.PullRequestRevision, + TargetCommitSha = I.TargetCommitSha, Url = ex.Message }; } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 19c25ad761..bb22081409 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -74,9 +74,7 @@ namespace Tgstation.Server.Host.Controllers BadRequestObjectResult CheckValidName(UserUpdate model, bool newUser) { var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null; -#pragma warning disable CA1508 // https://github.com/dotnet/roslyn-analyzers/issues/3685 if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name))) -#pragma warning restore CA1508 return BadRequest(new ErrorMessage(ErrorCode.UserMissingName)); model.Name = model.Name?.Trim(); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 071380e041..1e937fc745 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -26,6 +26,7 @@ using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Providers; +using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Interop.Converters; using Tgstation.Server.Host.Components.Repository; @@ -317,6 +318,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.Designer.cs new file mode 100644 index 0000000000..aee2d7f748 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.Designer.cs @@ -0,0 +1,825 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20201209194250_MSGenericTestMergingUpdate")] + partial class MSGenericTestMergingUpdate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("decimal(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.cs new file mode 100644 index 0000000000..00413a1d03 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194250_MSGenericTestMergingUpdate.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Renames the PullRequestRevision and adds the RepositoryOrigin columns for MSSQL. + /// + public partial class MSGenericTestMergingUpdate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "PullRequestRevision", + table: "TestMerges", + newName: "TargetCommitSha"); + + migrationBuilder.AddColumn( + name: "RepositoryOrigin", + table: "CompileJobs", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "TargetCommitSha", + table: "TestMerges", + newName: "PullRequestRevision"); + + migrationBuilder.DropColumn( + name: "RepositoryOrigin", + table: "CompileJobs"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.Designer.cs new file mode 100644 index 0000000000..b4a2731e53 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.Designer.cs @@ -0,0 +1,814 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20201209194348_MYGenericTestMergingUpdate")] + partial class MYGenericTestMergingUpdate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RepositoryOrigin") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("bigint unsigned"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SystemIdentifier") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.cs new file mode 100644 index 0000000000..d8cf64d164 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194348_MYGenericTestMergingUpdate.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Renames the PullRequestRevision and adds the RepositoryOrigin columns for MYSQL. + /// + public partial class MYGenericTestMergingUpdate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "PullRequestRevision", + table: "TestMerges", + newName: "TargetCommitSha"); + + migrationBuilder.AddColumn( + name: "RepositoryOrigin", + table: "CompileJobs", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "TargetCommitSha", + table: "TestMerges", + newName: "PullRequestRevision"); + + migrationBuilder.DropColumn( + name: "RepositoryOrigin", + table: "CompileJobs"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.Designer.cs new file mode 100644 index 0000000000..95b582de35 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.Designer.cs @@ -0,0 +1,822 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20201209194500_PGGenericTestMergingUpdate")] + partial class PGGenericTestMergingUpdate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RepositoryOrigin") + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("numeric(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.cs new file mode 100644 index 0000000000..33d3005c57 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194500_PGGenericTestMergingUpdate.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Renames the PullRequestRevision and adds the RepositoryOrigin columns for PostgresSQL. + /// + public partial class PGGenericTestMergingUpdate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "PullRequestRevision", + table: "TestMerges", + newName: "TargetCommitSha"); + + migrationBuilder.AddColumn( + name: "RepositoryOrigin", + table: "CompileJobs", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "TargetCommitSha", + table: "TestMerges", + newName: "PullRequestRevision"); + + migrationBuilder.DropColumn( + name: "RepositoryOrigin", + table: "CompileJobs"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.Designer.cs new file mode 100644 index 0000000000..7634e5c21f --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.Designer.cs @@ -0,0 +1,813 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20201209194554_SLGenericTestMergingUpdate")] + partial class SLGenericTestMergingUpdate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondRights") + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstanceUserRights") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs new file mode 100644 index 0000000000..eb0b3617a2 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Renames the PullRequestRevision and adds the RepositoryOrigin columns for SQLite. + /// + public partial class SLGenericTestMergingUpdate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "PullRequestRevision", + table: "TestMerges", + newName: "TargetCommitSha"); + + migrationBuilder.AddColumn( + name: "RepositoryOrigin", + table: "CompileJobs", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameColumn( + name: "TargetCommitSha", + table: "TestMerges", + newName: "PullRequestRevision"); + + migrationBuilder.DropColumn( + name: "RepositoryOrigin", + table: "CompileJobs"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index 6859a6252b..5d653e7651 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -17,792 +17,795 @@ namespace Tgstation.Server.Host.Database.Migrations .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("ChannelLimit") - .IsRequired() - .HasColumnType("smallint unsigned"); + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("Enabled") - .HasColumnType("tinyint(1)"); + b.Property("Enabled") + .HasColumnType("tinyint(1)"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("Name") - .IsRequired() - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("int"); + b.Property("Provider") + .HasColumnType("int"); - b.Property("ReconnectionInterval") - .IsRequired() - .HasColumnType("int unsigned"); + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "Name") - .IsUnique(); + b.HasIndex("InstanceId", "Name") + .IsUnique(); - b.ToTable("ChatBots"); - }); + b.ToTable("ChatBots"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("ChatSettingsId") - .HasColumnType("bigint"); + b.Property("ChatSettingsId") + .HasColumnType("bigint"); - b.Property("DiscordChannelId") - .HasColumnType("bigint unsigned"); + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); - b.Property("IrcChannel") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("Tag") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique(); + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique(); + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); - b.ToTable("ChatChannels"); - }); + b.ToTable("ChatChannels"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("DMApiMajorVersion") - .HasColumnType("int"); + b.Property("DMApiMajorVersion") + .HasColumnType("int"); - b.Property("DMApiMinorVersion") - .HasColumnType("int"); + b.Property("DMApiMinorVersion") + .HasColumnType("int"); - b.Property("DMApiPatchVersion") - .HasColumnType("int"); + b.Property("DMApiPatchVersion") + .HasColumnType("int"); - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("char(36)"); + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); - b.Property("DmeName") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("GitHubDeploymentId") - .HasColumnType("int"); + b.Property("GitHubDeploymentId") + .HasColumnType("int"); - b.Property("GitHubRepoId") - .HasColumnType("bigint"); + b.Property("GitHubRepoId") + .HasColumnType("bigint"); - b.Property("JobId") - .HasColumnType("bigint"); + b.Property("JobId") + .HasColumnType("bigint"); - b.Property("MinimumSecurityLevel") - .HasColumnType("int"); + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); - b.Property("Output") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("RevisionInformationId") - .HasColumnType("bigint"); + b.Property("RepositoryOrigin") + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.HasKey("Id"); + b.Property("RevisionInformationId") + .HasColumnType("bigint"); - b.HasIndex("DirectoryName"); + b.HasKey("Id"); - b.HasIndex("JobId") - .IsUnique(); + b.HasIndex("DirectoryName"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("JobId") + .IsUnique(); - b.ToTable("CompileJobs"); - }); + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("AutoStart") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("HeartbeatSeconds") - .IsRequired() - .HasColumnType("int unsigned"); + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("Port") - .IsRequired() - .HasColumnType("smallint unsigned"); + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); - b.Property("SecurityLevel") - .HasColumnType("int"); + b.Property("SecurityLevel") + .HasColumnType("int"); - b.Property("StartupTimeout") - .IsRequired() - .HasColumnType("int unsigned"); + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); - b.Property("TopicRequestTimeout") - .IsRequired() - .HasColumnType("int unsigned"); + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamDaemonSettings"); - }); + b.ToTable("DreamDaemonSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("ApiValidationPort") - .IsRequired() - .HasColumnType("smallint unsigned"); + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); - b.Property("ApiValidationSecurityLevel") - .HasColumnType("int"); + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("ProjectName") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamMakerSettings"); - }); + b.ToTable("DreamMakerSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("AutoUpdateInterval") - .IsRequired() - .HasColumnType("int unsigned"); + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); - b.Property("ChatBotLimit") - .IsRequired() - .HasColumnType("smallint unsigned"); + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); - b.Property("ConfigurationType") - .HasColumnType("int"); + b.Property("ConfigurationType") + .HasColumnType("int"); - b.Property("Name") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("Online") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("Path") - .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("Path") - .IsUnique(); + b.HasIndex("Path") + .IsUnique(); - b.ToTable("Instances"); - }); + b.ToTable("Instances"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("ByondRights") - .HasColumnType("bigint unsigned"); + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); - b.Property("ChatBotRights") - .HasColumnType("bigint unsigned"); + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); - b.Property("ConfigurationRights") - .HasColumnType("bigint unsigned"); + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); - b.Property("DreamDaemonRights") - .HasColumnType("bigint unsigned"); + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); - b.Property("DreamMakerRights") - .HasColumnType("bigint unsigned"); + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("InstanceUserRights") - .HasColumnType("bigint unsigned"); + b.Property("InstanceUserRights") + .HasColumnType("bigint unsigned"); - b.Property("RepositoryRights") - .HasColumnType("bigint unsigned"); + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("UserId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") - .IsUnique(); + b.HasIndex("UserId", "InstanceId") + .IsUnique(); - b.ToTable("InstanceUsers"); - }); + b.ToTable("InstanceUsers"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("CancelRight") - .HasColumnType("bigint unsigned"); + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); - b.Property("CancelRightsType") - .HasColumnType("bigint unsigned"); + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); - b.Property("Cancelled") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("CancelledById") - .HasColumnType("bigint"); + b.Property("CancelledById") + .HasColumnType("bigint"); - b.Property("Description") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("ErrorCode") - .HasColumnType("int unsigned"); + b.Property("ErrorCode") + .HasColumnType("int unsigned"); - b.Property("ExceptionDetails") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("StartedAt") - .IsRequired() - .HasColumnType("datetime(6)"); + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); - b.Property("StartedById") - .HasColumnType("bigint"); + b.Property("StartedById") + .HasColumnType("bigint"); - b.Property("StoppedAt") - .HasColumnType("datetime(6)"); + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CancelledById"); + b.HasIndex("CancelledById"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("StartedById"); + b.HasIndex("StartedById"); - b.ToTable("Jobs"); - }); + b.ToTable("Jobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("int"); + b.Property("Provider") + .HasColumnType("int"); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("UserId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("UserId"); + b.HasIndex("UserId"); - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); - b.ToTable("OAuthConnections"); - }); + b.ToTable("OAuthConnections"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("CompileJobId") - .HasColumnType("bigint"); + b.Property("CompileJobId") + .HasColumnType("bigint"); - b.Property("LaunchSecurityLevel") - .HasColumnType("int"); + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); - b.Property("Port") - .HasColumnType("smallint unsigned"); + b.Property("Port") + .HasColumnType("smallint unsigned"); - b.Property("ProcessId") - .HasColumnType("int"); + b.Property("ProcessId") + .HasColumnType("int"); - b.Property("RebootState") - .HasColumnType("int"); + b.Property("RebootState") + .HasColumnType("int"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CompileJobId"); + b.HasIndex("CompileJobId"); - b.ToTable("ReattachInformations"); - }); + b.ToTable("ReattachInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("AccessToken") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("AccessUser") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("CommitterName") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("RepositorySettings"); - }); + b.ToTable("RepositorySettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("RevisionInformationId") - .HasColumnType("bigint"); + b.Property("RevisionInformationId") + .HasColumnType("bigint"); - b.Property("TestMergeId") - .HasColumnType("bigint"); + b.Property("TestMergeId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("RevisionInformationId"); - b.HasIndex("TestMergeId"); + b.HasIndex("TestMergeId"); - b.ToTable("RevInfoTestMerges"); - }); + b.ToTable("RevInfoTestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("CommitSha") - .IsRequired() - .HasColumnType("varchar(40) CHARACTER SET utf8mb4") - .HasMaxLength(40); + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("varchar(40) CHARACTER SET utf8mb4") - .HasMaxLength(40); + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); - b.ToTable("RevisionInformations"); - }); + b.ToTable("RevisionInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("Author") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("Comment") - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("MergedAt") - .HasColumnType("datetime(6)"); + b.Property("MergedAt") + .HasColumnType("datetime(6)"); - b.Property("MergedById") - .HasColumnType("bigint"); + b.Property("MergedById") + .HasColumnType("bigint"); - b.Property("Number") - .HasColumnType("int"); + b.Property("Number") + .HasColumnType("int"); - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("bigint"); + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("varchar(40) CHARACTER SET utf8mb4") - .HasMaxLength(40); + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("Url") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("MergedById"); + b.HasIndex("MergedById"); - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); - b.ToTable("TestMerges"); - }); + b.ToTable("TestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); - b.Property("AdministrationRights") - .HasColumnType("bigint unsigned"); + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("datetime(6)"); + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); - b.Property("CreatedById") - .HasColumnType("bigint"); + b.Property("CreatedById") + .HasColumnType("bigint"); - b.Property("Enabled") - .IsRequired() - .HasColumnType("tinyint(1)"); + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); - b.Property("InstanceManagerRights") - .HasColumnType("bigint unsigned"); + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); - b.Property("LastPasswordUpdate") - .HasColumnType("datetime(6)"); + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); - b.Property("Name") - .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); - b.Property("PasswordHash") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); - b.Property("SystemIdentifier") - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + b.Property("SystemIdentifier") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CanonicalName") - .IsUnique(); + b.HasIndex("CanonicalName") + .IsUnique(); - b.HasIndex("CreatedById"); + b.HasIndex("CreatedById"); - b.HasIndex("SystemIdentifier") - .IsUnique(); + b.HasIndex("SystemIdentifier") + .IsUnique(); - b.ToTable("Users"); - }); + b.ToTable("Users"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index 5cfd13f687..6d4d6e91a4 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -19,798 +19,801 @@ namespace Tgstation.Server.Host.Database.Migrations .HasAnnotation("Relational:MaxIdentifierLength", 63); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ChannelLimit") - .HasColumnType("integer"); + b.Property("ChannelLimit") + .HasColumnType("integer"); - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("Enabled") - .HasColumnType("boolean"); + b.Property("Enabled") + .HasColumnType("boolean"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("Name") - .IsRequired() - .HasColumnType("character varying(100)") - .HasMaxLength(100); + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("integer"); + b.Property("Provider") + .HasColumnType("integer"); - b.Property("ReconnectionInterval") - .HasColumnType("bigint"); + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "Name") - .IsUnique(); + b.HasIndex("InstanceId", "Name") + .IsUnique(); - b.ToTable("ChatBots"); - }); + b.ToTable("ChatBots"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ChatSettingsId") - .HasColumnType("bigint"); + b.Property("ChatSettingsId") + .HasColumnType("bigint"); - b.Property("DiscordChannelId") - .HasColumnType("numeric(20,0)"); + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); - b.Property("IrcChannel") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("boolean"); + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("boolean"); + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("boolean"); + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); - b.Property("Tag") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("Tag") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique(); + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique(); + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); - b.ToTable("ChatChannels"); - }); + b.ToTable("ChatChannels"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("text"); + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); - b.Property("DMApiMajorVersion") - .HasColumnType("integer"); + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); - b.Property("DMApiMinorVersion") - .HasColumnType("integer"); + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); - b.Property("DMApiPatchVersion") - .HasColumnType("integer"); + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("uuid"); + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); - b.Property("DmeName") - .IsRequired() - .HasColumnType("text"); + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); - b.Property("GitHubDeploymentId") - .HasColumnType("integer"); + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); - b.Property("GitHubRepoId") - .HasColumnType("bigint"); + b.Property("GitHubRepoId") + .HasColumnType("bigint"); - b.Property("JobId") - .HasColumnType("bigint"); + b.Property("JobId") + .HasColumnType("bigint"); - b.Property("MinimumSecurityLevel") - .HasColumnType("integer"); + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); - b.Property("Output") - .IsRequired() - .HasColumnType("text"); + b.Property("Output") + .IsRequired() + .HasColumnType("text"); - b.Property("RevisionInformationId") - .HasColumnType("bigint"); + b.Property("RepositoryOrigin") + .HasColumnType("text"); - b.HasKey("Id"); + b.Property("RevisionInformationId") + .HasColumnType("bigint"); - b.HasIndex("DirectoryName"); + b.HasKey("Id"); - b.HasIndex("JobId") - .IsUnique(); + b.HasIndex("DirectoryName"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("JobId") + .IsUnique(); - b.ToTable("CompileJobs"); - }); + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("boolean"); + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); - b.Property("AutoStart") - .IsRequired() - .HasColumnType("boolean"); + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); - b.Property("HeartbeatSeconds") - .HasColumnType("bigint"); + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("Port") - .HasColumnType("integer"); + b.Property("Port") + .HasColumnType("integer"); - b.Property("SecurityLevel") - .HasColumnType("integer"); + b.Property("SecurityLevel") + .HasColumnType("integer"); - b.Property("StartupTimeout") - .HasColumnType("bigint"); + b.Property("StartupTimeout") + .HasColumnType("bigint"); - b.Property("TopicRequestTimeout") - .HasColumnType("bigint"); + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamDaemonSettings"); - }); + b.ToTable("DreamDaemonSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ApiValidationPort") - .HasColumnType("integer"); + b.Property("ApiValidationPort") + .HasColumnType("integer"); - b.Property("ApiValidationSecurityLevel") - .HasColumnType("integer"); + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("ProjectName") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("boolean"); + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamMakerSettings"); - }); + b.ToTable("DreamMakerSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AutoUpdateInterval") - .HasColumnType("bigint"); + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); - b.Property("ChatBotLimit") - .HasColumnType("integer"); + b.Property("ChatBotLimit") + .HasColumnType("integer"); - b.Property("ConfigurationType") - .HasColumnType("integer"); + b.Property("ConfigurationType") + .HasColumnType("integer"); - b.Property("Name") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("Online") - .IsRequired() - .HasColumnType("boolean"); + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); - b.Property("Path") - .IsRequired() - .HasColumnType("text"); + b.Property("Path") + .IsRequired() + .HasColumnType("text"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("Path") - .IsUnique(); + b.HasIndex("Path") + .IsUnique(); - b.ToTable("Instances"); - }); + b.ToTable("Instances"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ByondRights") - .HasColumnType("numeric(20,0)"); + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); - b.Property("ChatBotRights") - .HasColumnType("numeric(20,0)"); + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); - b.Property("ConfigurationRights") - .HasColumnType("numeric(20,0)"); + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); - b.Property("DreamDaemonRights") - .HasColumnType("numeric(20,0)"); + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); - b.Property("DreamMakerRights") - .HasColumnType("numeric(20,0)"); + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("InstanceUserRights") - .HasColumnType("numeric(20,0)"); + b.Property("InstanceUserRights") + .HasColumnType("numeric(20,0)"); - b.Property("RepositoryRights") - .HasColumnType("numeric(20,0)"); + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("UserId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") - .IsUnique(); + b.HasIndex("UserId", "InstanceId") + .IsUnique(); - b.ToTable("InstanceUsers"); - }); + b.ToTable("InstanceUsers"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("CancelRight") - .HasColumnType("numeric(20,0)"); + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); - b.Property("CancelRightsType") - .HasColumnType("numeric(20,0)"); + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); - b.Property("Cancelled") - .IsRequired() - .HasColumnType("boolean"); + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); - b.Property("CancelledById") - .HasColumnType("bigint"); + b.Property("CancelledById") + .HasColumnType("bigint"); - b.Property("Description") - .IsRequired() - .HasColumnType("text"); + b.Property("Description") + .IsRequired() + .HasColumnType("text"); - b.Property("ErrorCode") - .HasColumnType("bigint"); + b.Property("ErrorCode") + .HasColumnType("bigint"); - b.Property("ExceptionDetails") - .HasColumnType("text"); + b.Property("ExceptionDetails") + .HasColumnType("text"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("StartedAt") - .IsRequired() - .HasColumnType("timestamp with time zone"); + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); - b.Property("StartedById") - .HasColumnType("bigint"); + b.Property("StartedById") + .HasColumnType("bigint"); - b.Property("StoppedAt") - .HasColumnType("timestamp with time zone"); + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CancelledById"); + b.HasIndex("CancelledById"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("StartedById"); + b.HasIndex("StartedById"); - b.ToTable("Jobs"); - }); + b.ToTable("Jobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("character varying(100)") - .HasMaxLength(100); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("integer"); + b.Property("Provider") + .HasColumnType("integer"); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("UserId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("UserId"); + b.HasIndex("UserId"); - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); - b.ToTable("OAuthConnections"); - }); + b.ToTable("OAuthConnections"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("text"); + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); - b.Property("CompileJobId") - .HasColumnType("bigint"); + b.Property("CompileJobId") + .HasColumnType("bigint"); - b.Property("LaunchSecurityLevel") - .HasColumnType("integer"); + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); - b.Property("Port") - .HasColumnType("integer"); + b.Property("Port") + .HasColumnType("integer"); - b.Property("ProcessId") - .HasColumnType("integer"); + b.Property("ProcessId") + .HasColumnType("integer"); - b.Property("RebootState") - .HasColumnType("integer"); + b.Property("RebootState") + .HasColumnType("integer"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CompileJobId"); + b.HasIndex("CompileJobId"); - b.ToTable("ReattachInformations"); - }); + b.ToTable("ReattachInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AccessToken") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("AccessUser") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("boolean"); + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("boolean"); + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("CommitterName") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("boolean"); + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("boolean"); + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("boolean"); + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("boolean"); + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("RepositorySettings"); - }); + b.ToTable("RepositorySettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("RevisionInformationId") - .HasColumnType("bigint"); + b.Property("RevisionInformationId") + .HasColumnType("bigint"); - b.Property("TestMergeId") - .HasColumnType("bigint"); + b.Property("TestMergeId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("RevisionInformationId"); - b.HasIndex("TestMergeId"); + b.HasIndex("TestMergeId"); - b.ToTable("RevInfoTestMerges"); - }); + b.ToTable("RevInfoTestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("CommitSha") - .IsRequired() - .HasColumnType("character varying(40)") - .HasMaxLength(40); + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("character varying(40)") - .HasMaxLength(40); + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); - b.ToTable("RevisionInformations"); - }); + b.ToTable("RevisionInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("Author") - .IsRequired() - .HasColumnType("text"); + b.Property("Author") + .IsRequired() + .HasColumnType("text"); - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("text"); + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); - b.Property("Comment") - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("MergedAt") - .HasColumnType("timestamp with time zone"); + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); - b.Property("MergedById") - .HasColumnType("bigint"); + b.Property("MergedById") + .HasColumnType("bigint"); - b.Property("Number") - .HasColumnType("integer"); + b.Property("Number") + .HasColumnType("integer"); - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("bigint"); + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("character varying(40)") - .HasMaxLength(40); + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("text"); + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); - b.Property("Url") - .IsRequired() - .HasColumnType("text"); + b.Property("Url") + .IsRequired() + .HasColumnType("text"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("MergedById"); + b.HasIndex("MergedById"); - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); - b.ToTable("TestMerges"); - }); + b.ToTable("TestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AdministrationRights") - .HasColumnType("numeric(20,0)"); + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("text"); + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("text"); - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); - b.Property("CreatedById") - .HasColumnType("bigint"); + b.Property("CreatedById") + .HasColumnType("bigint"); - b.Property("Enabled") - .IsRequired() - .HasColumnType("boolean"); + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); - b.Property("InstanceManagerRights") - .HasColumnType("numeric(20,0)"); + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); - b.Property("LastPasswordUpdate") - .HasColumnType("timestamp with time zone"); + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); - b.Property("Name") - .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); - b.Property("PasswordHash") - .HasColumnType("text"); + b.Property("PasswordHash") + .HasColumnType("text"); - b.Property("SystemIdentifier") - .HasColumnType("text"); + b.Property("SystemIdentifier") + .HasColumnType("text"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CanonicalName") - .IsUnique(); + b.HasIndex("CanonicalName") + .IsUnique(); - b.HasIndex("CreatedById"); + b.HasIndex("CreatedById"); - b.HasIndex("SystemIdentifier") - .IsUnique(); + b.HasIndex("SystemIdentifier") + .IsUnique(); - b.ToTable("Users"); - }); + b.ToTable("Users"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index 8fba8e0ec6..44dab83a79 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -19,801 +19,804 @@ namespace Tgstation.Server.Host.Database.Migrations .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ChannelLimit") - .HasColumnType("int"); + b.Property("ChannelLimit") + .HasColumnType("int"); - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("Enabled") - .HasColumnType("bit"); + b.Property("Enabled") + .HasColumnType("bit"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("int"); + b.Property("Provider") + .HasColumnType("int"); - b.Property("ReconnectionInterval") - .HasColumnType("bigint"); + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "Name") - .IsUnique(); + b.HasIndex("InstanceId", "Name") + .IsUnique(); - b.ToTable("ChatBots"); - }); + b.ToTable("ChatBots"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ChatSettingsId") - .HasColumnType("bigint"); + b.Property("ChatSettingsId") + .HasColumnType("bigint"); - b.Property("DiscordChannelId") - .HasColumnType("decimal(20,0)"); + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); - b.Property("IrcChannel") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + b.Property("IrcChannel") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("bit"); + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("bit"); + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("bit"); + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); - b.Property("Tag") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("Tag") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique() - .HasFilter("[DiscordChannelId] IS NOT NULL"); + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique() - .HasFilter("[IrcChannel] IS NOT NULL"); + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); - b.ToTable("ChatChannels"); - }); + b.ToTable("ChatChannels"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("DMApiMajorVersion") - .HasColumnType("int"); + b.Property("DMApiMajorVersion") + .HasColumnType("int"); - b.Property("DMApiMinorVersion") - .HasColumnType("int"); + b.Property("DMApiMinorVersion") + .HasColumnType("int"); - b.Property("DMApiPatchVersion") - .HasColumnType("int"); + b.Property("DMApiPatchVersion") + .HasColumnType("int"); - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("uniqueidentifier"); + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); - b.Property("DmeName") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("GitHubDeploymentId") - .HasColumnType("int"); + b.Property("GitHubDeploymentId") + .HasColumnType("int"); - b.Property("GitHubRepoId") - .HasColumnType("bigint"); + b.Property("GitHubRepoId") + .HasColumnType("bigint"); - b.Property("JobId") - .HasColumnType("bigint"); + b.Property("JobId") + .HasColumnType("bigint"); - b.Property("MinimumSecurityLevel") - .HasColumnType("int"); + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); - b.Property("Output") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("RevisionInformationId") - .HasColumnType("bigint"); + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); - b.HasKey("Id"); + b.Property("RevisionInformationId") + .HasColumnType("bigint"); - b.HasIndex("DirectoryName"); + b.HasKey("Id"); - b.HasIndex("JobId") - .IsUnique(); + b.HasIndex("DirectoryName"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("JobId") + .IsUnique(); - b.ToTable("CompileJobs"); - }); + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("bit"); + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); - b.Property("AutoStart") - .IsRequired() - .HasColumnType("bit"); + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); - b.Property("HeartbeatSeconds") - .HasColumnType("bigint"); + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("Port") - .HasColumnType("int"); + b.Property("Port") + .HasColumnType("int"); - b.Property("SecurityLevel") - .HasColumnType("int"); + b.Property("SecurityLevel") + .HasColumnType("int"); - b.Property("StartupTimeout") - .HasColumnType("bigint"); + b.Property("StartupTimeout") + .HasColumnType("bigint"); - b.Property("TopicRequestTimeout") - .HasColumnType("bigint"); + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamDaemonSettings"); - }); + b.ToTable("DreamDaemonSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ApiValidationPort") - .HasColumnType("int"); + b.Property("ApiValidationPort") + .HasColumnType("int"); - b.Property("ApiValidationSecurityLevel") - .HasColumnType("int"); + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("ProjectName") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("ProjectName") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("bit"); + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamMakerSettings"); - }); + b.ToTable("DreamMakerSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("AutoUpdateInterval") - .HasColumnType("bigint"); + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); - b.Property("ChatBotLimit") - .HasColumnType("int"); + b.Property("ChatBotLimit") + .HasColumnType("int"); - b.Property("ConfigurationType") - .HasColumnType("int"); + b.Property("ConfigurationType") + .HasColumnType("int"); - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("Online") - .IsRequired() - .HasColumnType("bit"); + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); - b.Property("Path") - .IsRequired() - .HasColumnType("nvarchar(450)"); + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("Path") - .IsUnique(); + b.HasIndex("Path") + .IsUnique(); - b.ToTable("Instances"); - }); + b.ToTable("Instances"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ByondRights") - .HasColumnType("decimal(20,0)"); + b.Property("ByondRights") + .HasColumnType("decimal(20,0)"); - b.Property("ChatBotRights") - .HasColumnType("decimal(20,0)"); + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); - b.Property("ConfigurationRights") - .HasColumnType("decimal(20,0)"); + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); - b.Property("DreamDaemonRights") - .HasColumnType("decimal(20,0)"); + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); - b.Property("DreamMakerRights") - .HasColumnType("decimal(20,0)"); + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("InstanceUserRights") - .HasColumnType("decimal(20,0)"); + b.Property("InstanceUserRights") + .HasColumnType("decimal(20,0)"); - b.Property("RepositoryRights") - .HasColumnType("decimal(20,0)"); + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("UserId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") - .IsUnique(); + b.HasIndex("UserId", "InstanceId") + .IsUnique(); - b.ToTable("InstanceUsers"); - }); + b.ToTable("InstanceUsers"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("CancelRight") - .HasColumnType("decimal(20,0)"); + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); - b.Property("CancelRightsType") - .HasColumnType("decimal(20,0)"); + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); - b.Property("Cancelled") - .IsRequired() - .HasColumnType("bit"); + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); - b.Property("CancelledById") - .HasColumnType("bigint"); + b.Property("CancelledById") + .HasColumnType("bigint"); - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("ErrorCode") - .HasColumnType("bigint"); + b.Property("ErrorCode") + .HasColumnType("bigint"); - b.Property("ExceptionDetails") - .HasColumnType("nvarchar(max)"); + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("StartedAt") - .IsRequired() - .HasColumnType("datetimeoffset"); + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); - b.Property("StartedById") - .HasColumnType("bigint"); + b.Property("StartedById") + .HasColumnType("bigint"); - b.Property("StoppedAt") - .HasColumnType("datetimeoffset"); + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CancelledById"); + b.HasIndex("CancelledById"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("StartedById"); + b.HasIndex("StartedById"); - b.ToTable("Jobs"); - }); + b.ToTable("Jobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("int"); + b.Property("Provider") + .HasColumnType("int"); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("UserId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("UserId"); + b.HasIndex("UserId"); - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); - b.ToTable("OAuthConnections"); - }); + b.ToTable("OAuthConnections"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("CompileJobId") - .HasColumnType("bigint"); + b.Property("CompileJobId") + .HasColumnType("bigint"); - b.Property("LaunchSecurityLevel") - .HasColumnType("int"); + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); - b.Property("Port") - .HasColumnType("int"); + b.Property("Port") + .HasColumnType("int"); - b.Property("ProcessId") - .HasColumnType("int"); + b.Property("ProcessId") + .HasColumnType("int"); - b.Property("RebootState") - .HasColumnType("int"); + b.Property("RebootState") + .HasColumnType("int"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CompileJobId"); + b.HasIndex("CompileJobId"); - b.ToTable("ReattachInformations"); - }); + b.ToTable("ReattachInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("AccessToken") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("AccessToken") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("AccessUser") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("AccessUser") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("bit"); + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("bit"); + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("CommitterName") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("CommitterName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("bit"); + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("bit"); + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("bit"); + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("bit"); + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("RepositorySettings"); - }); + b.ToTable("RepositorySettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("RevisionInformationId") - .HasColumnType("bigint"); + b.Property("RevisionInformationId") + .HasColumnType("bigint"); - b.Property("TestMergeId") - .HasColumnType("bigint"); + b.Property("TestMergeId") + .HasColumnType("bigint"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("RevisionInformationId"); - b.HasIndex("TestMergeId"); + b.HasIndex("TestMergeId"); - b.ToTable("RevInfoTestMerges"); - }); + b.ToTable("RevInfoTestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("CommitSha") - .IsRequired() - .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + b.Property("CommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); - b.Property("InstanceId") - .HasColumnType("bigint"); + b.Property("InstanceId") + .HasColumnType("bigint"); - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); - b.ToTable("RevisionInformations"); - }); + b.ToTable("RevisionInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("Author") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("Comment") - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("Comment") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("MergedAt") - .HasColumnType("datetimeoffset"); + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); - b.Property("MergedById") - .HasColumnType("bigint"); + b.Property("MergedById") + .HasColumnType("bigint"); - b.Property("Number") - .HasColumnType("int"); + b.Property("Number") + .HasColumnType("int"); - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("bigint"); + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.Property("Url") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("MergedById"); + b.HasIndex("MergedById"); - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); - b.ToTable("TestMerges"); - }); + b.ToTable("TestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("AdministrationRights") - .HasColumnType("decimal(20,0)"); + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("nvarchar(450)"); + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("nvarchar(450)"); - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("datetimeoffset"); + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); - b.Property("CreatedById") - .HasColumnType("bigint"); + b.Property("CreatedById") + .HasColumnType("bigint"); - b.Property("Enabled") - .IsRequired() - .HasColumnType("bit"); + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); - b.Property("InstanceManagerRights") - .HasColumnType("decimal(20,0)"); + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); - b.Property("LastPasswordUpdate") - .HasColumnType("datetimeoffset"); + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); - b.Property("PasswordHash") - .HasColumnType("nvarchar(max)"); + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); - b.Property("SystemIdentifier") - .HasColumnType("nvarchar(450)"); + b.Property("SystemIdentifier") + .HasColumnType("nvarchar(450)"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CanonicalName") - .IsUnique(); + b.HasIndex("CanonicalName") + .IsUnique(); - b.HasIndex("CreatedById"); + b.HasIndex("CreatedById"); - b.HasIndex("SystemIdentifier") - .IsUnique() - .HasFilter("[SystemIdentifier] IS NOT NULL"); + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); - b.ToTable("Users"); - }); + b.ToTable("Users"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index c7ffe85f0f..d67f026abb 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -16,792 +16,795 @@ namespace Tgstation.Server.Host.Database.Migrations .HasAnnotation("ProductVersion", "3.1.10"); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ChannelLimit") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("ConnectionString") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("Enabled") - .HasColumnType("INTEGER"); + b.Property("Enabled") + .HasColumnType("INTEGER"); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(100); + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("INTEGER"); + b.Property("Provider") + .HasColumnType("INTEGER"); - b.Property("ReconnectionInterval") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "Name") - .IsUnique(); + b.HasIndex("InstanceId", "Name") + .IsUnique(); - b.ToTable("ChatBots"); - }); + b.ToTable("ChatBots"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ChatSettingsId") - .HasColumnType("INTEGER"); + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); - b.Property("DiscordChannelId") - .HasColumnType("INTEGER"); + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); - b.Property("IrcChannel") - .HasColumnType("TEXT") - .HasMaxLength(100); + b.Property("IrcChannel") + .HasColumnType("TEXT") + .HasMaxLength(100); - b.Property("IsAdminChannel") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("IsUpdatesChannel") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("IsWatchdogChannel") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("Tag") - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("Tag") + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("ChatSettingsId", "DiscordChannelId") - .IsUnique(); + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); - b.HasIndex("ChatSettingsId", "IrcChannel") - .IsUnique(); + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); - b.ToTable("ChatChannels"); - }); + b.ToTable("ChatChannels"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("DMApiMajorVersion") - .HasColumnType("INTEGER"); + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); - b.Property("DMApiMinorVersion") - .HasColumnType("INTEGER"); + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); - b.Property("DMApiPatchVersion") - .HasColumnType("INTEGER"); + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); - b.Property("DirectoryName") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("DmeName") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("GitHubDeploymentId") - .HasColumnType("INTEGER"); + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); - b.Property("GitHubRepoId") - .HasColumnType("INTEGER"); + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); - b.Property("JobId") - .HasColumnType("INTEGER"); + b.Property("JobId") + .HasColumnType("INTEGER"); - b.Property("MinimumSecurityLevel") - .HasColumnType("INTEGER"); + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); - b.Property("Output") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("RevisionInformationId") - .HasColumnType("INTEGER"); + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); - b.HasKey("Id"); + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); - b.HasIndex("DirectoryName"); + b.HasKey("Id"); - b.HasIndex("JobId") - .IsUnique(); + b.HasIndex("DirectoryName"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("JobId") + .IsUnique(); - b.ToTable("CompileJobs"); - }); + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AdditionalParameters") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("AllowWebClient") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("AutoStart") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("HeartbeatSeconds") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("Port") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("SecurityLevel") - .HasColumnType("INTEGER"); + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); - b.Property("StartupTimeout") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("TopicRequestTimeout") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamDaemonSettings"); - }); + b.ToTable("DreamDaemonSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ApiValidationPort") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("ApiValidationSecurityLevel") - .HasColumnType("INTEGER"); + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("ProjectName") - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("ProjectName") + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("RequireDMApiValidation") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("DreamMakerSettings"); - }); + b.ToTable("DreamMakerSettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AutoUpdateInterval") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("ChatBotLimit") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("ConfigurationType") - .HasColumnType("INTEGER"); + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("Online") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("Path") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("Path") - .IsUnique(); + b.HasIndex("Path") + .IsUnique(); - b.ToTable("Instances"); - }); + b.ToTable("Instances"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ByondRights") - .HasColumnType("INTEGER"); + b.Property("ByondRights") + .HasColumnType("INTEGER"); - b.Property("ChatBotRights") - .HasColumnType("INTEGER"); + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); - b.Property("ConfigurationRights") - .HasColumnType("INTEGER"); + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); - b.Property("DreamDaemonRights") - .HasColumnType("INTEGER"); + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); - b.Property("DreamMakerRights") - .HasColumnType("INTEGER"); + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("InstanceUserRights") - .HasColumnType("INTEGER"); + b.Property("InstanceUserRights") + .HasColumnType("INTEGER"); - b.Property("RepositoryRights") - .HasColumnType("INTEGER"); + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); - b.Property("UserId") - .HasColumnType("INTEGER"); + b.Property("UserId") + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") - .IsUnique(); + b.HasIndex("UserId", "InstanceId") + .IsUnique(); - b.ToTable("InstanceUsers"); - }); + b.ToTable("InstanceUsers"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("CancelRight") - .HasColumnType("INTEGER"); + b.Property("CancelRight") + .HasColumnType("INTEGER"); - b.Property("CancelRightsType") - .HasColumnType("INTEGER"); + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); - b.Property("Cancelled") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("CancelledById") - .HasColumnType("INTEGER"); + b.Property("CancelledById") + .HasColumnType("INTEGER"); - b.Property("Description") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("ErrorCode") - .HasColumnType("INTEGER"); + b.Property("ErrorCode") + .HasColumnType("INTEGER"); - b.Property("ExceptionDetails") - .HasColumnType("TEXT"); + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("StartedAt") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("StartedById") - .HasColumnType("INTEGER"); + b.Property("StartedById") + .HasColumnType("INTEGER"); - b.Property("StoppedAt") - .HasColumnType("TEXT"); + b.Property("StoppedAt") + .HasColumnType("TEXT"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CancelledById"); + b.HasIndex("CancelledById"); - b.HasIndex("InstanceId"); + b.HasIndex("InstanceId"); - b.HasIndex("StartedById"); + b.HasIndex("StartedById"); - b.ToTable("Jobs"); - }); + b.ToTable("Jobs"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ExternalUserId") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(100); + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); - b.Property("Provider") - .HasColumnType("INTEGER"); + b.Property("Provider") + .HasColumnType("INTEGER"); - b.Property("UserId") - .HasColumnType("INTEGER"); + b.Property("UserId") + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("UserId"); + b.HasIndex("UserId"); - b.HasIndex("Provider", "ExternalUserId") - .IsUnique(); + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); - b.ToTable("OAuthConnections"); - }); + b.ToTable("OAuthConnections"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AccessIdentifier") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("CompileJobId") - .HasColumnType("INTEGER"); + b.Property("CompileJobId") + .HasColumnType("INTEGER"); - b.Property("LaunchSecurityLevel") - .HasColumnType("INTEGER"); + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); - b.Property("Port") - .HasColumnType("INTEGER"); + b.Property("Port") + .HasColumnType("INTEGER"); - b.Property("ProcessId") - .HasColumnType("INTEGER"); + b.Property("ProcessId") + .HasColumnType("INTEGER"); - b.Property("RebootState") - .HasColumnType("INTEGER"); + b.Property("RebootState") + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CompileJobId"); + b.HasIndex("CompileJobId"); - b.ToTable("ReattachInformations"); - }); + b.ToTable("ReattachInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AccessToken") - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("AccessToken") + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("AccessUser") - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("AccessUser") + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("AutoUpdatesKeepTestMerges") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("AutoUpdatesSynchronize") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("CommitterEmail") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("CommitterName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("CommitterName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("CreateGitHubDeployments") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("PostTestMergeComment") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("PushTestMergeCommits") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("ShowTestMergeCommitters") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId") - .IsUnique(); + b.HasIndex("InstanceId") + .IsUnique(); - b.ToTable("RepositorySettings"); - }); + b.ToTable("RepositorySettings"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("RevisionInformationId") - .HasColumnType("INTEGER"); + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); - b.Property("TestMergeId") - .HasColumnType("INTEGER"); + b.Property("TestMergeId") + .HasColumnType("INTEGER"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("RevisionInformationId"); + b.HasIndex("RevisionInformationId"); - b.HasIndex("TestMergeId"); + b.HasIndex("TestMergeId"); - b.ToTable("RevInfoTestMerges"); - }); + b.ToTable("RevInfoTestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("CommitSha") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(40); + b.Property("CommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); - b.Property("InstanceId") - .HasColumnType("INTEGER"); + b.Property("InstanceId") + .HasColumnType("INTEGER"); - b.Property("OriginCommitSha") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(40); + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("InstanceId", "CommitSha") - .IsUnique(); + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); - b.ToTable("RevisionInformations"); - }); + b.ToTable("RevisionInformations"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("Author") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("BodyAtMerge") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("Comment") - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("MergedAt") - .HasColumnType("TEXT"); + b.Property("MergedAt") + .HasColumnType("TEXT"); - b.Property("MergedById") - .HasColumnType("INTEGER"); + b.Property("MergedById") + .HasColumnType("INTEGER"); - b.Property("Number") - .HasColumnType("INTEGER"); + b.Property("Number") + .HasColumnType("INTEGER"); - b.Property("PrimaryRevisionInformationId") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("PullRequestRevision") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(40); + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); - b.Property("TitleAtMerge") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("MergedById"); + b.HasIndex("MergedById"); - b.HasIndex("PrimaryRevisionInformationId") - .IsUnique(); + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); - b.ToTable("TestMerges"); - }); + b.ToTable("TestMerges"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("AdministrationRights") - .HasColumnType("INTEGER"); + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); - b.Property("CanonicalName") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("CreatedAt") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); - b.Property("CreatedById") - .HasColumnType("INTEGER"); + b.Property("CreatedById") + .HasColumnType("INTEGER"); - b.Property("Enabled") - .IsRequired() - .HasColumnType("INTEGER"); + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); - b.Property("InstanceManagerRights") - .HasColumnType("INTEGER"); + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); - b.Property("LastPasswordUpdate") - .HasColumnType("TEXT"); + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(10000); + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); - b.Property("PasswordHash") - .HasColumnType("TEXT"); + b.Property("PasswordHash") + .HasColumnType("TEXT"); - b.Property("SystemIdentifier") - .HasColumnType("TEXT"); + b.Property("SystemIdentifier") + .HasColumnType("TEXT"); - b.HasKey("Id"); + b.HasKey("Id"); - b.HasIndex("CanonicalName") - .IsUnique(); + b.HasIndex("CanonicalName") + .IsUnique(); - b.HasIndex("CreatedById"); + b.HasIndex("CreatedById"); - b.HasIndex("SystemIdentifier") - .IsUnique(); + b.HasIndex("SystemIdentifier") + .IsUnique(); - b.ToTable("Users"); - }); + b.ToTable("Users"); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("ChatSettings") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => - { - b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") - .WithMany("Channels") - .HasForeignKey("ChatSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => - { - b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithOne() - .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("CompileJobs") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamDaemonSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("DreamMakerSettings") - .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") - .WithMany() - .HasForeignKey("CancelledById"); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("Jobs") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") - .WithMany() - .HasForeignKey("StartedById") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "User") - .WithMany("OAuthConnections") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") - .WithMany() - .HasForeignKey("CompileJobId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithOne("RepositorySettings") - .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") - .WithMany("ActiveTestMerges") - .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") - .WithMany("RevisonInformations") - .HasForeignKey("TestMergeId") - .OnDelete(DeleteBehavior.ClientNoAction) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("RevisionInformations") - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") - .WithMany("TestMerges") - .HasForeignKey("MergedById") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") - .WithOne("PrimaryTestMerge") - .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => - { - b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") - .WithMany("CreatedUsers") - .HasForeignKey("CreatedById"); - }); + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index c74291520d..58949d3251 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { @@ -13,7 +14,7 @@ namespace Tgstation.Server.Host.Models public Job Job { get; set; } /// - /// The of + /// The of /// public long JobId { get; set; } @@ -44,6 +45,11 @@ namespace Tgstation.Server.Host.Models /// public int? DMApiPatchVersion { get; set; } + /// + /// The origin of the repository the compile job was built from. + /// + public string RepositoryOrigin { get; set; } + /// /// The source GitHub repository the deployment came from if any. /// @@ -86,7 +92,8 @@ namespace Tgstation.Server.Host.Models RevisionInformation = RevisionInformation.ToApi(), ByondVersion = Version.Parse(ByondVersion), MinimumSecurityLevel = MinimumSecurityLevel, - DMApiVersion = DMApiVersion + DMApiVersion = DMApiVersion, + RepositoryOrigin = new Uri(RepositoryOrigin), }; } } diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index e44b557cb4..6e9fd81880 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Models Id = Id, MergedBy = MergedBy.ToApi(false), Number = Number, - PullRequestRevision = PullRequestRevision, + TargetCommitSha = TargetCommitSha, Url = Url }; } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index f2de7240e3..b434ac6a61 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -10,7 +10,7 @@ false true bin\$(Configuration)\netcoreapp3.1\Tgstation.Server.Host.xml - API1000 + API1000,CA1508 diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index 1bdb2bb7fb..93815175c3 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -45,13 +45,13 @@ namespace Tgstation.Server.Tests.Instance Assert.IsNull(initalRepo.ActiveJob); const string Origin = "https://github.com/tgstation/tgstation-server"; - initalRepo.Origin = Origin; + initalRepo.Origin = new Uri(Origin); initalRepo.Reference = workingBranch; var clone = await repositoryClient.Clone(initalRepo, cancellationToken).ConfigureAwait(false); await ApiAssert.ThrowsException(() => repositoryClient.Read(cancellationToken), ErrorCode.RepoCloning); Assert.IsNotNull(clone); - Assert.AreEqual(Origin, clone.Origin); + Assert.AreEqual(initalRepo.Origin, clone.Origin); Assert.AreEqual(workingBranch, clone.Reference); Assert.IsNull(clone.RevisionInformation); Assert.IsNotNull(clone.ActiveJob); @@ -65,28 +65,28 @@ namespace Tgstation.Server.Tests.Instance clone = await repositoryClient.Clone(initalRepo, cancellationToken).ConfigureAwait(false); await WaitForJob(clone.ActiveJob, 600, false, null, cancellationToken).ConfigureAwait(false); - var cloned = await repositoryClient.Read(cancellationToken); + var readAfterClone = await repositoryClient.Read(cancellationToken); - Assert.AreEqual(Origin, cloned.Origin); - Assert.AreEqual(workingBranch, cloned.Reference); - Assert.IsNotNull(cloned.RevisionInformation); - Assert.IsNotNull(cloned.RevisionInformation.ActiveTestMerges); - Assert.AreEqual(0, cloned.RevisionInformation.ActiveTestMerges.Count); - Assert.IsNotNull(cloned.RevisionInformation.CommitSha); - Assert.IsNotNull(cloned.RevisionInformation.OriginCommitSha); - Assert.IsNotNull(cloned.RevisionInformation.CompileJobs); - Assert.AreEqual(0, cloned.RevisionInformation.CompileJobs.Count); - Assert.IsNotNull(cloned.RevisionInformation.OriginCommitSha); - Assert.IsNull(cloned.RevisionInformation.PrimaryTestMerge); - Assert.AreEqual(cloned.RevisionInformation.CommitSha, cloned.RevisionInformation.OriginCommitSha); + Assert.AreEqual(initalRepo.Origin, readAfterClone.Origin); + Assert.AreEqual(workingBranch, readAfterClone.Reference); + Assert.IsNotNull(readAfterClone.RevisionInformation); + Assert.IsNotNull(readAfterClone.RevisionInformation.ActiveTestMerges); + Assert.AreEqual(0, readAfterClone.RevisionInformation.ActiveTestMerges.Count); + Assert.IsNotNull(readAfterClone.RevisionInformation.CommitSha); + Assert.IsNotNull(readAfterClone.RevisionInformation.OriginCommitSha); + Assert.IsNotNull(readAfterClone.RevisionInformation.CompileJobs); + Assert.AreEqual(0, readAfterClone.RevisionInformation.CompileJobs.Count); + Assert.IsNotNull(readAfterClone.RevisionInformation.OriginCommitSha); + Assert.IsNull(readAfterClone.RevisionInformation.PrimaryTestMerge); + Assert.AreEqual(readAfterClone.RevisionInformation.CommitSha, readAfterClone.RevisionInformation.OriginCommitSha); - cloned.Origin = "https://github.com/tgstation/tgstation"; - await ApiAssert.ThrowsException(() => repositoryClient.Update(cloned, cancellationToken), ErrorCode.RepoCantChangeOrigin); - cloned.Origin = Origin; + readAfterClone.Origin = new Uri("https://github.com/tgstation/tgstation"); + await ApiAssert.ThrowsException(() => repositoryClient.Update(readAfterClone, cancellationToken), ErrorCode.RepoCantChangeOrigin); + readAfterClone.Origin = new Uri(Origin); // checkout V3 and back - cloned.Reference = "V3"; - var updated = await Checkout(cloned, false, true, cancellationToken); + readAfterClone.Reference = "V3"; + var updated = await Checkout(readAfterClone, false, true, cancellationToken); // Specific SHA updated.CheckoutSha = "f43f5bd"; @@ -166,7 +166,7 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(1, withMerge.RevisionInformation.ActiveTestMerges.Count); Assert.AreEqual(prNumber, withMerge.RevisionInformation.ActiveTestMerges.First().Number); Assert.AreEqual(prNumber, withMerge.RevisionInformation.PrimaryTestMerge.Number); - var prRevision = withMerge.RevisionInformation.PrimaryTestMerge.PullRequestRevision; + var prRevision = withMerge.RevisionInformation.PrimaryTestMerge.TargetCommitSha; Assert.IsNotNull(prRevision); Assert.IsNotNull(withMerge.RevisionInformation.PrimaryTestMerge.MergedBy); Assert.IsNotNull(withMerge.RevisionInformation.PrimaryTestMerge.MergedAt); @@ -188,7 +188,7 @@ namespace Tgstation.Server.Tests.Instance { Number = prNumber, Comment = "asdffdsa", - PullRequestRevision = prRevision + TargetCommitSha = prRevision } }; @@ -199,7 +199,7 @@ namespace Tgstation.Server.Tests.Instance var final = await repositoryClient.Read(cancellationToken); Assert.AreEqual("asdffdsa", final.RevisionInformation.PrimaryTestMerge.Comment); Assert.AreEqual(prNumber, final.RevisionInformation.PrimaryTestMerge.Number); - Assert.AreEqual(prRevision, final.RevisionInformation.PrimaryTestMerge.PullRequestRevision); + Assert.AreEqual(prRevision, final.RevisionInformation.PrimaryTestMerge.TargetCommitSha); } public async Task RunPostTest(CancellationToken cancellationToken) From 1df12a29c11259959be2597be4770d9bce912ef2 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 15:15:48 -0500 Subject: [PATCH 041/154] Add some OAuthConnections API tests - Fix returning null instead of an empty array --- src/Tgstation.Server.Host/Models/User.cs | 5 +++- tests/Tgstation.Server.Tests/UsersTest.cs | 36 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 547df857df..4a08c4f74f 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -77,7 +77,10 @@ namespace Tgstation.Server.Host.Models InstanceManagerRights = showDetails ? InstanceManagerRights : null, Name = Name, SystemIdentifier = showDetails ? SystemIdentifier : null, - OAuthConnections = OAuthConnections?.Select(x => x.ToApi()).ToList(), + OAuthConnections = OAuthConnections + ?.Select(x => x.ToApi()) + .ToList() + ?? new List(), }; /// diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index fade7832a0..0110d8a4dc 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -34,6 +34,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual("Admin", user.Name); Assert.IsNull(user.SystemIdentifier); Assert.AreEqual(true, user.Enabled); + Assert.IsNotNull(user.OAuthConnections); var systemUser = user.CreatedBy; Assert.IsNotNull(systemUser); @@ -49,6 +50,41 @@ namespace Tgstation.Server.Tests { Id = systemUser.Id }, cancellationToken), null); + + var sampleOAuthConnections = new List + { + new OAuthConnection + { + ExternalUserId = "asdfasdf", + Provider = OAuthProvider.Discord + } + }; + await ApiAssert.ThrowsException(() => client.Update(new UserUpdate + { + Id = user.Id, + OAuthConnections = sampleOAuthConnections + }, cancellationToken), ErrorCode.AdminUserCannotOAuth); + + var testUser = await client.Create( + new UserUpdate + { + Name = $"BasicTestUser", + Password = "asdfasdjfhauwiehruiy273894234jhndjkwh" + }, + cancellationToken).ConfigureAwait(false); + + Assert.IsNotNull(testUser.OAuthConnections); + testUser = await client.Update( + new UserUpdate + { + Id = testUser.Id, + OAuthConnections = sampleOAuthConnections + }, + cancellationToken).ConfigureAwait(false); + + Assert.AreEqual(1, testUser.OAuthConnections.Count); + Assert.AreEqual(sampleOAuthConnections.First().ExternalUserId, testUser.OAuthConnections.First().ExternalUserId); + Assert.AreEqual(sampleOAuthConnections.First().Provider, testUser.OAuthConnections.First().Provider); } async Task TestCreateSysUser(CancellationToken cancellationToken) From 3533cba8a1d18a989d45fc68b756c0002fb918e8 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 16:06:25 -0500 Subject: [PATCH 042/154] We throw the job exception before this point --- src/Tgstation.Server.Host/Components/Repository/Repository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index d8be8805fb..e5f14215c1 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -271,7 +271,7 @@ namespace Tgstation.Server.Host.Components.Repository committerEmail); if (RemoteGitProvider == Api.Models.RemoteGitProvider.Unknown) - throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); + throw new InvalidOperationException("Cannot test merge with an Unknown RemoteGitProvider!"); var commitMessage = String.Format( CultureInfo.InvariantCulture, From 39d80a5b009a81ffd856704380ebe21086c6b5ac Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 16:18:12 -0500 Subject: [PATCH 043/154] Fix DMAPI version test --- tests/Tgstation.Server.Tests/VersionsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/VersionsTest.cs b/tests/Tgstation.Server.Tests/VersionsTest.cs index 0d1eec02a9..3b5a7cb39b 100644 --- a/tests/Tgstation.Server.Tests/VersionsTest.cs +++ b/tests/Tgstation.Server.Tests/VersionsTest.cs @@ -93,7 +93,7 @@ namespace Tgstation.Server.Tests var versionLine = lines.FirstOrDefault(l => l.StartsWith(Prefix)); Assert.IsNotNull(versionLine); - versionLine = versionLine.Substring(Prefix.Length + 1, 5); + versionLine = versionLine.Substring(Prefix.Length + 1, expected.ToString().Length); Assert.IsTrue(Version.TryParse(versionLine, out var actual)); Assert.AreEqual(expected, actual); From 723d2d9a6e22fe8f007346694e18077130abdc63 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 16:34:19 -0500 Subject: [PATCH 044/154] Fix SQLite down migration Wish were on .NET 5 for that sweet sweet DropColumn support --- ...201209194554_SLGenericTestMergingUpdate.cs | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs b/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs index eb0b3617a2..0e88449ff8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201209194554_SLGenericTestMergingUpdate.cs @@ -36,9 +36,50 @@ namespace Tgstation.Server.Host.Database.Migrations table: "TestMerges", newName: "PullRequestRevision"); - migrationBuilder.DropColumn( - name: "RepositoryOrigin", - table: "CompileJobs"); + migrationBuilder.RenameTable( + name: "CompileJobs", + newName: "CompileJobs_down"); + + migrationBuilder.CreateTable( + name: "CompileJobs", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DmeName = table.Column(nullable: false), + Output = table.Column(nullable: false), + DirectoryName = table.Column(nullable: false), + MinimumSecurityLevel = table.Column(nullable: true), + JobId = table.Column(nullable: false), + RevisionInformationId = table.Column(nullable: false), + ByondVersion = table.Column(nullable: false), + DMApiMajorVersion = table.Column(nullable: true), + DMApiMinorVersion = table.Column(nullable: true), + DMApiPatchVersion = table.Column(nullable: true), + GitHubDeploymentId = table.Column(nullable: true), + GitHubRepoId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CompileJobs", x => x.Id); + table.ForeignKey( + name: "FK_CompileJobs_Jobs_JobId", + column: x => x.JobId, + principalTable: "Jobs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", + column: x => x.RevisionInformationId, + principalTable: "RevisionInformations", + principalColumn: "Id"); + }); + + migrationBuilder.Sql( + $"INSERT INTO CompileJobs SELECT Id,DmeName,Output,DirectoryName,MinimumSecurityLevel,JobId,RevisionInformationId,ByondVersion,DMApiMajorVersion,DMApiMinorVersion,DMApiPatchVersion,GitHubDeploymentId,GitHubRepoId FROM CompileJobs_down"); + + migrationBuilder.DropTable( + name: "CompileJobs_down"); } } } From 945ad4a4450fe10763909d2c57d493413f06fe01 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 21:31:37 -0500 Subject: [PATCH 045/154] Fix OAuthConnections showing everywhere --- .../Controllers/UserController.cs | 15 ++++++++------- src/Tgstation.Server.Host/Models/User.cs | 3 +-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index bb22081409..3b5a8c5a9d 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -367,13 +367,14 @@ namespace Tgstation.Server.Host.Controllers SystemIdentifier = model.SystemIdentifier, InstanceUsers = new List(), OAuthConnections = model - .OAuthConnections - ?.Select(x => new Models.OAuthConnection - { - Provider = x.Provider, - ExternalUserId = x.ExternalUserId - }) - .ToList() + .OAuthConnections + ?.Select(x => new Models.OAuthConnection + { + Provider = x.Provider, + ExternalUserId = x.ExternalUserId + }) + .ToList() + ?? new List(), }; } } diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 4a08c4f74f..263112f193 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -79,8 +79,7 @@ namespace Tgstation.Server.Host.Models SystemIdentifier = showDetails ? SystemIdentifier : null, OAuthConnections = OAuthConnections ?.Select(x => x.ToApi()) - .ToList() - ?? new List(), + .ToList(), }; /// From ebe5bdd55beb9156933a2197785c40df8857b4f4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 21:32:45 -0500 Subject: [PATCH 046/154] Fix test merge comment posting --- .../Components/Deployment/DreamMaker.cs | 11 +++- .../Remote/BaseRemoteDeploymentManager.cs | 50 ++++++++++++------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 48079534c7..a7fb801b9e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -602,13 +602,15 @@ namespace Tgstation.Server.Host.Components.Deployment .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider.Value); var repoSha = repo.Head; + repoOwner = repo.RemoteRepositoryOwner; + repoName = repo.RemoteRepositoryName; revInfo = await databaseContext .RevisionInformations .AsQueryable() .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -661,10 +663,12 @@ namespace Tgstation.Server.Host.Components.Deployment await databaseContextFactory.UseContext( async databaseContext => { + var fullJob = compileJob.Job; compileJob.Job = new Models.Job { Id = job.Id }; + var fullRevInfo = compileJob.RevisionInformation; compileJob.RevisionInformation = new Models.RevisionInformation { Id = revInfo.Id @@ -690,6 +694,9 @@ namespace Tgstation.Server.Host.Components.Deployment await databaseContext.Save(default).ConfigureAwait(false); throw; } + + compileJob.Job = fullJob; + compileJob.RevisionInformation = fullRevInfo; }) .ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs index 6b80ec9cb4..a9ea424e52 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs @@ -47,23 +47,49 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote if (repositorySettings?.AccessToken == null) return; - if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha) + var deployedRevisionInformation = compileJob.RevisionInformation; + if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == deployedRevisionInformation.CommitSha) || !repositorySettings.PostTestMergeComment.Value) return; previousRevisionInformation ??= new RevisionInformation(); previousRevisionInformation.ActiveTestMerges ??= new List(); - var deployedRevisionInformation = compileJob.RevisionInformation; + deployedRevisionInformation.ActiveTestMerges ??= new List(); var tasks = new List(); // added prs - foreach (var I in deployedRevisionInformation + var tmsAdded = deployedRevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => !previousRevisionInformation .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) + .Any(y => y.TestMerge.Number == x.Number)) + .ToList(); + var tmsRemoved = previousRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !deployedRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number)) + .ToList(); + var tmsUpdated = deployedRevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => previousRevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number)) + .ToList(); + + if (!tmsAdded.Any() && !tmsRemoved.Any() && !tmsUpdated.Any()) + return; + + Logger.LogTrace( + "Commenting on {0} added, {1} removed, and {2} updated test merge sources...", + tmsAdded.Count, + tmsRemoved.Count, + tmsUpdated.Count); + foreach (var I in tmsAdded) tasks.Add( CommentOnTestMergeSource( repositorySettings, @@ -79,13 +105,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote I.Number, cancellationToken)); - // removed prs - foreach (var I in previousRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !deployedRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) + foreach (var I in tmsRemoved) tasks.Add( CommentOnTestMergeSource( repositorySettings, @@ -95,13 +115,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote I.Number, cancellationToken)); - // updated prs - foreach (var I in deployedRevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => previousRevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) + foreach (var I in tmsUpdated) tasks.Add( CommentOnTestMergeSource( repositorySettings, From 2433b5733b3f8d6d867ee5fc5af696fea7371ba2 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 22:26:54 -0500 Subject: [PATCH 047/154] Replace the term "pull request" with "test merge" Where possible without majorly breaking the DMAPI --- src/DMAPI/tgs.dm | 18 +++++++++--------- src/Tgstation.Server.Api/Models/ErrorCode.cs | 4 ++-- .../Models/Internal/TestMerge.cs | 4 ++-- .../Models/Internal/TestMergeBase.cs | 8 ++++---- .../Models/TestMergeParameters.cs | 4 ++-- .../Rights/RepositoryRights.cs | 2 +- .../Chat/Commands/PullRequestsCommand.cs | 2 +- .../Components/Events/EventType.cs | 6 +++--- .../Interop/Bridge/TestMergeInformation.cs | 14 +++++++++++++- .../Repository/IGitRemoteFeatures.cs | 2 +- .../Components/Repository/IRepository.cs | 2 +- .../Components/Repository/Repository.cs | 4 ++-- 12 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 3f70916664..3225f14d8c 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -67,7 +67,7 @@ #define TGS_EVENT_REPO_CHECKOUT 1 /// When the repository performs a fetch operation. No parameters #define TGS_EVENT_REPO_FETCH 2 -/// When the repository merges a pull request. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user +/// When the repository test merges. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user #define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3 /// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path #define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4 @@ -190,21 +190,21 @@ /// Represents a merge of a GitHub pull request. /datum/tgs_revision_information/test_merge - /// The pull request number. + /// The test merge number. var/number - /// The pull request title when it was merged. + /// The test merge source's title when it was merged. var/title - /// The pull request body when it was merged. + /// The test merge source's body when it was merged. var/body - /// The GitHub username of the pull request's author. + /// The Username of the test merge source's author. var/author - /// An http URL to the pull request. + /// An http URL to the test merge source. var/url - /// The SHA of the pull request when that was merged. + /// The SHA of the test merge when that was merged. var/pull_request_commit - /// ISO 8601 timestamp of when the pull request was merged. + /// ISO 8601 timestamp of when the test merge was created on TGS. var/time_merged - /// (Nullable) Comment left by the TGS user who initiated the merge.. + /// Optional comment left by the TGS user who initiated the merge. var/comment /// Represents a connected chat channel. diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index c372458735..cb9a77a786 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -234,7 +234,7 @@ namespace Tgstation.Server.Api.Models /// /// contained duplicate s. /// - [Description("The same pull request was present more than once in the test merge requests or is already merged!")] + [Description("The same test merge was present more than once or is already merged!")] RepoDuplicateTestMerge, /// @@ -460,7 +460,7 @@ namespace Tgstation.Server.Api.Models /// /// Encounted merge conflicts while test merging. /// - [Description("Encountered merge conflicts while test merging one or more pull requests!")] + [Description("Encountered merge conflicts while test merging one or more sources!")] RepoTestMergeConflict, /// diff --git a/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs b/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs index 6189c0327c..76ee553023 100644 --- a/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs +++ b/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models.Internal { /// - /// Represents a merge of a GitHub pull request + /// Represents a test merge of a remote "pull request". /// public class TestMerge : TestMergeBase { diff --git a/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs b/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs index d8cc971b5b..c8b87ee4d2 100644 --- a/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs @@ -9,19 +9,19 @@ namespace Tgstation.Server.Api.Models.Internal public abstract class TestMergeBase : TestMergeParameters { /// - /// The title of the pull request + /// The title of the test merge source. /// [Required] public string? TitleAtMerge { get; set; } /// - /// The body of the pull request + /// The body of the test merge source. /// [Required] public string? BodyAtMerge { get; set; } /// - /// The URL of the pull request + /// The URL of the test merge source. /// [Required] #pragma warning disable CA1056 // Uri properties should not be strings @@ -29,7 +29,7 @@ namespace Tgstation.Server.Api.Models.Internal #pragma warning restore CA1056 // Uri properties should not be strings /// - /// The author of the pull request + /// The author of the test merge source. /// [Required] public string? Author { get; set; } diff --git a/src/Tgstation.Server.Api/Models/TestMergeParameters.cs b/src/Tgstation.Server.Api/Models/TestMergeParameters.cs index 0697522f6f..fbac15c974 100644 --- a/src/Tgstation.Server.Api/Models/TestMergeParameters.cs +++ b/src/Tgstation.Server.Api/Models/TestMergeParameters.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Api.Models public class TestMergeParameters { /// - /// The number of the pull request + /// The number of the test merge source. /// public int Number { get; set; } @@ -17,7 +17,7 @@ namespace Tgstation.Server.Api.Models /// [Required] [StringLength(40)] - public string? TargetCommitSha { get; set; } + public virtual string? TargetCommitSha { get; set; } /// /// Optional comment about the test diff --git a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs index 3b3e79578f..a65ca17e0a 100644 --- a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs +++ b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs @@ -29,7 +29,7 @@ namespace Tgstation.Server.Api.Rights SetSha = 4, /// - /// User may fetch and merge GitHub pull requests. + /// User may create s. /// MergePullRequest = 8, diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs index 56cce4cc1e..617a67f6ac 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands public string Name => "prs"; /// - public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges"; + public string HelpText => "Display live test merge numbers. Add --repo to view test merges in the repository as opposed to live."; /// public bool AdminOnly => false; diff --git a/src/Tgstation.Server.Host/Components/Events/EventType.cs b/src/Tgstation.Server.Host/Components/Events/EventType.cs index 9b648382c9..9085c6e215 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventType.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventType.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Host.Components.Events +namespace Tgstation.Server.Host.Components.Events { /// /// Types of events. Mirror in tgs.dm @@ -24,10 +24,10 @@ RepoFetch, /// - /// Parameters: Pull request number, pull request sha, merger message + /// Parameters: Test merge number, test merge target sha, merger message /// [EventScript("RepoMergePullRequest")] - RepoMergePullRequest, + RepoAddTestMerge, /// /// Parameters: Absolute path to repository root diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs index 314a6d5ed3..d3dc664503 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using Tgstation.Server.Api.Models.Internal; @@ -14,6 +14,18 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// public string TimeMerged { get; set; } + /// + /// Backing field for needed to continue to support DMAPI 5. + /// + public string PullRequestRevision { get; set; } + + /// + public override string TargetCommitSha + { + get => PullRequestRevision; + set => PullRequestRevision = value; + } + /// /// The of the /// diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs index 7dc7502ba7..d91dca020d 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Components.Repository interface IGitRemoteFeatures : IGitRemoteAdditionalInformation { /// - /// Gets a formatter string which creates the remote refspec for fetching the HEAD of passed in pull request number. + /// Gets a formatter string which creates the remote refspec for fetching the HEAD of passed in test merge number. /// string TestMergeRefSpecFormatter { get; } diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 84dc366a55..e2caa16611 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Repository Task CheckoutObject(string committish, Action progressReporter, CancellationToken cancellationToken); /// - /// Attempt to merge a GitHub pull request into HEAD + /// Attempt to merge the revision specified by a given set of into HEAD /// /// The of the pull request /// The name of the merge committer diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index e5f14215c1..e04c113935 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -275,7 +275,7 @@ namespace Tgstation.Server.Host.Components.Repository var commitMessage = String.Format( CultureInfo.InvariantCulture, - "Test merge of pull request #{0}{1}{2}", + "TGS Test merge #{0}{1}{2}", testMergeParameters.Number, testMergeParameters.Comment != null ? Environment.NewLine @@ -390,7 +390,7 @@ namespace Tgstation.Server.Host.Components.Repository } await eventConsumer.HandleEvent( - EventType.RepoMergePullRequest, + EventType.RepoAddTestMerge, new List { testMergeParameters.Number.ToString(CultureInfo.InvariantCulture), From 60ac813925174b114677cf866c381cf32146118a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 22:27:12 -0500 Subject: [PATCH 048/154] Fix test for renamed error --- tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index 93815175c3..8f32300ef7 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -174,7 +174,7 @@ namespace Tgstation.Server.Tests.Instance Assert.IsNull(withMerge.RevisionInformation.PrimaryTestMerge.Comment); Assert.IsNotNull(withMerge.RevisionInformation.PrimaryTestMerge.TitleAtMerge); Assert.IsNotNull(withMerge.RevisionInformation.PrimaryTestMerge.BodyAtMerge); - if (withMerge.RevisionInformation.PrimaryTestMerge.Url != "REMOTE API ERROR: RATE LIMITED") + if (withMerge.RevisionInformation.PrimaryTestMerge.Url != "GITHUB API ERROR: RATE LIMITED") Assert.AreEqual($"https://github.com/tgstation/tgstation-server/pull/{prNumber}", withMerge.RevisionInformation.PrimaryTestMerge.Url); Assert.AreEqual(orignCommit, withMerge.RevisionInformation.OriginCommitSha); Assert.AreNotEqual(orignCommit, withMerge.RevisionInformation.CommitSha); From ed99b0db2dd9a09ae14ca335ecb50d5094b7e224 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 9 Dec 2020 22:27:30 -0500 Subject: [PATCH 049/154] Fix TargetCommitSha being null --- src/Tgstation.Server.Host/Controllers/RepositoryController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index cfd4135299..6942a3e670 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -816,11 +816,13 @@ namespace Tgstation.Server.Host.Controllers TitleAtMerge = ex.Message, Comment = I.Comment, Number = I.Number, - TargetCommitSha = I.TargetCommitSha, Url = ex.Message }; } + // Ensure we're getting the full sha from git itself + fullTestMerge.TargetCommitSha = I.TargetCommitSha; + // MergedBy will be set later ++doneSteps; From e69603da3e51cfcdd0f2b401e50c9ccc0c565424 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 10 Dec 2020 13:14:41 -0500 Subject: [PATCH 050/154] Fix Unit Tests --- .../Database/DatabaseContext.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 909f171e14..2681f8adb9 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -336,22 +336,22 @@ namespace Tgstation.Server.Host.Database /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - public static readonly Type MSLatestMigration = typeof(MSAddAdditionalDDParameters); + public static readonly Type MSLatestMigration = typeof(MSGenericTestMergingUpdate); /// - /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. + /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - public static readonly Type MYLatestMigration = typeof(MYAddAdditionalDDParameters); + public static readonly Type MYLatestMigration = typeof(MYGenericTestMergingUpdate); /// - /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. + /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - public static readonly Type PGLatestMigration = typeof(PGAddAdditionalDDParameters); + public static readonly Type PGLatestMigration = typeof(PGGenericTestMergingUpdate); /// - /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. + /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - public static readonly Type SLLatestMigration = typeof(SLAddAdditionalDDParameters); + public static readonly Type SLLatestMigration = typeof(SLGenericTestMergingUpdate); #endif /// From 17fd52cee9f038e05c9b9c16ff55beecc9dac96f Mon Sep 17 00:00:00 2001 From: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com> Date: Fri, 11 Dec 2020 16:35:07 +0000 Subject: [PATCH 051/154] Updates readme links --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b24aee2e..35984f9a1f 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,7 @@ TGS 4 can self update without stopping your DreamDaemon servers. Any V4 release Here are tools for interacting with the TGS 4 web API -- [tgstation-server-control-panel]: Official client and included with the server (WIP). A react web app for using tgstation-server. +- [tgstation-server-webpanel](https://github.com/tgstation/tgstation-server-webpanel): Official client and included with the server (WIP). A react web app for using tgstation-server. - [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server. Feature complete but lacks OAuth login options. - [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server. Feature complete. - [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server. Feature complete. From 3e6219125700cbc7c41c2a0a2c6be317475aef86 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 10 Dec 2020 19:02:04 -0500 Subject: [PATCH 052/154] Adds UserGroups - Added MinimumLength to several existing and new StringLengthAttributes. - Refactored permissions off User model to new PermissionSet model. - Added UserGroups, which contain their own PermissionSet. A user can have either a UserGroup or dedicated PermissionSet. - Added UserGroupController and client for handling groups. - Renamed InstanceUser to InstancePermissionSet and associated it with PermissionSets instead of Users. - Inject active permission sets into AuthenticationContext. - Sanitize away top level child properties of client request bodies (hopefully). - Added groups integration tests. Main test now runs in a group context. - Added migrations with hand-written transition SQL. - Resetting the admin user disassociates them from any groups. Fuck this huge commit --- .../Models/ChatChannel.cs | 4 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 20 +- ...stanceUser.cs => InstancePermissionSet.cs} | 26 +- .../Models/Internal/ChatBot.cs | 4 +- .../Models/Internal/User.cs | 21 +- .../Models/Internal/UserGroup.cs | 17 + .../Models/OAuthConnection.cs | 2 +- .../Models/PermissionSet.cs | 29 + src/Tgstation.Server.Api/Models/User.cs | 12 + src/Tgstation.Server.Api/Models/UserGroup.cs | 18 + .../Rights/AdministrationRights.cs | 2 +- .../Rights/InstanceManagerRights.cs | 4 +- ...ghts.cs => InstancePermissionSetRights.cs} | 14 +- .../Rights/RightsHelper.cs | 2 +- src/Tgstation.Server.Api/Rights/RightsType.cs | 6 +- src/Tgstation.Server.Api/Routes.cs | 9 +- src/Tgstation.Server.Client/ApiClient.cs | 34 +- .../Components/IInstanceClient.cs | 8 +- .../IInstancePermissionSetClient.cs | 59 ++ .../Components/IInstanceUserClient.cs | 59 -- .../Components/InstanceClient.cs | 4 +- .../Components/InstancePermissionSetClient.cs | 57 ++ .../Components/InstanceUserClient.cs | 57 -- src/Tgstation.Server.Client/IApiClient.cs | 22 +- src/Tgstation.Server.Client/IServerClient.cs | 7 +- .../IUserGroupsClient.cs | 52 + src/Tgstation.Server.Client/ServerClient.cs | 8 +- .../UserGroupsClient.cs | 42 + .../Controllers/ApiController.cs | 2 +- .../Controllers/ByondController.cs | 2 +- .../Controllers/DreamDaemonController.cs | 4 +- .../Controllers/DreamMakerController.cs | 8 +- .../Controllers/InstanceController.cs | 60 +- ....cs => InstancePermissionSetController.cs} | 145 +-- .../Controllers/InstanceRequiredController.cs | 2 +- .../Controllers/TgsAuthorizeAttribute.cs | 6 +- .../Controllers/UserController.cs | 122 ++- .../Controllers/UserGroupController.cs | 213 +++++ .../Core/SwaggerConfiguration.cs | 2 + .../Database/DatabaseContext.cs | 61 +- .../Database/DatabaseSeeder.cs | 36 +- .../Database/IDatabaseContext.cs | 14 +- ...20201214181824_MSAddUserGroups.Designer.cs | 895 ++++++++++++++++++ .../20201214181824_MSAddUserGroups.cs | 308 ++++++ ...20201214181914_MYAddUserGroups.Designer.cs | 880 +++++++++++++++++ .../20201214181914_MYAddUserGroups.cs | 307 ++++++ ...20201214182008_PGAddUserGroups.Designer.cs | 890 +++++++++++++++++ .../20201214182008_PGAddUserGroups.cs | 307 ++++++ ...20201214182101_SLAddUserGroups.Designer.cs | 879 +++++++++++++++++ .../20201214182101_SLAddUserGroups.cs | 308 ++++++ .../MySqlDatabaseContextModelSnapshot.cs | 108 ++- ...PostgresSqlDatabaseContextModelSnapshot.cs | 110 ++- .../SqlServerDatabaseContextModelSnapshot.cs | 112 ++- .../SqliteDatabaseContextModelSnapshot.cs | 104 +- .../Database/SqliteDatabaseContext.cs | 2 +- src/Tgstation.Server.Host/Models/Instance.cs | 6 +- .../Models/InstancePermissionSet.cs | 46 + .../Models/InstanceUser.cs | 40 - .../Models/PermissionSet.cs | 44 + src/Tgstation.Server.Host/Models/User.cs | 36 +- src/Tgstation.Server.Host/Models/UserGroup.cs | 36 + .../Security/AuthenticationContext.cs | 22 +- .../Security/AuthenticationContextFactory.cs | 14 +- .../Security/ClaimsInjector.cs | 2 +- .../Security/IAuthenticationContext.cs | 15 +- .../Security/TestAuthenticationContext.cs | 20 +- tests/Tgstation.Server.Tests/ApiAssert.cs | 4 +- .../InstanceManagerTest.cs | 17 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 97 +- tests/Tgstation.Server.Tests/TestingServer.cs | 3 +- tests/Tgstation.Server.Tests/UsersTest.cs | 124 ++- 71 files changed, 6456 insertions(+), 556 deletions(-) rename src/Tgstation.Server.Api/Models/{InstanceUser.cs => InstancePermissionSet.cs} (64%) create mode 100644 src/Tgstation.Server.Api/Models/Internal/UserGroup.cs create mode 100644 src/Tgstation.Server.Api/Models/PermissionSet.cs create mode 100644 src/Tgstation.Server.Api/Models/UserGroup.cs rename src/Tgstation.Server.Api/Rights/{InstanceUserRights.cs => InstancePermissionSetRights.cs} (51%) create mode 100644 src/Tgstation.Server.Client/Components/IInstancePermissionSetClient.cs delete mode 100644 src/Tgstation.Server.Client/Components/IInstanceUserClient.cs create mode 100644 src/Tgstation.Server.Client/Components/InstancePermissionSetClient.cs delete mode 100644 src/Tgstation.Server.Client/Components/InstanceUserClient.cs create mode 100644 src/Tgstation.Server.Client/IUserGroupsClient.cs create mode 100644 src/Tgstation.Server.Client/UserGroupsClient.cs rename src/Tgstation.Server.Host/Controllers/{InstanceUserController.cs => InstancePermissionSetController.cs} (54%) create mode 100644 src/Tgstation.Server.Host/Controllers/UserGroupController.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.cs create mode 100644 src/Tgstation.Server.Host/Models/InstancePermissionSet.cs delete mode 100644 src/Tgstation.Server.Host/Models/InstanceUser.cs create mode 100644 src/Tgstation.Server.Host/Models/PermissionSet.cs create mode 100644 src/Tgstation.Server.Host/Models/UserGroup.cs diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs index 805fa1fbbb..b3490556eb 100644 --- a/src/Tgstation.Server.Api/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models { @@ -11,7 +11,7 @@ namespace Tgstation.Server.Api.Models /// The IRC channel name. Also potentially contains the channel passsword (if separated by a colon). /// If multiple copies of the same channel with different keys are added to the server, the one that will be used is undefined. /// - [StringLength(Limits.MaximumIndexableStringLength)] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? IrcChannel { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index cb9a77a786..abe3def22d 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -288,7 +288,7 @@ namespace Tgstation.Server.Api.Models ChatBotProviderMissing, /// - /// Attempted to update a or without its ID. + /// Attempted to update a or without its ID. /// [Description("Missing user ID!")] [Obsolete("Deprecated in favor of code 2", true)] @@ -600,5 +600,23 @@ namespace Tgstation.Server.Api.Models /// [Description("The job did not receive a required upload before timing out!")] FileUploadExpired, + + /// + /// Tried to update a to have both a and + /// + [Description("A user may not have both a permissionSet and group!")] + UserGroupAndPermissionSet, + + /// + /// Tried to delete a non-empty . + /// + [Description("Cannot delete the user group as it is not empty!")] + UserGroupNotEmpty, + + /// + /// Tried to edit membership using . + /// + [Description("The " + Routes.UserGroup + " endpoint cannot edit group members. Please update each member user individually.")] + UserGroupControllerCantEditMembers, } } diff --git a/src/Tgstation.Server.Api/Models/InstanceUser.cs b/src/Tgstation.Server.Api/Models/InstancePermissionSet.cs similarity index 64% rename from src/Tgstation.Server.Api/Models/InstanceUser.cs rename to src/Tgstation.Server.Api/Models/InstancePermissionSet.cs index 569d6516e1..eb1a5b80f9 100644 --- a/src/Tgstation.Server.Api/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Api/Models/InstancePermissionSet.cs @@ -1,56 +1,56 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models { /// - /// Represents a s permissions in an + /// Represents a s permissions in an /// - public class InstanceUser + public class InstancePermissionSet { /// - /// The of the the belongs to + /// The of the the belongs to /// - public long UserId { get; set; } + public long PermissionSetId { get; set; } /// - /// The of the + /// The of the /// [Required] - public InstanceUserRights? InstanceUserRights { get; set; } + public InstancePermissionSetRights? InstancePermissionSetRights { get; set; } /// - /// The of the + /// The of the /// [Required] public ByondRights? ByondRights { get; set; } /// - /// The of the + /// The of the /// [Required] public DreamDaemonRights? DreamDaemonRights { get; set; } /// - /// The of the + /// The of the /// [Required] public DreamMakerRights? DreamMakerRights { get; set; } /// - /// The of the + /// The of the /// [Required] public RepositoryRights? RepositoryRights { get; set; } /// - /// The of the + /// The of the /// [Required] public ChatBotRights? ChatBotRights { get; set; } /// - /// The of the + /// The of the /// [Required] public ConfigurationRights? ConfigurationRights { get; set; } diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs index 5db8be2eda..992affc7b2 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models.Internal @@ -17,7 +17,7 @@ namespace Tgstation.Server.Api.Models.Internal /// The name of the connection /// [Required] - [StringLength(Limits.MaximumIndexableStringLength)] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? Name { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/Internal/User.cs b/src/Tgstation.Server.Api/Models/Internal/User.cs index e50b23afde..c15015b4ca 100644 --- a/src/Tgstation.Server.Api/Models/Internal/User.cs +++ b/src/Tgstation.Server.Api/Models/Internal/User.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; -using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models.Internal { @@ -30,26 +29,14 @@ namespace Tgstation.Server.Api.Models.Internal /// /// The SID/UID of the on Windows/POSIX respectively /// - // No need for StringLength as the server MUST validate it. + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? SystemIdentifier { get; set; } /// /// The name of the /// [Required] - [StringLength(Limits.MaximumStringLength)] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? Name { get; set; } - - /// - /// The for the - /// - [Required] - public AdministrationRights? AdministrationRights { get; set; } - - /// - /// The for the - /// - [Required] - public InstanceManagerRights? InstanceManagerRights { get; set; } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs b/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs new file mode 100644 index 0000000000..8c981782dc --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Represents a group of s. + /// + public class UserGroup : EntityId + { + /// + /// The name of the . + /// + [Required] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] + public string? Name { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/OAuthConnection.cs b/src/Tgstation.Server.Api/Models/OAuthConnection.cs index 99f5e4d05e..678846bbe9 100644 --- a/src/Tgstation.Server.Api/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Api/Models/OAuthConnection.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Api.Models /// The ID of the user in the . /// [Required] - [StringLength(Limits.MaximumIndexableStringLength)] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? ExternalUserId { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/PermissionSet.cs b/src/Tgstation.Server.Api/Models/PermissionSet.cs new file mode 100644 index 0000000000..a84ae1d27b --- /dev/null +++ b/src/Tgstation.Server.Api/Models/PermissionSet.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Rights; + +namespace Tgstation.Server.Api.Models +{ + /// + /// Represents a set of server permissions. + /// + public class PermissionSet + { + /// + /// The ID of the . + /// + [Required] + public long? Id { get; set; } + + /// + /// The for the + /// + [Required] + public AdministrationRights? AdministrationRights { get; set; } + + /// + /// The for the + /// + [Required] + public InstanceManagerRights? InstanceManagerRights { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs index 9ac62d975e..089d502846 100644 --- a/src/Tgstation.Server.Api/Models/User.cs +++ b/src/Tgstation.Server.Api/Models/User.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models { @@ -18,11 +19,22 @@ namespace Tgstation.Server.Api.Models /// /// The who created this /// + [Required] public Internal.User? CreatedBy { get; set; } /// /// List of s associated with the . /// public ICollection? OAuthConnections { get; set; } + + /// + /// The directly associated with the . + /// + public PermissionSet? PermissionSet { get; set; } + + /// + /// The asociated with the , if any. + /// + public Internal.UserGroup? Group { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/UserGroup.cs b/src/Tgstation.Server.Api/Models/UserGroup.cs new file mode 100644 index 0000000000..81cfca98ae --- /dev/null +++ b/src/Tgstation.Server.Api/Models/UserGroup.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Tgstation.Server.Api.Models +{ + /// + public sealed class UserGroup : Internal.UserGroup + { + /// + /// The of the . + /// + public PermissionSet? PermissionSet { get; set; } + + /// + /// The s the has. + /// + public ICollection? Users { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs index 73d9663d8f..5d3daf7b99 100644 --- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs +++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Api.Rights None = 0, /// - /// User can edit their and other s and create new ones. + /// User has complete control over creating/editing s and s (and deleting in the case of the latter). /// WriteUsers = 1, diff --git a/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs b/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs index b8f403843d..b8700b9e30 100644 --- a/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs +++ b/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Api.Rights None = 0, /// - /// User can view s which they have an for. + /// User can view s which they have an for. /// Read = 1, @@ -64,7 +64,7 @@ namespace Tgstation.Server.Api.Rights SetChatBotLimit = 512, /// - /// User can give themselves full rights on ALL instances. + /// User can give themselves or their group full rights on ALL instances. /// GrantPermissions = 1024, } diff --git a/src/Tgstation.Server.Api/Rights/InstanceUserRights.cs b/src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs similarity index 51% rename from src/Tgstation.Server.Api/Rights/InstanceUserRights.cs rename to src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs index fe966dcca0..285411f32c 100644 --- a/src/Tgstation.Server.Api/Rights/InstanceUserRights.cs +++ b/src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights /// Rights for an /// [Flags] - public enum InstanceUserRights : ulong + public enum InstancePermissionSetRights : ulong { /// /// User has no rights/ @@ -14,18 +14,18 @@ namespace Tgstation.Server.Api.Rights None = 0, /// - /// Allow read access to all s in the . + /// Allow read access to all s in the . /// - ReadUsers = 1, + Read = 1, /// - /// Allow write and delete access to all for the . + /// Allow write and delete access to all for the . /// - WriteUsers = 2, + Write = 2, /// - /// Allow adding additional s to the . + /// Allow adding additional s to the . /// - CreateUsers = 4 + Create = 4 } } diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs index eeda047b88..97bfc6be2d 100644 --- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs +++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Api.Rights { RightsType.DreamDaemon, typeof(DreamDaemonRights) }, { RightsType.ChatBots, typeof(ChatBotRights) }, { RightsType.Configuration, typeof(ConfigurationRights) }, - { RightsType.InstanceUser, typeof(InstanceUserRights) } + { RightsType.InstancePermissionSet, typeof(InstancePermissionSetRights) } }; /// diff --git a/src/Tgstation.Server.Api/Rights/RightsType.cs b/src/Tgstation.Server.Api/Rights/RightsType.cs index a541c5048f..2787c63f48 100644 --- a/src/Tgstation.Server.Api/Rights/RightsType.cs +++ b/src/Tgstation.Server.Api/Rights/RightsType.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Api.Rights +namespace Tgstation.Server.Api.Rights { /// /// The type of rights a model uses @@ -46,8 +46,8 @@ Configuration, /// - /// + /// /// - InstanceUser + InstancePermissionSet } } diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index 64e46067ba..99784db093 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -28,6 +28,11 @@ namespace Tgstation.Server.Api /// public const string User = Root + nameof(Models.User); + /// + /// The controller. + /// + public const string UserGroup = Root + nameof(Models.UserGroup); + /// /// The controller /// @@ -69,9 +74,9 @@ namespace Tgstation.Server.Api public const string ConfigurationFile = Configuration + "/" + File; /// - /// The controller + /// The controller /// - public const string InstanceUser = Root + nameof(Models.InstanceUser); + public const string InstancePermissionSet = Root + nameof(Models.InstancePermissionSet); /// /// The controller diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 3252782576..84e339954f 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -138,7 +138,8 @@ namespace Tgstation.Server.Client /// /// Main request method /// - /// The resulting POCO type + /// The body . + /// The resulting POCO type. /// The route to run /// The body of the request /// The method of the request @@ -146,18 +147,19 @@ namespace Tgstation.Server.Client /// If this is a token refresh operation. /// The for the operation /// A resulting in the response on success - Task RunRequest( + Task RunRequest( string route, - object? body, + TBody? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken) + where TBody : class { HttpContent? content = null; if(body != null) content = new StringContent( - JsonConvert.SerializeObject(body, GetSerializerSettings()), + JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, GetSerializerSettings()), Encoding.UTF8, MediaTypeNames.Application.Json); @@ -269,7 +271,7 @@ namespace Tgstation.Server.Client if (startingToken != headers.Token) return true; - var token = await RunRequest(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken); + var token = await RunRequest(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken); headers = new ApiHeaders(headers.UserAgent!, token.Bearer!); } catch (ClientException) @@ -285,52 +287,52 @@ namespace Tgstation.Server.Client } /// - public Task Create(string route, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, null, false, cancellationToken); + public Task Create(string route, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, null, false, cancellationToken); /// - public Task Read(string route, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Get, null, false, cancellationToken); + public Task Read(string route, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Get, null, false, cancellationToken); /// - public Task Update(string route, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Post, null, false, cancellationToken); + public Task Update(string route, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Post, null, false, cancellationToken); /// - public Task Update(string route, TBody body, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Post, null, false, cancellationToken); + public Task Update(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Post, null, false, cancellationToken); /// public Task Patch(string route, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Patch, null, false, cancellationToken); /// - public Task Update(string route, TBody body, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Post, null, false, cancellationToken); + public Task Update(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Post, null, false, cancellationToken); /// - public Task Create(string route, TBody body, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Put, null, false, cancellationToken); + public Task Create(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Put, null, false, cancellationToken); /// public Task Delete(string route, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Delete, null, false, cancellationToken); /// - public Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Put, instanceId, false, cancellationToken); + public Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Put, instanceId, false, cancellationToken); /// public Task Read(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Get, instanceId, false, cancellationToken); /// - public Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Post, instanceId, false, cancellationToken); + public Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Post, instanceId, false, cancellationToken); /// public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Delete, instanceId, false, cancellationToken); /// - public Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Delete, instanceId, false, cancellationToken); + public Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Delete, instanceId, false, cancellationToken); /// public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Delete, instanceId, false, cancellationToken); /// - public Task Create(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken); + public Task Create(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken); /// - public Task Patch(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken); + public Task Patch(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken); /// public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger))); diff --git a/src/Tgstation.Server.Client/Components/IInstanceClient.cs b/src/Tgstation.Server.Client/Components/IInstanceClient.cs index daedcb1634..63ea36d530 100644 --- a/src/Tgstation.Server.Client/Components/IInstanceClient.cs +++ b/src/Tgstation.Server.Client/Components/IInstanceClient.cs @@ -1,4 +1,4 @@ -using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { @@ -33,9 +33,9 @@ namespace Tgstation.Server.Client.Components IConfigurationClient Configuration { get; } /// - /// Access the + /// Access the . /// - IInstanceUserClient Users { get; } + IInstancePermissionSetClient PermissionSets { get; } /// /// Access the @@ -52,4 +52,4 @@ namespace Tgstation.Server.Client.Components /// IJobsClient Jobs { get; } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/IInstancePermissionSetClient.cs b/src/Tgstation.Server.Client/Components/IInstancePermissionSetClient.cs new file mode 100644 index 0000000000..0d1b4165a5 --- /dev/null +++ b/src/Tgstation.Server.Client/Components/IInstancePermissionSetClient.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client.Components +{ + /// + /// For managing s + /// + public interface IInstancePermissionSetClient + { + /// + /// Get the associated with the logged on user + /// + /// The for the operation + /// A resulting in the associated with the logged on user + Task Read(CancellationToken cancellationToken); + + /// + /// Get a specific + /// + /// The to get + /// The for the operation + /// A resulting in the requested + Task GetId(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken); + + /// + /// Get the s in the + /// + /// The for the operation + /// A resulting in a of s in the instance + Task> List(CancellationToken cancellationToken); + + /// + /// Update a + /// + /// The to update + /// The for the operation + /// A representing the running operation + Task Update(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken); + + /// + /// Create a + /// + /// The to create + /// The for the operation + /// A reulting in the new + Task Create(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken); + + /// + /// Delete a + /// + /// The to delete + /// The for the operation + /// A representing the running operation + Task Delete(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs b/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs deleted file mode 100644 index 3d1d54e8d7..0000000000 --- a/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Tgstation.Server.Api.Models; - -namespace Tgstation.Server.Client.Components -{ - /// - /// For managing s - /// - public interface IInstanceUserClient - { - /// - /// Get the associated with the logged on user - /// - /// The for the operation - /// A resulting in the associated with the logged on user - Task Read(CancellationToken cancellationToken); - - /// - /// Get a specific - /// - /// The to get - /// The for the operation - /// A resulting in the requested - Task GetId(InstanceUser instanceUser, CancellationToken cancellationToken); - - /// - /// Get the s in the - /// - /// The for the operation - /// A resulting in a of s in the instance - Task> List(CancellationToken cancellationToken); - - /// - /// Update a - /// - /// The to update - /// The for the operation - /// A representing the running operation - Task Update(InstanceUser instanceUser, CancellationToken cancellationToken); - - /// - /// Create a - /// - /// The to create - /// The for the operation - /// A reulting in the new - Task Create(InstanceUser instanceUser, CancellationToken cancellationToken); - - /// - /// Delete a - /// - /// The to delete - /// The for the operation - /// A representing the running operation - Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Client/Components/InstanceClient.cs b/src/Tgstation.Server.Client/Components/InstanceClient.cs index f5f31907ec..1f854ff680 100644 --- a/src/Tgstation.Server.Client/Components/InstanceClient.cs +++ b/src/Tgstation.Server.Client/Components/InstanceClient.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Client.Components public IConfigurationClient Configuration { get; } /// - public IInstanceUserClient Users { get; } + public IInstancePermissionSetClient PermissionSets { get; } /// public IChatBotsClient ChatBots { get; } @@ -49,7 +49,7 @@ namespace Tgstation.Server.Client.Components Repository = new RepositoryClient(apiClient, instance); DreamDaemon = new DreamDaemonClient(apiClient, instance); Configuration = new ConfigurationClient(apiClient, instance); - Users = new InstanceUserClient(apiClient, instance); + PermissionSets = new InstancePermissionSetClient(apiClient, instance); ChatBots = new ChatBotsClient(apiClient, instance); DreamMaker = new DreamMakerClient(apiClient, instance); Jobs = new JobsClient(apiClient, instance); diff --git a/src/Tgstation.Server.Client/Components/InstancePermissionSetClient.cs b/src/Tgstation.Server.Client/Components/InstancePermissionSetClient.cs new file mode 100644 index 0000000000..41e6bb26d7 --- /dev/null +++ b/src/Tgstation.Server.Client/Components/InstancePermissionSetClient.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client.Components +{ + /// + sealed class InstancePermissionSetClient : IInstancePermissionSetClient + { + /// + /// The for the + /// + readonly IApiClient apiClient; + + /// + /// The for the + /// + readonly Instance instance; + + /// + /// Construct an + /// + /// The value of + /// The value of + public InstancePermissionSetClient(IApiClient apiClient, Instance instance) + { + this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + } + + /// + public Task Create(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken) => apiClient.Create(Routes.InstancePermissionSet, instancePermissionSet ?? throw new ArgumentNullException(nameof(instancePermissionSet)), instance.Id, cancellationToken); + + /// + public Task Delete(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken) => apiClient.Delete( + Routes.SetID( + Routes.InstancePermissionSet, + instancePermissionSet.PermissionSetId), + instance.Id, + cancellationToken); + + /// + public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.InstancePermissionSet, instance.Id, cancellationToken); + + /// + public Task Update(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken) => apiClient.Update(Routes.InstancePermissionSet, instancePermissionSet ?? throw new ArgumentNullException(nameof(instancePermissionSet)), instance.Id, cancellationToken); + + /// + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstancePermissionSet), instance.Id, cancellationToken); + + /// + public Task GetId(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.InstancePermissionSet, instancePermissionSet?.PermissionSetId ?? throw new ArgumentNullException(nameof(instancePermissionSet))), instance.Id, cancellationToken); + } +} diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs deleted file mode 100644 index 1dd1659ed7..0000000000 --- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Tgstation.Server.Api; -using Tgstation.Server.Api.Models; - -namespace Tgstation.Server.Client.Components -{ - /// - sealed class InstanceUserClient : IInstanceUserClient - { - /// - /// The for the - /// - readonly IApiClient apiClient; - - /// - /// The for the - /// - readonly Instance instance; - - /// - /// Construct an - /// - /// The value of - /// The value of - public InstanceUserClient(IApiClient apiClient, Instance instance) - { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); - this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); - } - - /// - public Task Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken); - - /// - public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete( - Routes.SetID( - Routes.InstanceUser, - instanceUser.UserId), - instance.Id, - cancellationToken); - - /// - public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.InstanceUser, instance.Id, cancellationToken); - - /// - 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.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); - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs index 94cc657f0a..47b93e84d7 100644 --- a/src/Tgstation.Server.Client/IApiClient.cs +++ b/src/Tgstation.Server.Client/IApiClient.cs @@ -42,7 +42,7 @@ namespace Tgstation.Server.Client /// The request body /// The for the operation /// A resulting in the response body as a - Task Create(string route, TBody body, CancellationToken cancellationToken); + Task Create(string route, TBody body, CancellationToken cancellationToken) where TBody : class; /// /// Run an HTTP PUT request @@ -71,7 +71,7 @@ namespace Tgstation.Server.Client /// The request body /// The for the operation /// A resulting in the response body as a - Task Update(string route, TBody body, CancellationToken cancellationToken); + Task Update(string route, TBody body, CancellationToken cancellationToken) where TBody : class; /// /// Run an HTTP POST request @@ -90,7 +90,7 @@ namespace Tgstation.Server.Client /// The request body /// The for the operation /// A representing the running operation - Task Update(string route, TBody body, CancellationToken cancellationToken); + Task Update(string route, TBody body, CancellationToken cancellationToken) where TBody : class; /// /// Run an HTTP PATCH request. @@ -118,7 +118,12 @@ namespace Tgstation.Server.Client /// The instance to make the request to /// The for the operation /// A resulting in the response body as a - Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken); + Task Create( + string route, + TBody body, + long instanceId, + CancellationToken cancellationToken) + where TBody : class; /// /// Run an HTTP PUT request @@ -160,7 +165,12 @@ namespace Tgstation.Server.Client /// The instance to make the request to /// The for the operation /// A resulting in the response body as a - Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken); + Task Update( + string route, + TBody body, + long instanceId, + CancellationToken cancellationToken) + where TBody : class; /// /// Run an HTTP DELETE request @@ -180,7 +190,7 @@ namespace Tgstation.Server.Client /// The instance to make the request to /// The for the operation /// A representing the running operation - Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken); + Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class; /// /// Run an HTTP DELETE request diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs index 685606df7d..9732e57352 100644 --- a/src/Tgstation.Server.Client/IServerClient.cs +++ b/src/Tgstation.Server.Client/IServerClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -40,6 +40,11 @@ namespace Tgstation.Server.Client /// IUsersClient Users { get; } + /// + /// Access the . + /// + IUserGroupsClient Groups { get; } + /// /// The of the /// diff --git a/src/Tgstation.Server.Client/IUserGroupsClient.cs b/src/Tgstation.Server.Client/IUserGroupsClient.cs new file mode 100644 index 0000000000..8072cf0dc1 --- /dev/null +++ b/src/Tgstation.Server.Client/IUserGroupsClient.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client +{ + /// + /// For managing s. + /// + public interface IUserGroupsClient + { + /// + /// Get a specific . + /// + /// The of the to get. + /// The for the operation. + /// A resulting in the requested . + Task GetId(EntityId group, CancellationToken cancellationToken); + + /// + /// List all s. + /// + /// The for the operation. + /// A resulting in a of all s. + Task> List(CancellationToken cancellationToken); + + /// + /// Create a new . + /// + /// The to create. + /// The for the operation. + /// The new . + Task Create(UserGroup group, CancellationToken cancellationToken); + + /// + /// Update a . + /// + /// The updated . + /// The for the operation. + /// The updated . + Task Update(UserGroup group, CancellationToken cancellationToken); + + /// + /// Deletes a . + /// + /// The of the to delete. + /// The for the operation. + /// A representing the running operation. + Task Delete(EntityId group, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Client/ServerClient.cs b/src/Tgstation.Server.Client/ServerClient.cs index 950b890094..0704caafb7 100644 --- a/src/Tgstation.Server.Client/ServerClient.cs +++ b/src/Tgstation.Server.Client/ServerClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -39,6 +39,9 @@ namespace Tgstation.Server.Client /// public IUsersClient Users { get; } + /// + public IUserGroupsClient Groups { get; } + /// /// The for the /// @@ -65,6 +68,7 @@ namespace Tgstation.Server.Client Instances = new InstanceManagerClient(apiClient); Users = new UsersClient(apiClient); Administration = new AdministrationClient(apiClient); + Groups = new UserGroupsClient(apiClient); } /// @@ -76,4 +80,4 @@ namespace Tgstation.Server.Client /// public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/UserGroupsClient.cs b/src/Tgstation.Server.Client/UserGroupsClient.cs new file mode 100644 index 0000000000..72b937f283 --- /dev/null +++ b/src/Tgstation.Server.Client/UserGroupsClient.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client +{ + /// + sealed class UserGroupsClient : IUserGroupsClient + { + /// + /// The for the . + /// + readonly IApiClient apiClient; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public UserGroupsClient(IApiClient apiClient) + { + this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + } + + /// + public Task Create(UserGroup group, CancellationToken cancellationToken) => apiClient.Create(Routes.UserGroup, group ?? throw new ArgumentNullException(nameof(group)), cancellationToken); + + /// + public Task GetId(EntityId group, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.UserGroup, group?.Id ?? throw new ArgumentNullException(nameof(group))), cancellationToken); + + /// + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.UserGroup), cancellationToken); + + /// + public Task Update(UserGroup group, CancellationToken cancellationToken) => apiClient.Update(Routes.UserGroup, group ?? throw new ArgumentNullException(nameof(group)), cancellationToken); + + /// + public Task Delete(EntityId group, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.UserGroup, group?.Id ?? throw new ArgumentNullException(nameof(group))), cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 0a772be319..7699c5249c 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(authenticationContextFactory)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext; - Instance = AuthenticationContext?.InstanceUser?.Instance; + Instance = AuthenticationContext?.InstancePermissionSet?.Instance; this.requireHeaders = requireHeaders; } diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 8c503ba47b..e732f10cda 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -121,7 +121,7 @@ namespace Tgstation.Server.Host.Controllers || (uploadingZip && model.Version.Build > 0)) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); - var userByondRights = AuthenticationContext.InstanceUser.ByondRights.Value; + var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value; if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip) || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip)) return Forbid(); diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 1c97a4a071..c6841287aa 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -270,8 +270,8 @@ namespace Tgstation.Server.Host.Controllers || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) || CheckModified(x => x.Port, DreamDaemonRights.SetPort) || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) - || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) - || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) + || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) + || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval) || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout) diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index b1bc9b6432..273a2e76f0 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.Controllers if (model.ProjectName != null) { - if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) + if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) return Forbid(); if (model.ProjectName.Length == 0) hostModel.ProjectName = null; @@ -209,7 +209,7 @@ namespace Tgstation.Server.Host.Controllers if (model.ApiValidationPort.HasValue) { - if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) + if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) return Forbid(); if (model.ApiValidationPort.Value != hostModel.ApiValidationPort.Value) @@ -229,14 +229,14 @@ namespace Tgstation.Server.Host.Controllers if (model.ApiValidationSecurityLevel.HasValue) { - if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) + if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) return Forbid(); hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; } if (model.RequireDMApiValidation.HasValue) { - if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationRequirement)) + if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationRequirement)) return Forbid(); hostModel.RequireDMApiValidation = model.RequireDMApiValidation; } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index cf475e6836..c471786360 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -169,9 +169,9 @@ namespace Tgstation.Server.Host.Controllers PostTestMergeComment = false, CreateGitHubDeployments = false }, - InstanceUsers = new List // give this user full privileges on the instance + InstancePermissionSets = new List // give this user full privileges on the instance { - InstanceAdminUser(null) + InstanceAdminPermissionSet(null) } }; } @@ -188,21 +188,21 @@ namespace Tgstation.Server.Host.Controllers return path; } - Models.InstanceUser InstanceAdminUser(Models.InstanceUser userToModify) + Models.InstancePermissionSet InstanceAdminPermissionSet(Models.InstancePermissionSet permissionSetToModify) { - if (userToModify == null) - userToModify = new Models.InstanceUser() + if (permissionSetToModify == null) + permissionSetToModify = new Models.InstancePermissionSet() { - UserId = AuthenticationContext.User.Id.Value + PermissionSetId = AuthenticationContext.PermissionSet.Id.Value }; - userToModify.ByondRights = RightsHelper.AllRights(); - userToModify.ChatBotRights = RightsHelper.AllRights(); - userToModify.ConfigurationRights = RightsHelper.AllRights(); - userToModify.DreamDaemonRights = RightsHelper.AllRights(); - userToModify.DreamMakerRights = RightsHelper.AllRights(); - userToModify.RepositoryRights = RightsHelper.AllRights(); - userToModify.InstanceUserRights = RightsHelper.AllRights(); - return userToModify; + permissionSetToModify.ByondRights = RightsHelper.AllRights(); + permissionSetToModify.ChatBotRights = RightsHelper.AllRights(); + permissionSetToModify.ConfigurationRights = RightsHelper.AllRights(); + permissionSetToModify.DreamDaemonRights = RightsHelper.AllRights(); + permissionSetToModify.DreamMakerRights = RightsHelper.AllRights(); + permissionSetToModify.RepositoryRights = RightsHelper.AllRights(); + permissionSetToModify.InstancePermissionSetRights = RightsHelper.AllRights(); + return permissionSetToModify; } /// @@ -431,7 +431,7 @@ namespace Tgstation.Server.Host.Controllers if (moveJob != default) { // don't allow them to cancel it if they can't start it. - if (!AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.Relocate)) + if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now } @@ -597,16 +597,16 @@ namespace Tgstation.Server.Host.Controllers IQueryable GetBaseQuery() { IQueryable query = DatabaseContext.Instances; - if (!AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List)) + if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List)) query = query - .Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id)) - .Where(x => x.InstanceUsers.Any(instanceUser => + .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value)) + .Where(x => x.InstancePermissionSets.Any(instanceUser => instanceUser.ByondRights != ByondRights.None || instanceUser.ChatBotRights != ChatBotRights.None || instanceUser.ConfigurationRights != ConfigurationRights.None || instanceUser.DreamDaemonRights != DreamDaemonRights.None || instanceUser.DreamMakerRights != DreamMakerRights.None || - instanceUser.InstanceUserRights != InstanceUserRights.None)); + instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None)); // Hack for EF IAsyncEnumerable BS return query.Select(x => x); @@ -653,7 +653,7 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessage), 410)] public async Task GetId(long id, CancellationToken cancellationToken) { - var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List); + var cantList = !AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List); IQueryable QueryForUser() { var query = DatabaseContext @@ -662,7 +662,7 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.Id == id); if (cantList) - query = query.Include(x => x.InstanceUsers); + query = query.Include(x => x.InstancePermissionSets); return query; } @@ -674,13 +674,13 @@ namespace Tgstation.Server.Host.Controllers if (InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance)) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - if (cantList && !instance.InstanceUsers.Any(instanceUser => instanceUser.UserId == AuthenticationContext.User.Id && + if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value && (instanceUser.ByondRights != ByondRights.None || instanceUser.ChatBotRights != ChatBotRights.None || instanceUser.ConfigurationRights != ConfigurationRights.None || instanceUser.DreamDaemonRights != DreamDaemonRights.None || instanceUser.DreamMakerRights != DreamMakerRights.None || - instanceUser.InstanceUserRights != InstanceUserRights.None))) + instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None))) return Forbid(); var api = instance.ToApi(); @@ -710,22 +710,22 @@ namespace Tgstation.Server.Host.Controllers public async Task GrantPermissions(long id, CancellationToken cancellationToken) { // ensure the current user has write privilege on the instance - var usersInstanceUser = await DatabaseContext + var usersInstancePermissionSet = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == id) - .SelectMany(x => x.InstanceUsers) - .Where(x => x.UserId == AuthenticationContext.User.Id) + .SelectMany(x => x.InstancePermissionSets) + .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (usersInstanceUser == default) + if (usersInstancePermissionSet == default) { - var instanceAdminUser = InstanceAdminUser(null); + var instanceAdminUser = InstanceAdminPermissionSet(null); instanceAdminUser.InstanceId = id; - DatabaseContext.InstanceUsers.Add(instanceAdminUser); + DatabaseContext.InstancePermissionSets.Add(instanceAdminUser); } else - InstanceAdminUser(usersInstanceUser); + InstanceAdminPermissionSet(usersInstancePermissionSet); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs similarity index 54% rename from src/Tgstation.Server.Host/Controllers/InstanceUserController.cs rename to src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs index 34586ea5cf..3003bcab1c 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; @@ -18,10 +18,10 @@ using Z.EntityFramework.Plus; namespace Tgstation.Server.Host.Controllers { /// - /// for managing s. + /// for managing s. /// - [Route(Routes.InstanceUser)] - public sealed class InstanceUserController : InstanceRequiredController + [Route(Routes.InstancePermissionSet)] + public sealed class InstancePermissionSetController : InstanceRequiredController { /// /// Construct a @@ -30,11 +30,11 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The for the - public InstanceUserController( + public InstancePermissionSetController( IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - ILogger logger) + ILogger logger) : base( instanceManager, databaseContext, @@ -43,16 +43,16 @@ namespace Tgstation.Server.Host.Controllers { } /// - /// Create an . + /// Create an . /// - /// The to create. + /// The to create. /// The for the operation. /// A resulting in the of the request. - /// created successfully. + /// created successfully. [HttpPut] - [TgsAuthorize(InstanceUserRights.CreateUsers)] - [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)] - public async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + [TgsAuthorize(InstancePermissionSetRights.Create)] + [ProducesResponseType(typeof(Api.Models.InstancePermissionSet), 201)] + public async Task Create([FromBody] Api.Models.InstancePermissionSet model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -60,7 +60,7 @@ namespace Tgstation.Server.Host.Controllers var userCanonicalName = await DatabaseContext .Users .AsQueryable() - .Where(x => x.Id == model.UserId) + .Where(x => x.Id == model.PermissionSetId) .Select(x => x.CanonicalName) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -71,7 +71,7 @@ namespace Tgstation.Server.Host.Controllers if (userCanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return Forbid(); - var dbUser = new Models.InstanceUser + var dbUser = new Models.InstancePermissionSet { ByondRights = RightsHelper.Clamp(model.ByondRights ?? ByondRights.None), ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None), @@ -79,141 +79,148 @@ namespace Tgstation.Server.Host.Controllers DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None), DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? DreamMakerRights.None), RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None), - InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? InstanceUserRights.None), - UserId = model.UserId, + InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? InstancePermissionSetRights.None), + PermissionSetId = model.PermissionSetId, InstanceId = Instance.Id }; - DatabaseContext.InstanceUsers.Add(dbUser); + DatabaseContext.InstancePermissionSets.Add(dbUser); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return Created(dbUser.ToApi()); } /// - /// Update the permissions for an . + /// Update the permissions for an . /// - /// The updated . + /// The updated . /// The for the operation. /// A resulting in the of the request. - /// updated successfully. - /// The requested does not currently exist. + /// updated successfully. + /// The requested does not currently exist. [HttpPost] - [TgsAuthorize(InstanceUserRights.WriteUsers)] - [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] + [TgsAuthorize(InstancePermissionSetRights.Write)] + [ProducesResponseType(typeof(Api.Models.InstancePermissionSet), 200)] [ProducesResponseType(typeof(ErrorMessage), 410)] #pragma warning disable CA1506 // TODO: Decomplexify - public async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + public async Task Update([FromBody] Api.Models.InstancePermissionSet model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); - var originalUser = await DatabaseContext + var originalPermissionSet = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) - .SelectMany(x => x.InstanceUsers) - .Where(x => x.UserId == model.UserId) + .SelectMany(x => x.InstancePermissionSets) + .Where(x => x.PermissionSetId == model.PermissionSetId) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (originalUser == null) + if (originalPermissionSet == null) return Gone(); - originalUser.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalUser.ByondRights.Value); - originalUser.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalUser.RepositoryRights.Value); - originalUser.InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? originalUser.InstanceUserRights.Value); - originalUser.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalUser.ChatBotRights.Value); - originalUser.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalUser.ConfigurationRights.Value); - originalUser.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalUser.DreamDaemonRights.Value); - originalUser.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalUser.DreamMakerRights.Value); + originalPermissionSet.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalPermissionSet.ByondRights.Value); + originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights.Value); + originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights.Value); + originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights.Value); + originalPermissionSet.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalPermissionSet.ConfigurationRights.Value); + originalPermissionSet.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalPermissionSet.DreamDaemonRights.Value); + originalPermissionSet.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalPermissionSet.DreamMakerRights.Value); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser - { - UserId = originalUser.UserId - }); + var showFullPermissionSet = originalPermissionSet.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value + || (AuthenticationContext.GetRight(RightsType.InstancePermissionSet) & (ulong)InstancePermissionSetRights.Read) != 0; + return Json( + showFullPermissionSet + ? originalPermissionSet.ToApi() + : new Api.Models.InstancePermissionSet + { + PermissionSetId = originalPermissionSet.PermissionSetId + }); } #pragma warning restore CA1506 /// - /// Read the active . + /// Read the active . /// /// The of the request. - /// retrieved successfully. + /// retrieved successfully. [HttpGet] [TgsAuthorize] - [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] - public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi()); + [ProducesResponseType(typeof(Api.Models.InstancePermissionSet), 200)] + public IActionResult Read() => Json(AuthenticationContext.InstancePermissionSet.ToApi()); /// - /// Lists s for the instance. + /// Lists s for the instance. /// /// The for the operation. /// A resulting in the of the request. - /// Retrieved s successfully. + /// Retrieved s successfully. [HttpGet(Routes.List)] - [TgsAuthorize(InstanceUserRights.ReadUsers)] - [ProducesResponseType(typeof(IEnumerable), 200)] + [TgsAuthorize(InstancePermissionSetRights.Read)] + [ProducesResponseType(typeof(IEnumerable), 200)] public async Task List(CancellationToken cancellationToken) { - var users = await DatabaseContext + var permissionSets = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) - .SelectMany(x => x.InstanceUsers) + .SelectMany(x => x.InstancePermissionSets) .ToListAsync(cancellationToken) .ConfigureAwait(false); - return Json(users.Select(x => x.ToApi())); + return Json(permissionSets.Select(x => x.ToApi())); } /// - /// Gets a specific . + /// Gets a specific . /// - /// The . + /// The . /// The for the operation. /// A resulting in the of the request. - /// Retrieve successfully. - /// The requested does not currently exist. + /// Retrieve successfully. + /// The requested does not currently exist. [HttpGet("{id}")] - [TgsAuthorize(InstanceUserRights.ReadUsers)] - [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)] + [TgsAuthorize(InstancePermissionSetRights.Read)] + [ProducesResponseType(typeof(Api.Models.InstancePermissionSet), 200)] [ProducesResponseType(typeof(ErrorMessage), 410)] public async Task GetId(long id, CancellationToken cancellationToken) { // this functions as userId - var user = await DatabaseContext + var permissionSet = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) - .SelectMany(x => x.InstanceUsers) - .Where(x => x.UserId == id) + .SelectMany(x => x.InstancePermissionSets) + .Where(x => x.PermissionSetId == id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (user == default) + if (permissionSet == default) return Gone(); - return Json(user.ToApi()); + return Json(permissionSet.ToApi()); } /// - /// Delete an . + /// Delete an . /// - /// The to delete. + /// The to delete. /// The for the operation. /// A resulting in the of the request. - /// deleted or no longer exists. + /// Target deleted. + /// Target or no longer exists. [HttpDelete("{id}")] - [TgsAuthorize(InstanceUserRights.WriteUsers)] + [TgsAuthorize(InstancePermissionSetRights.Write)] [ProducesResponseType(204)] + [ProducesResponseType(typeof(ErrorMessage), 410)] public async Task Delete(long id, CancellationToken cancellationToken) { - await DatabaseContext + var numDeleted = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) - .SelectMany(x => x.InstanceUsers) - .Where(x => x.UserId == id) + .SelectMany(x => x.InstancePermissionSets) + .Where(x => x.PermissionSetId == id) .DeleteAsync(cancellationToken) .ConfigureAwait(false); - return NoContent(); + return numDeleted > 0 ? (IActionResult)NoContent() : Gone(); } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs index 2ec482ef59..1005c5fcb2 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Controllers { if (!ApiHeaders.InstanceId.HasValue) return BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)); - if (AuthenticationContext.InstanceUser == null) + if (AuthenticationContext.InstancePermissionSet == null) return Forbid(); if (ValidateInstanceOnlineStatus(instanceManager, Logger, Instance)) diff --git a/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs index 4501a9783f..b7e5f74ec1 100644 --- a/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs +++ b/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs @@ -102,13 +102,13 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Construct a for + /// Construct a for /// /// The rights required - public TgsAuthorizeAttribute(InstanceUserRights requiredRights) + public TgsAuthorizeAttribute(InstancePermissionSetRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); - RightsType = Api.Rights.RightsType.InstanceUser; + RightsType = Api.Rights.RightsType.InstancePermissionSet; } } } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 3b5a8c5a9d..6fdf0546e3 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -103,7 +103,7 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Create a . + /// Create a new . /// /// The to create. /// The for the operation. @@ -124,6 +124,9 @@ namespace Tgstation.Server.Host.Controllers if (!(model.Password == null ^ model.SystemIdentifier == null)) return BadRequest(new ErrorMessage(ErrorCode.UserMismatchPasswordSid)); + if (model.Group != null && model.PermissionSet != null) + return BadRequest(new ErrorMessage(ErrorCode.UserGroupAndPermissionSet)); + model.Name = model.Name?.Trim(); if (model.Name?.Length == 0) model.Name = null; @@ -135,7 +138,9 @@ namespace Tgstation.Server.Host.Controllers if (fail != null) return fail; - var dbUser = CreateNewUserFromModel(model); + var dbUser = await CreateNewUserFromModel(model, cancellationToken).ConfigureAwait(false); + if (dbUser == null) + return Gone(); if (model.SystemIdentifier != null) try @@ -180,8 +185,8 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)] [ProducesResponseType(typeof(Api.Models.User), 200)] [ProducesResponseType(typeof(ErrorMessage), 404)] - #pragma warning disable CA1502 // TODO: Decomplexify - #pragma warning disable CA1506 +#pragma warning disable CA1502 // TODO: Decomplexify +#pragma warning disable CA1506 public async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) @@ -190,6 +195,9 @@ namespace Tgstation.Server.Host.Controllers if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); + if (model.Group != null && model.PermissionSet != null) + return BadRequest(new ErrorMessage(ErrorCode.UserGroupAndPermissionSet)); + var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword); @@ -203,6 +211,8 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) + .Include(x => x.Group) + .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -215,9 +225,9 @@ namespace Tgstation.Server.Host.Controllers // Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request) if ((!canEditAllUsers && (model.Id != originalUser.Id - || model.InstanceManagerRights.HasValue - || model.AdministrationRights.HasValue || model.Enabled.HasValue + || model.Group != null + || model.PermissionSet != null || model.Name != null)) || (!passwordEdit && model.Password != null) || (!oAuthEdit && model.OAuthConnections != null)) @@ -236,8 +246,6 @@ namespace Tgstation.Server.Host.Controllers if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName) return BadRequest(new ErrorMessage(ErrorCode.UserNameChange)); - originalUser.InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? originalUser.InstanceManagerRights.Value); - originalUser.AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? originalUser.AdministrationRights.Value); if (model.Enabled.HasValue) { if (originalUser.Enabled.Value && !model.Enabled.Value) @@ -262,6 +270,41 @@ namespace Tgstation.Server.Host.Controllers }); } + if (model.Group != null) + { + originalUser.Group = await DatabaseContext + .Groups + .AsQueryable() + .Where(x => x.Id == model.Group.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (originalUser.Group == default) + return Gone(); + + DatabaseContext.Groups.Attach(originalUser.Group); + if (originalUser.PermissionSet != null) + { + Logger.LogInformation("Deleting permission set {0}...", originalUser.PermissionSet.Id); + DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet); + originalUser.PermissionSet = null; + } + } + else if (model.PermissionSet != null) + { + if (originalUser.PermissionSet == null) + { + Logger.LogTrace("Creating new permission set..."); + originalUser.PermissionSet = new Models.PermissionSet(); + } + + originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights; + originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights; + + originalUser.Group = null; + originalUser.GroupId = null; + } + var fail = CheckValidName(model, false); if (fail != null) return fail; @@ -282,8 +325,8 @@ namespace Tgstation.Server.Host.Controllers Id = originalUser.Id }); } - #pragma warning restore CA1506 - #pragma warning restore CA1502 +#pragma warning restore CA1506 +#pragma warning restore CA1502 /// /// Get information about the current . @@ -312,6 +355,7 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) + .Include(x => x.Group) .ToListAsync(cancellationToken).ConfigureAwait(false); return Json(users.Select(x => x.ToApi(true))); } @@ -341,6 +385,7 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.Id == id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) + .Include(x => x.Group) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) return NotFound(); @@ -355,26 +400,45 @@ namespace Tgstation.Server.Host.Controllers /// Creates a new from a given . /// /// The to use as a template. - /// A new . - Models.User CreateNewUserFromModel(Api.Models.User model) => new Models.User + /// The for the operation. + /// A resulting in a new on success, if the requested did not exist. + async Task CreateNewUserFromModel(Api.Models.User model, CancellationToken cancellationToken) { - AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None), - CreatedAt = DateTimeOffset.Now, - CreatedBy = AuthenticationContext.User, - Enabled = model.Enabled ?? false, - InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None), - Name = model.Name, - SystemIdentifier = model.SystemIdentifier, - InstanceUsers = new List(), - OAuthConnections = model - .OAuthConnections - ?.Select(x => new Models.OAuthConnection + Models.PermissionSet permissionSet = null; + Models.UserGroup group = null; + if (model.Group != null) + group = await DatabaseContext + .Groups + .AsQueryable() + .Where(x => x.Id == model.Group.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + else + permissionSet = new Models.PermissionSet { - Provider = x.Provider, - ExternalUserId = x.ExternalUserId - }) - .ToList() - ?? new List(), - }; + AdministrationRights = model.PermissionSet?.AdministrationRights ?? AdministrationRights.None, + InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None, + }; + + return new Models.User + { + CreatedAt = DateTimeOffset.Now, + CreatedBy = AuthenticationContext.User, + Enabled = model.Enabled ?? false, + PermissionSet = permissionSet, + Group = group, + Name = model.Name, + SystemIdentifier = model.SystemIdentifier, + OAuthConnections = model + .OAuthConnections + ?.Select(x => new Models.OAuthConnection + { + Provider = x.Provider, + ExternalUserId = x.ExternalUserId + }) + .ToList() + ?? new List(), + }; + } } } diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs new file mode 100644 index 0000000000..63ad368d29 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -0,0 +1,213 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Security; +using Z.EntityFramework.Plus; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for managing s. + /// + [Route(Routes.UserGroup)] + public class UserGroupController : ApiController + { + /// + /// Initializes a new instance of the . + /// + /// The for the + /// The for the + /// The for the . + public UserGroupController( + IDatabaseContext databaseContext, + IAuthenticationContextFactory authenticationContextFactory, + ILogger logger) + : base( + databaseContext, + authenticationContextFactory, + logger, + true) + { + } + + /// + /// Create a new . + /// + /// The to create. + /// The for the operation. + /// A resulting in the of the operation. + /// created successfully. + [HttpPut] + [TgsAuthorize(AdministrationRights.WriteUsers)] + [ProducesResponseType(typeof(UserGroup), 201)] + public async Task Create([FromBody] UserGroup model, CancellationToken cancellationToken) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + if (model.Name == null) + return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); + + var permissionSet = new Models.PermissionSet + { + AdministrationRights = model.PermissionSet?.AdministrationRights ?? AdministrationRights.None, + InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None + }; + + var dbGroup = new Models.UserGroup + { + Name = model.Name, + PermissionSet = permissionSet, + }; + + DatabaseContext.Groups.Add(dbGroup); + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + Logger.LogInformation("Created new user group {0} ({1})", dbGroup.Name, dbGroup.Id); + + return Created(dbGroup.ToApi(true)); + } + + /// + /// Update a new . + /// + /// The to update. + /// The for the operation. + /// A resulting in the of the operation. + /// updated successfully. + /// The requested does not currently exist. + [HttpPost] + [TgsAuthorize(AdministrationRights.WriteUsers)] + [ProducesResponseType(typeof(UserGroup), 201)] + public async Task Update([FromBody] UserGroup model, CancellationToken cancellationToken) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + // For my sanity, I'm not allowing user management here + // Use the UserController for that + if (model.Users != null) + return BadRequest(new ErrorMessage(ErrorCode.UserGroupControllerCantEditMembers)); + + var currentGroup = await DatabaseContext + .Groups + .AsQueryable() + .Where(x => x.Id == model.Id) + .Include(x => x.PermissionSet) + .Include(x => x.Users) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (currentGroup == default) + return Gone(); + + if (model.PermissionSet != null) + { + currentGroup.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; + currentGroup.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights; + } + + currentGroup.Name = model.Name ?? currentGroup.Name; + + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + + return Created(currentGroup.ToApi(true)); + } + + /// + /// Gets a specific . + /// + /// The of the . + /// The for the operation. + /// A resulting in the of the request. + /// Retrieve successfully. + /// The requested does not currently exist. + [HttpGet("{id}")] + [TgsAuthorize(InstancePermissionSetRights.Read)] + [ProducesResponseType(typeof(UserGroup), 200)] + [ProducesResponseType(typeof(ErrorMessage), 410)] + public async Task GetId(long id, CancellationToken cancellationToken) + { + // this functions as userId + var group = await DatabaseContext + .Groups + .AsQueryable() + .Where(x => x.Id == id) + .Include(x => x.Users) + .Include(x => x.PermissionSet) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + if (group == default) + return Gone(); + return Json(group.ToApi(true)); + } + + /// + /// Lists s for the instance. + /// + /// The for the operation. + /// A resulting in the of the request. + /// Retrieved s successfully. + [HttpGet(Routes.List)] + [TgsAuthorize(InstancePermissionSetRights.Read)] + [ProducesResponseType(typeof(IEnumerable), 200)] + public async Task List(CancellationToken cancellationToken) + { + var users = await DatabaseContext + .Groups + .AsQueryable() + .Include(x => x.Users) + .Include(x => x.PermissionSet) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + return Json(users.Select(x => x.ToApi(true))); + } + + /// + /// Delete an . + /// + /// The of the to delete. + /// The for the operation. + /// A resulting in the of the request. + /// was deleted. + /// The is not empty. + /// The didn't exist. + [HttpDelete("{id}")] + [TgsAuthorize(InstancePermissionSetRights.Write)] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ErrorMessage), 409)] + [ProducesResponseType(typeof(ErrorMessage), 410)] + public async Task Delete(long id, CancellationToken cancellationToken) + { + var numDeleted = await DatabaseContext + .Groups + .AsQueryable() + .Where(x => x.Id == id && x.Users.Count == 0) + .DeleteAsync(cancellationToken) + .ConfigureAwait(false); + + if (numDeleted > 0) + return NoContent(); + + // find out how we failed + var groupExists = await DatabaseContext + .Groups + .AsQueryable() + .Where(x => x.Id == id) + .AnyAsync(cancellationToken) + .ConfigureAwait(false); + + return groupExists + ? Conflict(new ErrorMessage(ErrorCode.UserGroupNotEmpty)) + : Gone(); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index 67d2dccc61..4e0ca9dcfa 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -163,6 +163,8 @@ namespace Tgstation.Server.Host.Core { if (type == typeof(Api.Models.Internal.User)) return "ShallowUser"; + if (type == typeof(Api.Models.Internal.UserGroup)) + return "ShallowUserGroup"; return type.Name; }); diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 2681f8adb9..07d234f566 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -62,9 +62,9 @@ namespace Tgstation.Server.Host.Database public DbSet RepositorySettings { get; set; } /// - /// The s in the . + /// The s in the . /// - public DbSet InstanceUsers { get; set; } + public DbSet InstancePermissionSets { get; set; } /// /// The s in the . @@ -96,6 +96,16 @@ namespace Tgstation.Server.Host.Database /// public DbSet OAuthConnections { get; set; } + /// + /// The s in the + /// + public DbSet PermissionSets { get; set; } + + /// + /// The s in the + /// + public DbSet Groups { get; set; } + /// /// The for the / foreign key. /// @@ -108,7 +118,7 @@ namespace Tgstation.Server.Host.Database IDatabaseCollection IDatabaseContext.Instances => instancesCollection; /// - IDatabaseCollection IDatabaseContext.InstanceUsers => instanceUsersCollection; + IDatabaseCollection IDatabaseContext.InstancePermissionSets => instancePermissionSetsCollection; /// IDatabaseCollection IDatabaseContext.Jobs => jobsCollection; @@ -140,6 +150,12 @@ namespace Tgstation.Server.Host.Database /// IDatabaseCollection IDatabaseContext.OAuthConnections => oAuthConnections; + /// + IDatabaseCollection IDatabaseContext.Groups => groups; + + /// + IDatabaseCollection IDatabaseContext.PermissionSets => permissionSets; + /// /// Backing field for . /// @@ -156,9 +172,9 @@ namespace Tgstation.Server.Host.Database readonly IDatabaseCollection compileJobsCollection; /// - /// Backing field for . + /// Backing field for . /// - readonly IDatabaseCollection instanceUsersCollection; + readonly IDatabaseCollection instancePermissionSetsCollection; /// /// Backing field for . @@ -205,6 +221,16 @@ namespace Tgstation.Server.Host.Database /// readonly IDatabaseCollection oAuthConnections; + /// + /// Backing field for . + /// + readonly IDatabaseCollection groups; + + /// + /// Backing field for . + /// + readonly IDatabaseCollection permissionSets; + /// /// Gets the configure action for a given . /// @@ -233,7 +259,7 @@ namespace Tgstation.Server.Host.Database { usersCollection = new DatabaseCollection(Users); instancesCollection = new DatabaseCollection(Instances); - instanceUsersCollection = new DatabaseCollection(InstanceUsers); + instancePermissionSetsCollection = new DatabaseCollection(InstancePermissionSets); compileJobsCollection = new DatabaseCollection(CompileJobs); repositorySettingsCollection = new DatabaseCollection(RepositorySettings); dreamMakerSettingsCollection = new DatabaseCollection(DreamMakerSettings); @@ -244,6 +270,8 @@ namespace Tgstation.Server.Host.Database jobsCollection = new DatabaseCollection(Jobs); reattachInformationsCollection = new DatabaseCollection(ReattachInformations); oAuthConnections = new DatabaseCollection(OAuthConnections); + groups = new DatabaseCollection(Groups); + permissionSets = new DatabaseCollection(PermissionSets); } /// @@ -262,7 +290,16 @@ namespace Tgstation.Server.Host.Database modelBuilder.Entity().HasIndex(x => new { x.Provider, x.ExternalUserId }).IsUnique(); - modelBuilder.Entity().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique(); + var groupsModel = modelBuilder.Entity(); + groupsModel.HasIndex(x => x.Name).IsUnique(); + groupsModel.HasMany(x => x.Users).WithOne(x => x.Group).OnDelete(DeleteBehavior.ClientSetNull); + + var permissionSetModel = modelBuilder.Entity(); + permissionSetModel.HasOne(x => x.Group).WithOne(x => x.PermissionSet).OnDelete(DeleteBehavior.Cascade); + permissionSetModel.HasOne(x => x.User).WithOne(x => x.PermissionSet).OnDelete(DeleteBehavior.Cascade); + permissionSetModel.HasMany(x => x.InstancePermissionSets).WithOne(x => x.PermissionSet).OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity().HasIndex(x => new { x.PermissionSetId, x.InstanceId }).IsUnique(); var revInfo = modelBuilder.Entity(); revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade); @@ -301,7 +338,7 @@ namespace Tgstation.Server.Host.Database instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); - instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); + instanceModel.HasMany(x => x.InstancePermissionSets).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); } @@ -336,22 +373,22 @@ namespace Tgstation.Server.Host.Database /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - public static readonly Type MSLatestMigration = typeof(MSGenericTestMergingUpdate); + public static readonly Type MSLatestMigration = typeof(MSAddUserGroups); /// /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - public static readonly Type MYLatestMigration = typeof(MYGenericTestMergingUpdate); + public static readonly Type MYLatestMigration = typeof(MYAddUserGroups); /// /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - public static readonly Type PGLatestMigration = typeof(PGGenericTestMergingUpdate); + public static readonly Type PGLatestMigration = typeof(PGAddUserGroups); /// /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - public static readonly Type SLLatestMigration = typeof(SLGenericTestMergingUpdate); + public static readonly Type SLLatestMigration = typeof(SLAddUserGroups); #endif /// diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 44b598ca8e..39a25c3245 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -87,11 +87,10 @@ namespace Tgstation.Server.Host.Database CanonicalName = User.CanonicalizeName(User.TgsSystemUserName), }; + // intentionally not giving a group or permissionset tgsUser.Name = User.TgsSystemUserName; tgsUser.PasswordHash = "_"; // This can't be hashed tgsUser.Enabled = false; - tgsUser.InstanceManagerRights = InstanceManagerRights.None; - tgsUser.AdministrationRights = AdministrationRights.None; if (!alreadyExists) databaseContext.Users.Add(tgsUser); @@ -107,9 +106,12 @@ namespace Tgstation.Server.Host.Database { var admin = new User { - AdministrationRights = RightsHelper.AllRights(), + PermissionSet = new PermissionSet + { + AdministrationRights = RightsHelper.AllRights(), + InstanceManagerRights = RightsHelper.AllRights(), + }, CreatedAt = DateTimeOffset.Now, - InstanceManagerRights = RightsHelper.AllRights(), Name = Api.Models.User.AdminName, CanonicalName = User.CanonicalizeName(Api.Models.User.AdminName), Enabled = true, @@ -149,11 +151,14 @@ namespace Tgstation.Server.Host.Database var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false); if (admin != null) { - // Fix the issue with ulong enums - // https://github.com/tgstation/tgstation-server/commit/db341d43b3dab74fe3681f5172ca9bfeaafa6b6d#diff-09f06ec4584665cf89bb77b97f5ccfb9R36-R39 - // https://github.com/JamesNK/Newtonsoft.Json/issues/2301 - admin.AdministrationRights &= RightsHelper.AllRights(); - admin.InstanceManagerRights &= RightsHelper.AllRights(); + if (admin.PermissionSet != null) + { + // Fix the issue with ulong enums + // https://github.com/tgstation/tgstation-server/commit/db341d43b3dab74fe3681f5172ca9bfeaafa6b6d#diff-09f06ec4584665cf89bb77b97f5ccfb9R36-R39 + // https://github.com/JamesNK/Newtonsoft.Json/issues/2301 + admin.PermissionSet.AdministrationRights &= RightsHelper.AllRights(); + admin.PermissionSet.InstanceManagerRights &= RightsHelper.AllRights(); + } if (admin.CreatedBy == null) { @@ -229,7 +234,16 @@ namespace Tgstation.Server.Host.Database if (admin != null) { admin.Enabled = true; - admin.AdministrationRights |= AdministrationRights.WriteUsers; + + // force the user out of any groups + if (admin.PermissionSet == null) + { + admin.Group = null; + admin.GroupId = null; + admin.PermissionSet = new PermissionSet(); + } + + admin.PermissionSet.AdministrationRights |= AdministrationRights.WriteUsers; cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false); } @@ -249,6 +263,8 @@ namespace Tgstation.Server.Host.Database .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName)) .Include(x => x.CreatedBy) + .Include(x => x.PermissionSet) + .Include(x => x.Group) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (admin == default) diff --git a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs index 7e86e66ba1..ab122b7e89 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs @@ -24,9 +24,9 @@ namespace Tgstation.Server.Host.Database IDatabaseCollection Instances { get; } /// - /// The s in the . + /// The s in the . /// - IDatabaseCollection InstanceUsers { get; } + IDatabaseCollection InstancePermissionSets { get; } /// /// The s in the . @@ -78,6 +78,16 @@ namespace Tgstation.Server.Host.Database /// IDatabaseCollection OAuthConnections { get; } + /// + /// The for s. + /// + IDatabaseCollection Groups { get; } + + /// + /// The for s. + /// + IDatabaseCollection PermissionSets { get; } + /// /// Saves changes made to the /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.Designer.cs new file mode 100644 index 0000000000..0f406197eb --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.Designer.cs @@ -0,0 +1,895 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20201214181824_MSAddUserGroups")] + partial class MSAddUserGroups + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("decimal(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique() + .HasFilter("[GroupId] IS NOT NULL"); + + b.HasIndex("UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs new file mode 100644 index 0000000000..b023278303 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs @@ -0,0 +1,308 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds UserGroups for MSSQL. + /// + public partial class MSAddUserGroups : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "SystemIdentifier", + table: "Users", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Users", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldMaxLength: 10000); + + migrationBuilder.AlterColumn( + name: "CanonicalName", + table: "Users", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AddColumn( + name: "GroupId", + table: "Users", + nullable: true); + + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + AdministrationRights = table.Column(nullable: false), + InstanceManagerRights = table.Column(nullable: false), + UserId = table.Column(nullable: true), + GroupId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_PermissionSets_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PermissionSets_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InstancePermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PermissionSetId = table.Column(nullable: false), + InstancePermissionSetRights = table.Column(nullable: false), + ByondRights = table.Column(nullable: false), + DreamDaemonRights = table.Column(nullable: false), + DreamMakerRights = table.Column(nullable: false), + RepositoryRights = table.Column(nullable: false), + ChatBotRights = table.Column(nullable: false), + ConfigurationRights = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstancePermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_InstancePermissionSets_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstancePermissionSets_PermissionSets_PermissionSetId", + column: x => x.PermissionSetId, + principalTable: "PermissionSets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_GroupId", + table: "Users", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_Groups_Name", + table: "Groups", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_InstanceId", + table: "InstancePermissionSets", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_PermissionSetId_InstanceId", + table: "InstancePermissionSets", + columns: new[] { "PermissionSetId", "InstanceId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_GroupId", + table: "PermissionSets", + column: "GroupId", + unique: true, + filter: "[GroupId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_UserId", + table: "PermissionSets", + column: "UserId", + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Groups_GroupId", + table: "Users", + column: "GroupId", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.Sql( + "INSERT INTO PermissionSets (UserId, AdministrationRights, InstanceManagerRights) SELECT Id, AdministrationRights, InstanceManagerRights FROM Users"); + + migrationBuilder.Sql( + "INSERT INTO InstancePermissionSets (PermissionSetId, InstanceId, InstancePermissionSetRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.Id, iu.InstanceId, iu.InstanceUserRights, iu.ByondRights, iu.DreamDaemonRights, iu.DreamMakerRights, iu.RepositoryRights, iu.ChatBotRights, iu.ConfigurationRights FROM InstanceUsers iu JOIN PermissionSets p ON iu.UserId = p.UserId"); + + migrationBuilder.DropTable( + name: "InstanceUsers"); + + migrationBuilder.DropColumn( + name: "AdministrationRights", + table: "Users"); + + migrationBuilder.DropColumn( + name: "InstanceManagerRights", + table: "Users"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "SystemIdentifier", + table: "Users", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Users", + type: "nvarchar(max)", + maxLength: 10000, + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "CanonicalName", + table: "Users", + type: "nvarchar(450)", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + + migrationBuilder.AddColumn( + name: "AdministrationRights", + table: "Users", + type: "decimal(20,0)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "InstanceManagerRights", + table: "Users", + type: "decimal(20,0)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.CreateTable( + name: "InstanceUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ByondRights = table.Column(type: "decimal(20,0)", nullable: false), + ChatBotRights = table.Column(type: "decimal(20,0)", nullable: false), + ConfigurationRights = table.Column(type: "decimal(20,0)", nullable: false), + DreamDaemonRights = table.Column(type: "decimal(20,0)", nullable: false), + DreamMakerRights = table.Column(type: "decimal(20,0)", nullable: false), + InstanceId = table.Column(type: "bigint", nullable: false), + InstanceUserRights = table.Column(type: "decimal(20,0)", nullable: false), + RepositoryRights = table.Column(type: "decimal(20,0)", nullable: false), + UserId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstanceUsers", x => x.Id); + table.ForeignKey( + name: "FK_InstanceUsers_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstanceUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_InstanceId", + table: "InstanceUsers", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_UserId_InstanceId", + table: "InstanceUsers", + columns: new[] { "UserId", "InstanceId" }, + unique: true); + + migrationBuilder.Sql( + "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.Id = p.UserId WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.DropForeignKey( + name: "FK_Users_Groups_GroupId", + table: "Users"); + + migrationBuilder.DropTable( + name: "InstancePermissionSets"); + + migrationBuilder.DropTable( + name: "PermissionSets"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropIndex( + name: "IX_Users_GroupId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "GroupId", + table: "Users"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.Designer.cs new file mode 100644 index 0000000000..2adb2e47d4 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.Designer.cs @@ -0,0 +1,880 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20201214181914_MYAddUserGroups")] + partial class MYAddUserGroups + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RepositoryOrigin") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("bigint unsigned"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SystemIdentifier") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs new file mode 100644 index 0000000000..dbc3d98ebe --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs @@ -0,0 +1,307 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds UserGroups for MySQL. + /// + public partial class MYAddUserGroups : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "SystemIdentifier", + table: "Users", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(255) CHARACTER SET utf8mb4", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Users", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldMaxLength: 10000); + + migrationBuilder.AlterColumn( + name: "CanonicalName", + table: "Users", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255) CHARACTER SET utf8mb4"); + + migrationBuilder.AddColumn( + name: "GroupId", + table: "Users", + nullable: true); + + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AdministrationRights = table.Column(nullable: false), + InstanceManagerRights = table.Column(nullable: false), + UserId = table.Column(nullable: true), + GroupId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_PermissionSets_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PermissionSets_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InstancePermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + PermissionSetId = table.Column(nullable: false), + InstancePermissionSetRights = table.Column(nullable: false), + ByondRights = table.Column(nullable: false), + DreamDaemonRights = table.Column(nullable: false), + DreamMakerRights = table.Column(nullable: false), + RepositoryRights = table.Column(nullable: false), + ChatBotRights = table.Column(nullable: false), + ConfigurationRights = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstancePermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_InstancePermissionSets_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstancePermissionSets_PermissionSets_PermissionSetId", + column: x => x.PermissionSetId, + principalTable: "PermissionSets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_GroupId", + table: "Users", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_Groups_Name", + table: "Groups", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_InstanceId", + table: "InstancePermissionSets", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_PermissionSetId_InstanceId", + table: "InstancePermissionSets", + columns: new[] { "PermissionSetId", "InstanceId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_GroupId", + table: "PermissionSets", + column: "GroupId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_UserId", + table: "PermissionSets", + column: "UserId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Groups_GroupId", + table: "Users", + column: "GroupId", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.Sql( + "INSERT INTO PermissionSets (UserId, AdministrationRights, InstanceManagerRights) SELECT Id, AdministrationRights, InstanceManagerRights FROM Users"); + + migrationBuilder.Sql( + "INSERT INTO InstancePermissionSets (PermissionSetId, InstanceId, InstancePermissionSetRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.Id, iu.InstanceId, iu.InstanceUserRights, iu.ByondRights, iu.DreamDaemonRights, iu.DreamMakerRights, iu.RepositoryRights, iu.ChatBotRights, iu.ConfigurationRights FROM InstanceUsers iu JOIN PermissionSets p ON iu.UserId = p.UserId"); + + migrationBuilder.DropTable( + name: "InstanceUsers"); + + migrationBuilder.DropColumn( + name: "AdministrationRights", + table: "Users"); + + migrationBuilder.DropColumn( + name: "InstanceManagerRights", + table: "Users"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "SystemIdentifier", + table: "Users", + type: "varchar(255) CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Users", + type: "longtext CHARACTER SET utf8mb4", + maxLength: 10000, + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "CanonicalName", + table: "Users", + type: "varchar(255) CHARACTER SET utf8mb4", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + + migrationBuilder.AddColumn( + name: "AdministrationRights", + table: "Users", + type: "bigint unsigned", + nullable: false, + defaultValue: 0ul); + + migrationBuilder.AddColumn( + name: "InstanceManagerRights", + table: "Users", + type: "bigint unsigned", + nullable: false, + defaultValue: 0ul); + + migrationBuilder.CreateTable( + name: "InstanceUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ByondRights = table.Column(type: "bigint unsigned", nullable: false), + ChatBotRights = table.Column(type: "bigint unsigned", nullable: false), + ConfigurationRights = table.Column(type: "bigint unsigned", nullable: false), + DreamDaemonRights = table.Column(type: "bigint unsigned", nullable: false), + DreamMakerRights = table.Column(type: "bigint unsigned", nullable: false), + InstanceId = table.Column(type: "bigint", nullable: false), + InstanceUserRights = table.Column(type: "bigint unsigned", nullable: false), + RepositoryRights = table.Column(type: "bigint unsigned", nullable: false), + UserId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstanceUsers", x => x.Id); + table.ForeignKey( + name: "FK_InstanceUsers_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstanceUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_InstanceId", + table: "InstanceUsers", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_UserId_InstanceId", + table: "InstanceUsers", + columns: new[] { "UserId", "InstanceId" }, + unique: true); + + migrationBuilder.Sql( + "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.Id = p.UserId WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.DropForeignKey( + name: "FK_Users_Groups_GroupId", + table: "Users"); + + migrationBuilder.DropTable( + name: "InstancePermissionSets"); + + migrationBuilder.DropTable( + name: "PermissionSets"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropIndex( + name: "IX_Users_GroupId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "GroupId", + table: "Users"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.Designer.cs new file mode 100644 index 0000000000..595d5a17db --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.Designer.cs @@ -0,0 +1,890 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20201214182008_PGAddUserGroups")] + partial class PGAddUserGroups + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RepositoryOrigin") + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("numeric(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs new file mode 100644 index 0000000000..219912379a --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs @@ -0,0 +1,307 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds UserGroups for PostgresSql. + /// + public partial class PGAddUserGroups : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "SystemIdentifier", + table: "Users", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Users", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(10000)", + oldMaxLength: 10000); + + migrationBuilder.AlterColumn( + name: "CanonicalName", + table: "Users", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "GroupId", + table: "Users", + nullable: true); + + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AdministrationRights = table.Column(nullable: false), + InstanceManagerRights = table.Column(nullable: false), + UserId = table.Column(nullable: true), + GroupId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_PermissionSets_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PermissionSets_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InstancePermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PermissionSetId = table.Column(nullable: false), + InstancePermissionSetRights = table.Column(nullable: false), + ByondRights = table.Column(nullable: false), + DreamDaemonRights = table.Column(nullable: false), + DreamMakerRights = table.Column(nullable: false), + RepositoryRights = table.Column(nullable: false), + ChatBotRights = table.Column(nullable: false), + ConfigurationRights = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstancePermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_InstancePermissionSets_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstancePermissionSets_PermissionSets_PermissionSetId", + column: x => x.PermissionSetId, + principalTable: "PermissionSets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_GroupId", + table: "Users", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_Groups_Name", + table: "Groups", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_InstanceId", + table: "InstancePermissionSets", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_PermissionSetId_InstanceId", + table: "InstancePermissionSets", + columns: new[] { "PermissionSetId", "InstanceId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_GroupId", + table: "PermissionSets", + column: "GroupId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_UserId", + table: "PermissionSets", + column: "UserId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Groups_GroupId", + table: "Users", + column: "GroupId", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.Sql( + "INSERT INTO PermissionSets (UserId, AdministrationRights, InstanceManagerRights) SELECT Id, AdministrationRights, InstanceManagerRights FROM Users"); + + migrationBuilder.Sql( + "INSERT INTO InstancePermissionSets (PermissionSetId, InstanceId, InstancePermissionSetRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.Id, iu.InstanceId, iu.InstanceUserRights, iu.ByondRights, iu.DreamDaemonRights, iu.DreamMakerRights, iu.RepositoryRights, iu.ChatBotRights, iu.ConfigurationRights FROM InstanceUsers iu JOIN PermissionSets p ON iu.UserId = p.UserId"); + + migrationBuilder.DropTable( + name: "InstanceUsers"); + + migrationBuilder.DropColumn( + name: "AdministrationRights", + table: "Users"); + + migrationBuilder.DropColumn( + name: "InstanceManagerRights", + table: "Users"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AlterColumn( + name: "SystemIdentifier", + table: "Users", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Users", + type: "character varying(10000)", + maxLength: 10000, + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "CanonicalName", + table: "Users", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100); + + migrationBuilder.AddColumn( + name: "AdministrationRights", + table: "Users", + type: "numeric(20,0)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "InstanceManagerRights", + table: "Users", + type: "numeric(20,0)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.CreateTable( + name: "InstanceUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ByondRights = table.Column(type: "numeric(20,0)", nullable: false), + ChatBotRights = table.Column(type: "numeric(20,0)", nullable: false), + ConfigurationRights = table.Column(type: "numeric(20,0)", nullable: false), + DreamDaemonRights = table.Column(type: "numeric(20,0)", nullable: false), + DreamMakerRights = table.Column(type: "numeric(20,0)", nullable: false), + InstanceId = table.Column(type: "bigint", nullable: false), + InstanceUserRights = table.Column(type: "numeric(20,0)", nullable: false), + RepositoryRights = table.Column(type: "numeric(20,0)", nullable: false), + UserId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstanceUsers", x => x.Id); + table.ForeignKey( + name: "FK_InstanceUsers_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstanceUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_InstanceId", + table: "InstanceUsers", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_UserId_InstanceId", + table: "InstanceUsers", + columns: new[] { "UserId", "InstanceId" }, + unique: true); + + migrationBuilder.Sql( + "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.Id = p.UserId WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.DropForeignKey( + name: "FK_Users_Groups_GroupId", + table: "Users"); + + migrationBuilder.DropTable( + name: "InstancePermissionSets"); + + migrationBuilder.DropTable( + name: "PermissionSets"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropIndex( + name: "IX_Users_GroupId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "GroupId", + table: "Users"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.Designer.cs new file mode 100644 index 0000000000..0714682d93 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.Designer.cs @@ -0,0 +1,879 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20201214182101_SLAddUserGroups")] + partial class SLAddUserGroups + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondRights") + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("INTEGER"); + + b.Property("PermissionSetId") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.cs new file mode 100644 index 0000000000..ef40a990e2 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214182101_SLAddUserGroups.cs @@ -0,0 +1,308 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds UserGroups for SQLite. + /// + public partial class SLAddUserGroups : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.AddColumn( + name: "GroupId", + table: "Users", + nullable: true); + + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AdministrationRights = table.Column(nullable: false), + InstanceManagerRights = table.Column(nullable: false), + UserId = table.Column(nullable: true), + GroupId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_PermissionSets_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PermissionSets_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InstancePermissionSets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PermissionSetId = table.Column(nullable: false), + InstancePermissionSetRights = table.Column(nullable: false), + ByondRights = table.Column(nullable: false), + DreamDaemonRights = table.Column(nullable: false), + DreamMakerRights = table.Column(nullable: false), + RepositoryRights = table.Column(nullable: false), + ChatBotRights = table.Column(nullable: false), + ConfigurationRights = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstancePermissionSets", x => x.Id); + table.ForeignKey( + name: "FK_InstancePermissionSets_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstancePermissionSets_PermissionSets_PermissionSetId", + column: x => x.PermissionSetId, + principalTable: "PermissionSets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.RenameTable( + name: "Users", + newName: "Users_up"); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Enabled = table.Column(nullable: false), + CreatedAt = table.Column(nullable: false), + SystemIdentifier = table.Column(nullable: true), + Name = table.Column(maxLength: 10000, nullable: false), + PasswordHash = table.Column(nullable: true), + CreatedById = table.Column(nullable: true), + GroupId = table.Column(nullable: true), + CanonicalName = table.Column(nullable: false), + LastPasswordUpdate = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Users_CreatedById", + column: x => x.CreatedById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Users_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_GroupId", + table: "Users", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_Groups_Name", + table: "Groups", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_InstanceId", + table: "InstancePermissionSets", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstancePermissionSets_PermissionSetId_InstanceId", + table: "InstancePermissionSets", + columns: new[] { "PermissionSetId", "InstanceId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_GroupId", + table: "PermissionSets", + column: "GroupId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PermissionSets_UserId", + table: "PermissionSets", + column: "UserId", + unique: true); + + migrationBuilder.Sql( + "INSERT INTO Users (Id, Enabled, CreatedAt, SystemIdentifier, Name, PasswordHash, CreatedById, CanonicalName, LastPasswordUpdate) SELECT Id, Enabled, CreatedAt, SystemIdentifier, Name, PasswordHash, CreatedById, CanonicalName, LastPasswordUpdate FROM Users_up"); + + migrationBuilder.Sql( + "INSERT INTO PermissionSets (UserId, AdministrationRights, InstanceManagerRights) SELECT Id, AdministrationRights, InstanceManagerRights FROM Users_up"); + + migrationBuilder.Sql( + "INSERT INTO InstancePermissionSets (PermissionSetId, InstanceId, InstancePermissionSetRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.Id, iu.InstanceId, iu.InstanceUserRights, iu.ByondRights, iu.DreamDaemonRights, iu.DreamMakerRights, iu.RepositoryRights, iu.ChatBotRights, iu.ConfigurationRights FROM InstanceUsers iu JOIN PermissionSets p ON iu.UserId = p.UserId"); + + migrationBuilder.DropTable( + name: "InstanceUsers"); + + migrationBuilder.DropTable( + name: "Users_up"); + + // Had to do this in SLAllowNullDMApi too, renames confuse the fuck out of the ORM + migrationBuilder.RenameTable( + name: "Users", + newName: "Users_up"); + + migrationBuilder.RenameTable( + name: "Users_up", + newName: "Users"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.CreateTable( + name: "InstanceUsers", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ByondRights = table.Column(type: "INTEGER", nullable: false), + ChatBotRights = table.Column(type: "INTEGER", nullable: false), + ConfigurationRights = table.Column(type: "INTEGER", nullable: false), + DreamDaemonRights = table.Column(type: "INTEGER", nullable: false), + DreamMakerRights = table.Column(type: "INTEGER", nullable: false), + InstanceId = table.Column(type: "INTEGER", nullable: false), + InstanceUserRights = table.Column(type: "INTEGER", nullable: false), + RepositoryRights = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstanceUsers", x => x.Id); + table.ForeignKey( + name: "FK_InstanceUsers_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstanceUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_InstanceId", + table: "InstanceUsers", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_UserId_InstanceId", + table: "InstanceUsers", + columns: new[] { "UserId", "InstanceId" }, + unique: true); + + migrationBuilder.RenameTable( + name: "Users", + newName: "Users_down"); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Enabled = table.Column(nullable: false), + CreatedAt = table.Column(nullable: false), + SystemIdentifier = table.Column(nullable: true), + Name = table.Column(maxLength: 100, nullable: false), + AdministrationRights = table.Column(nullable: false), + InstanceManagerRights = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + CreatedById = table.Column(nullable: true), + CanonicalName = table.Column(maxLength: 100, nullable: false), + LastPasswordUpdate = table.Column(nullable: true), + GroupId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Users_CreatedById", + column: x => x.CreatedById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.Sql( + "INSERT INTO Users (Id, Enabled, CreatedAt, SystemIdentifier, Name, PasswordHash, CreatedById, CanonicalName, LastPasswordUpdate, AdministrationRights, InstanceManagerRights) SELECT u.Id, u.Enabled, u.CreatedAt, u.SystemIdentifier, u.Name, u.PasswordHash, u.CreatedById, u.CanonicalName, u.LastPasswordUpdate, p.AdministrationRights, p.InstanceManagerRights FROM Users_down u JOIN PermissionSets p ON p.UserId = u.Id WHERE u.GroupId = NULL"); + + migrationBuilder.Sql( + "INSERT INTO Users (Id, Enabled, CreatedAt, SystemIdentifier, Name, PasswordHash, CreatedById, CanonicalName, LastPasswordUpdate, AdministrationRights, InstanceManagerRights) SELECT u.Id, u.Enabled, u.CreatedAt, u.SystemIdentifier, u.Name, u.PasswordHash, u.CreatedById, u.CanonicalName, u.LastPasswordUpdate, p.AdministrationRights, p.InstanceManagerRights FROM Users_down u JOIN PermissionSets p ON p.GroupId = u.GroupId WHERE u.GroupId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + + migrationBuilder.Sql( + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users_down u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + + migrationBuilder.DropTable( + name: "InstancePermissionSets"); + + migrationBuilder.DropTable( + name: "PermissionSets"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropTable( + name: "Users_down"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "Users_down"); + + migrationBuilder.RenameTable( + name: "Users_down", + newName: "Users"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index 5d653e7651..9d1317fd6d 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -280,7 +280,7 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Instances"); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -304,23 +304,23 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("InstanceId") .HasColumnType("bigint"); - b.Property("InstanceUserRights") + b.Property("InstancePermissionSetRights") .HasColumnType("bigint unsigned"); + b.Property("PermissionSetId") + .HasColumnType("bigint"); + b.Property("RepositoryRights") .HasColumnType("bigint unsigned"); - b.Property("UserId") - .HasColumnType("bigint"); - b.HasKey("Id"); b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") + b.HasIndex("PermissionSetId", "InstanceId") .IsUnique(); - b.ToTable("InstanceUsers"); + b.ToTable("InstancePermissionSets"); }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => @@ -403,6 +403,35 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("OAuthConnections"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -602,12 +631,10 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - b.Property("AdministrationRights") - .HasColumnType("bigint unsigned"); - b.Property("CanonicalName") .IsRequired() - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); b.Property("CreatedAt") .IsRequired() @@ -620,22 +647,23 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("tinyint(1)"); - b.Property("InstanceManagerRights") - .HasColumnType("bigint unsigned"); + b.Property("GroupId") + .HasColumnType("bigint"); b.Property("LastPasswordUpdate") .HasColumnType("datetime(6)"); b.Property("Name") .IsRequired() - .HasColumnType("longtext CHARACTER SET utf8mb4") - .HasMaxLength(10000); + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); b.Property("PasswordHash") .HasColumnType("longtext CHARACTER SET utf8mb4"); b.Property("SystemIdentifier") - .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); b.HasKey("Id"); @@ -644,12 +672,33 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasIndex("CreatedById"); + b.HasIndex("GroupId"); + b.HasIndex("SystemIdentifier") .IsUnique(); b.ToTable("Users"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -701,17 +750,17 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") + .WithMany("InstancePermissionSets") .HasForeignKey("InstanceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -743,6 +792,19 @@ namespace Tgstation.Server.Host.Database.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") @@ -805,6 +867,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") .WithMany("CreatedUsers") .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); }); #pragma warning restore 612, 618 } diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index 6d4d6e91a4..2e57e221e0 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -279,7 +279,7 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Instances"); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -304,23 +304,23 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("InstanceId") .HasColumnType("bigint"); - b.Property("InstanceUserRights") + b.Property("InstancePermissionSetRights") .HasColumnType("numeric(20,0)"); + b.Property("PermissionSetId") + .HasColumnType("bigint"); + b.Property("RepositoryRights") .HasColumnType("numeric(20,0)"); - b.Property("UserId") - .HasColumnType("bigint"); - b.HasKey("Id"); b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") + b.HasIndex("PermissionSetId", "InstanceId") .IsUnique(); - b.ToTable("InstanceUsers"); + b.ToTable("InstancePermissionSets"); }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => @@ -405,6 +405,36 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("OAuthConnections"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -610,12 +640,10 @@ namespace Tgstation.Server.Host.Database.Migrations .HasColumnType("bigint") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("AdministrationRights") - .HasColumnType("numeric(20,0)"); - b.Property("CanonicalName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("character varying(100)") + .HasMaxLength(100); b.Property("CreatedAt") .IsRequired() @@ -628,22 +656,23 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("boolean"); - b.Property("InstanceManagerRights") - .HasColumnType("numeric(20,0)"); + b.Property("GroupId") + .HasColumnType("bigint"); b.Property("LastPasswordUpdate") .HasColumnType("timestamp with time zone"); b.Property("Name") .IsRequired() - .HasColumnType("character varying(10000)") - .HasMaxLength(10000); + .HasColumnType("character varying(100)") + .HasMaxLength(100); b.Property("PasswordHash") .HasColumnType("text"); b.Property("SystemIdentifier") - .HasColumnType("text"); + .HasColumnType("character varying(100)") + .HasMaxLength(100); b.HasKey("Id"); @@ -652,12 +681,34 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasIndex("CreatedById"); + b.HasIndex("GroupId"); + b.HasIndex("SystemIdentifier") .IsUnique(); b.ToTable("Users"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -709,17 +760,17 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") + .WithMany("InstancePermissionSets") .HasForeignKey("InstanceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -751,6 +802,19 @@ namespace Tgstation.Server.Host.Database.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") @@ -813,6 +877,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") .WithMany("CreatedUsers") .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); }); #pragma warning restore 612, 618 } diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index 44dab83a79..a6c42736a5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -281,7 +281,7 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Instances"); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -306,23 +306,23 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("InstanceId") .HasColumnType("bigint"); - b.Property("InstanceUserRights") + b.Property("InstancePermissionSetRights") .HasColumnType("decimal(20,0)"); + b.Property("PermissionSetId") + .HasColumnType("bigint"); + b.Property("RepositoryRights") .HasColumnType("decimal(20,0)"); - b.Property("UserId") - .HasColumnType("bigint"); - b.HasKey("Id"); b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") + b.HasIndex("PermissionSetId", "InstanceId") .IsUnique(); - b.ToTable("InstanceUsers"); + b.ToTable("InstancePermissionSets"); }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => @@ -407,6 +407,38 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("OAuthConnections"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique() + .HasFilter("[GroupId] IS NOT NULL"); + + b.HasIndex("UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PermissionSets"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -612,12 +644,10 @@ namespace Tgstation.Server.Host.Database.Migrations .HasColumnType("bigint") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("AdministrationRights") - .HasColumnType("decimal(20,0)"); - b.Property("CanonicalName") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); b.Property("CreatedAt") .IsRequired() @@ -630,22 +660,23 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("bit"); - b.Property("InstanceManagerRights") - .HasColumnType("decimal(20,0)"); + b.Property("GroupId") + .HasColumnType("bigint"); b.Property("LastPasswordUpdate") .HasColumnType("datetimeoffset"); b.Property("Name") .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(10000); + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); b.Property("PasswordHash") .HasColumnType("nvarchar(max)"); b.Property("SystemIdentifier") - .HasColumnType("nvarchar(450)"); + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); b.HasKey("Id"); @@ -654,6 +685,8 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasIndex("CreatedById"); + b.HasIndex("GroupId"); + b.HasIndex("SystemIdentifier") .IsUnique() .HasFilter("[SystemIdentifier] IS NOT NULL"); @@ -661,6 +694,26 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -712,17 +765,17 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") + .WithMany("InstancePermissionSets") .HasForeignKey("InstanceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -754,6 +807,19 @@ namespace Tgstation.Server.Host.Database.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") @@ -816,6 +882,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") .WithMany("CreatedUsers") .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); }); #pragma warning restore 612, 618 } diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index d67f026abb..59ee3f2b8f 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -279,7 +279,7 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Instances"); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -303,23 +303,23 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("InstanceId") .HasColumnType("INTEGER"); - b.Property("InstanceUserRights") + b.Property("InstancePermissionSetRights") + .HasColumnType("INTEGER"); + + b.Property("PermissionSetId") .HasColumnType("INTEGER"); b.Property("RepositoryRights") .HasColumnType("INTEGER"); - b.Property("UserId") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("InstanceId"); - b.HasIndex("UserId", "InstanceId") + b.HasIndex("PermissionSetId", "InstanceId") .IsUnique(); - b.ToTable("InstanceUsers"); + b.ToTable("InstancePermissionSets"); }); modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => @@ -402,6 +402,35 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("OAuthConnections"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.Property("Id") @@ -601,12 +630,10 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AdministrationRights") - .HasColumnType("INTEGER"); - b.Property("CanonicalName") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasMaxLength(100); b.Property("CreatedAt") .IsRequired() @@ -619,7 +646,7 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("INTEGER"); - b.Property("InstanceManagerRights") + b.Property("GroupId") .HasColumnType("INTEGER"); b.Property("LastPasswordUpdate") @@ -628,13 +655,14 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("Name") .IsRequired() .HasColumnType("TEXT") - .HasMaxLength(10000); + .HasMaxLength(100); b.Property("PasswordHash") .HasColumnType("TEXT"); b.Property("SystemIdentifier") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasMaxLength(100); b.HasKey("Id"); @@ -643,12 +671,33 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasIndex("CreatedById"); + b.HasIndex("GroupId"); + b.HasIndex("SystemIdentifier") .IsUnique(); b.ToTable("Users"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -700,17 +749,17 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") - .WithMany("InstanceUsers") + .WithMany("InstancePermissionSets") .HasForeignKey("InstanceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Tgstation.Server.Host.Models.User", null) - .WithMany("InstanceUsers") - .HasForeignKey("UserId") + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -742,6 +791,19 @@ namespace Tgstation.Server.Host.Database.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") @@ -804,6 +866,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") .WithMany("CreatedUsers") .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); }); #pragma warning restore 612, 618 } diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index f23aedca95..a22872e935 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System; using System.Linq; diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 1827a9511d..311bd154ec 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Tgstation.Server.Host.Models { @@ -28,9 +28,9 @@ namespace Tgstation.Server.Host.Models public RepositorySettings RepositorySettings { get; set; } /// - /// The s in the + /// The s in the /// - public ICollection InstanceUsers { get; set; } + public ICollection InstancePermissionSets { get; set; } /// /// The s for the diff --git a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs new file mode 100644 index 0000000000..937b4093d1 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Host.Models +{ + /// + public sealed class InstancePermissionSet : Api.Models.InstancePermissionSet + { + /// + /// The row Id + /// + public long Id { get; set; } + + /// + /// The of + /// + public long InstanceId { get; set; } + + /// + /// The the belongs to. + /// + [Required] + public Instance Instance { get; set; } + + /// + /// The the belongs to. + /// + [Required] + public PermissionSet PermissionSet { get; set; } + + /// + /// Convert the to it's API form + /// + /// A new + public Api.Models.InstancePermissionSet ToApi() => new Api.Models.InstancePermissionSet + { + ByondRights = ByondRights, + ChatBotRights = ChatBotRights, + ConfigurationRights = ConfigurationRights, + DreamDaemonRights = DreamDaemonRights, + DreamMakerRights = DreamMakerRights, + RepositoryRights = RepositoryRights, + InstancePermissionSetRights = InstancePermissionSetRights, + PermissionSetId = PermissionSetId + }; + } +} diff --git a/src/Tgstation.Server.Host/Models/InstanceUser.cs b/src/Tgstation.Server.Host/Models/InstanceUser.cs deleted file mode 100644 index 2aa74e5042..0000000000 --- a/src/Tgstation.Server.Host/Models/InstanceUser.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Tgstation.Server.Host.Models -{ - /// - public sealed class InstanceUser : Api.Models.InstanceUser - { - /// - /// The row Id - /// - public long Id { get; set; } - - /// - /// The of - /// - public long InstanceId { get; set; } - - /// - /// The the belongs to - /// - [Required] - public Instance Instance { get; set; } - - /// - /// Convert the to it's API form - /// - /// A new - public Api.Models.InstanceUser ToApi() => new Api.Models.InstanceUser - { - ByondRights = ByondRights, - ChatBotRights = ChatBotRights, - ConfigurationRights = ConfigurationRights, - DreamDaemonRights = DreamDaemonRights, - DreamMakerRights = DreamMakerRights, - RepositoryRights = RepositoryRights, - InstanceUserRights = InstanceUserRights, - UserId = UserId - }; - } -} diff --git a/src/Tgstation.Server.Host/Models/PermissionSet.cs b/src/Tgstation.Server.Host/Models/PermissionSet.cs new file mode 100644 index 0000000000..ccc3e51b7f --- /dev/null +++ b/src/Tgstation.Server.Host/Models/PermissionSet.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Tgstation.Server.Host.Models +{ + /// + public sealed class PermissionSet : Api.Models.PermissionSet + { + /// + /// The of . + /// + public long? UserId { get; set; } + + /// + /// The of . + /// + public long? GroupId { get; set; } + + /// + /// The the belongs to, if it is for a . + /// + public User User { get; set; } + + /// + /// The the belongs to, if it is for a . + /// + public UserGroup Group { get; set; } + + /// + /// The s associated with the . + /// + public ICollection InstancePermissionSets { get; set; } + + /// + /// Convert the to it's API form. + /// + /// A new . + public Api.Models.PermissionSet ToApi() => new Api.Models.PermissionSet + { + Id = Id, + AdministrationRights = AdministrationRights, + InstanceManagerRights = InstanceManagerRights, + }; + } +} diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 263112f193..ac92eb5af9 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { @@ -23,10 +24,26 @@ namespace Tgstation.Server.Host.Models /// public User CreatedBy { get; set; } + /// + /// The the belongs to, if any. + /// + public UserGroup Group { get; set; } + + /// + /// The ID of the 's . + /// + public long? GroupId { get; set; } + + /// + /// The the has, if any. + /// + public PermissionSet PermissionSet { get; set; } + /// /// The uppercase invariant of /// [Required] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string CanonicalName { get; set; } /// @@ -39,11 +56,6 @@ namespace Tgstation.Server.Host.Models /// public ICollection CreatedUsers { get; set; } - /// - /// The s for the - /// - public ICollection InstanceUsers { get; set; } - /// /// The s made by the /// @@ -69,23 +81,25 @@ namespace Tgstation.Server.Host.Models /// A new Api.Models.User ToApi(bool recursive, bool showDetails) => new Api.Models.User { - AdministrationRights = showDetails ? AdministrationRights : null, CreatedAt = CreatedAt, CreatedBy = recursive ? CreatedBy?.ToApi(false, false) : null, Enabled = Enabled, Id = Id, - InstanceManagerRights = showDetails ? InstanceManagerRights : null, Name = Name, SystemIdentifier = showDetails ? SystemIdentifier : null, - OAuthConnections = OAuthConnections - ?.Select(x => x.ToApi()) - .ToList(), + OAuthConnections = showDetails + ? OAuthConnections + ?.Select(x => x.ToApi()) + .ToList() + : null, + Group = showDetails ? Group?.ToApi(false) : null, + PermissionSet = showDetails ? PermissionSet?.ToApi() : null, }; /// /// Convert the to it's API form /// - /// If rights and system identifier should be shown + /// If system identifier, oauth connections, and group/permission set should be shown. /// A new public Api.Models.User ToApi(bool showDetails) => ToApi(true, showDetails); } diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs new file mode 100644 index 0000000000..c81fae1708 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Tgstation.Server.Host.Models +{ + /// + public sealed class UserGroup : Api.Models.Internal.UserGroup + { + /// + /// The the has. + /// + [Required] + public PermissionSet PermissionSet { get; set; } + + /// + /// The s the has. + /// + public ICollection Users { get; set; } + + /// + /// Convert the to it's API form. + /// + /// If should be populated. + /// A new . + public Api.Models.UserGroup ToApi(bool showUsers) => new Api.Models.UserGroup + { + Id = Id, + Name = Name, + PermissionSet = PermissionSet?.ToApi(), + Users = showUsers + ? Users?.Select(x => x.ToApi(false)).OfType().ToList() ?? new List() + : null, + }; + } +} diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs index b621a41c04..23b82e350a 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; @@ -14,7 +14,10 @@ namespace Tgstation.Server.Host.Security public User User { get; } /// - public InstanceUser InstanceUser { get; } + public PermissionSet PermissionSet { get; } + + /// + public InstancePermissionSet InstancePermissionSet { get; } /// public ISystemIdentity SystemIdentity { get; } @@ -29,13 +32,16 @@ namespace Tgstation.Server.Host.Security /// /// The value of /// The value of - /// The value of - public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser) + /// The value of + public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstancePermissionSet instanceUser) { User = user ?? throw new ArgumentNullException(nameof(user)); if (systemIdentity == null && User.SystemIdentifier != null) throw new ArgumentNullException(nameof(systemIdentity)); - InstanceUser = instanceUser; + PermissionSet = user.PermissionSet + ?? user.Group.PermissionSet + ?? throw new ArgumentException("No PermissionSet provider", nameof(user)); + InstancePermissionSet = instanceUser; SystemIdentity = systemIdentity; } @@ -50,19 +56,19 @@ namespace Tgstation.Server.Host.Security if (User == null) throw new InvalidOperationException("Authentication context has no user!"); - if (isInstance && InstanceUser == null) + if (isInstance && InstancePermissionSet == null) return 0; var rightsEnum = RightsHelper.RightToType(rightsType); // use the api versions because they're the ones that contain the actual properties - var typeToCheck = isInstance ? typeof(InstanceUser) : typeof(User); + var typeToCheck = isInstance ? typeof(InstancePermissionSet) : typeof(PermissionSet); var nullableType = typeof(Nullable<>); var nullableRightsType = nullableType.MakeGenericType(rightsEnum); var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType).First(); - var right = prop.GetMethod.Invoke(isInstance ? (object)InstanceUser : User, Array.Empty()); + var right = prop.GetMethod.Invoke(isInstance ? (object)InstancePermissionSet : PermissionSet, Array.Empty()); if (right == null) throw new InvalidOperationException("A user right was null!"); diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 04bf2e2062..bef18b9b7c 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -60,6 +60,9 @@ namespace Tgstation.Server.Host.Security .AsQueryable() .Where(x => x.Id == userId) .Include(x => x.CreatedBy) + .Include(x => x.PermissionSet) + .Include(x => x.Group) + .ThenInclude(x => x.PermissionSet) .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -85,23 +88,24 @@ namespace Tgstation.Server.Host.Security systemIdentity = null; } + var userPermissionSet = user.PermissionSet ?? user.Group.PermissionSet; try { - InstanceUser instanceUser = null; + InstancePermissionSet instancePermissionSet = null; if (instanceId.HasValue) { - instanceUser = await databaseContext.InstanceUsers + instancePermissionSet = await databaseContext.InstancePermissionSets .AsQueryable() - .Where(x => x.UserId == userId && x.InstanceId == instanceId) + .Where(x => x.PermissionSetId == userPermissionSet.Id && x.InstanceId == instanceId) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (instanceUser == null) + if (instancePermissionSet == null) logger.LogDebug("User {0} does not have permissions on instance {1}!", userId, instanceId.Value); } - CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser); + CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instancePermissionSet); } catch { diff --git a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs index 00c3ce2bed..b370cb62a3 100644 --- a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs +++ b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Security // we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid // if user is null that means they got the token with an expired password var rightAsULong = authenticationContext.User == null - || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) + || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstancePermissionSet == null) ? ~0UL : authenticationContext.GetRight(I); var rightEnum = RightsHelper.RightToType(I); diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs index 6d8b53894d..4b81a2244a 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; @@ -15,15 +15,20 @@ namespace Tgstation.Server.Host.Security User User { get; } /// - /// The of if applicable + /// The 's effective . /// - InstanceUser InstanceUser { get; } + PermissionSet PermissionSet { get; } + + /// + /// The 's effective if applicable. + /// + InstancePermissionSet InstancePermissionSet { get; } /// /// Get the value of a given /// /// The of the right to get - /// The value of . Note that if is all based rights will return 0 + /// The value of . Note that if is all based rights will return 0 ulong GetRight(RightsType rightsType); /// @@ -31,4 +36,4 @@ namespace Tgstation.Server.Host.Security /// ISystemIdentity SystemIdentity { get; } } -} \ No newline at end of file +} diff --git a/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs b/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs index c5c45cebce..f97b7c0795 100644 --- a/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs +++ b/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; using Tgstation.Server.Api.Rights; @@ -18,12 +18,15 @@ namespace Tgstation.Server.Host.Security.Tests Assert.ThrowsException(() => new AuthenticationContext(null, null, null)); var mockSystemIdentity = new Mock(); - var user = new User(); + var user = new User() + { + PermissionSet = new PermissionSet() + }; var authContext = new AuthenticationContext(null, user, null); Assert.ThrowsException(() => new AuthenticationContext(mockSystemIdentity.Object, null, null)); - var instanceUser = new InstanceUser(); + var instanceUser = new InstancePermissionSet(); Assert.ThrowsException(() => new AuthenticationContext(null, null, instanceUser)); Assert.ThrowsException(() => new AuthenticationContext(mockSystemIdentity.Object, null, instanceUser)); @@ -38,13 +41,16 @@ namespace Tgstation.Server.Host.Security.Tests [TestMethod] public void TestGetRightsGeneric() { - var user = new User(); - var instanceUser = new InstanceUser(); + var user = new User() + { + PermissionSet = new PermissionSet() + }; + var instanceUser = new InstancePermissionSet(); var authContext = new AuthenticationContext(null, user, instanceUser); - user.AdministrationRights = AdministrationRights.WriteUsers; + user.PermissionSet.AdministrationRights = AdministrationRights.WriteUsers; instanceUser.ByondRights = ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ReadActive; - Assert.AreEqual((ulong)user.AdministrationRights, authContext.GetRight(RightsType.Administration)); + Assert.AreEqual((ulong)user.PermissionSet.AdministrationRights, authContext.GetRight(RightsType.Administration)); Assert.AreEqual((ulong)instanceUser.ByondRights, authContext.GetRight(RightsType.Byond)); } } diff --git a/tests/Tgstation.Server.Tests/ApiAssert.cs b/tests/Tgstation.Server.Tests/ApiAssert.cs index fcb7f8ba9b..0689fe51d2 100644 --- a/tests/Tgstation.Server.Tests/ApiAssert.cs +++ b/tests/Tgstation.Server.Tests/ApiAssert.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -27,7 +27,7 @@ namespace Tgstation.Server.Tests } catch (TApiException ex) { - Assert.AreEqual(expectedErrorCode, ex.ErrorCode, "Wrong error code for expected API exception!"); + Assert.AreEqual(expectedErrorCode, ex.ErrorCode, $"Wrong error code for expected API exception! Additional Data: {ex.AdditionalServerData}"); return; } diff --git a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs index fbab7dec20..783d6b45ee 100644 --- a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; using System.Linq; @@ -142,16 +142,16 @@ namespace Tgstation.Server.Tests var instanceClient = instanceManagerClient.CreateClient(firstTest); //can regain permissions on instance without instance user - var ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false); - await instanceClient.Users.Delete(ourInstanceUser, cancellationToken).ConfigureAwait(false); + var ourInstanceUser = await instanceClient.PermissionSets.Read(cancellationToken).ConfigureAwait(false); + await instanceClient.PermissionSets.Delete(ourInstanceUser, cancellationToken).ConfigureAwait(false); - await Assert.ThrowsExceptionAsync(() => instanceClient.Users.Read(cancellationToken)).ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(() => instanceClient.PermissionSets.Read(cancellationToken)).ConfigureAwait(false); await instanceManagerClient.GrantPermissions(new Api.Models.Instance { Id = firstTest.Id }, cancellationToken).ConfigureAwait(false); - ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false); + ourInstanceUser = await instanceClient.PermissionSets.Read(cancellationToken).ConfigureAwait(false); Assert.AreEqual(RightsHelper.AllRights(), ourInstanceUser.DreamDaemonRights.Value); @@ -174,7 +174,10 @@ namespace Tgstation.Server.Tests var update = new UserUpdate { Id = current.Id, - InstanceManagerRights = InstanceManagerRights.SetChatBotLimit + PermissionSet = new PermissionSet + { + InstanceManagerRights = InstanceManagerRights.SetChatBotLimit + } }; await usersClient.Update(update, cancellationToken); var update2 = new Api.Models.Instance @@ -184,7 +187,7 @@ namespace Tgstation.Server.Tests }; var newThing = await instanceManagerClient.Update(update2, cancellationToken); - update.InstanceManagerRights |= InstanceManagerRights.Delete | InstanceManagerRights.Create | InstanceManagerRights.List; + update.PermissionSet.InstanceManagerRights |= InstanceManagerRights.Delete | InstanceManagerRights.Create | InstanceManagerRights.List; await usersClient.Update(update, cancellationToken); //but only if the attach file exists diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index d0d50482eb..abb4e6ba1d 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -7,6 +7,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -20,6 +21,7 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; @@ -164,7 +166,7 @@ namespace Tgstation.Server.Tests Assert.Inconclusive("No/invalid database type configured in env var TGS4_TEST_DATABASE_TYPE!"); string migrationName = null; - DbContext CreateContext() + DatabaseContext CreateContext() { string serverVersion = Environment.GetEnvironmentVariable($"{DatabaseConfiguration.Section}__{nameof(DatabaseConfiguration.ServerVersion)}"); if (String.IsNullOrWhiteSpace(serverVersion)) @@ -205,15 +207,95 @@ namespace Tgstation.Server.Tests return null; } - Task Delete(DbContext context) => databaseType == DatabaseType.Sqlite ? Task.CompletedTask : context.Database.EnsureCreatedAsync(); - using var context = CreateContext(); - await Delete(context); + await context.Database.EnsureDeletedAsync(); await context.Database.MigrateAsync(default); + + // add usergroups and dummy instances for testing purposes + var group = new Host.Models.UserGroup + { + PermissionSet = new Host.Models.PermissionSet + { + AdministrationRights = AdministrationRights.ChangeVersion, + InstanceManagerRights = InstanceManagerRights.GrantPermissions + }, + Name = "TestGroup", + }; + + const string TestUserName = "TestUser42"; + var user = new Host.Models.User + { + Name = TestUserName, + CreatedAt = DateTimeOffset.Now, + OAuthConnections = new List(), + CanonicalName = Host.Models.User.CanonicalizeName(TestUserName), + Enabled = false, + Group = group, + PasswordHash = "_", + }; + + var instance = new Host.Models.Instance + { + AutoUpdateInterval = 0, + ChatBotLimit = 1, + ChatSettings = new List(), + ConfigurationType = ConfigurationType.HostWrite, + DreamDaemonSettings = new Host.Models.DreamDaemonSettings + { + AllowWebClient = false, + AutoStart = false, + HeartbeatSeconds = 0, + Port = 1447, + SecurityLevel = DreamDaemonSecurity.Safe, + StartupTimeout = 1000, + TopicRequestTimeout = 1000, + AdditionalParameters = String.Empty, + }, + DreamMakerSettings = new Host.Models.DreamMakerSettings + { + ApiValidationPort = 1557, + ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted, + RequireDMApiValidation = false, + }, + InstancePermissionSets = new List + { + new Host.Models.InstancePermissionSet + { + ByondRights = ByondRights.InstallCustomVersion, + ChatBotRights = ChatBotRights.None, + ConfigurationRights = ConfigurationRights.Read, + DreamDaemonRights = DreamDaemonRights.ReadRevision, + DreamMakerRights = DreamMakerRights.SetApiValidationPort, + InstancePermissionSetRights = InstancePermissionSetRights.Write, + PermissionSet = group.PermissionSet, + RepositoryRights = RepositoryRights.SetReference + } + }, + Name = "sfdsadfsa", + Online = false, + Path = "/a/b/c/d", + RepositorySettings = new Host.Models.RepositorySettings + { + AutoUpdatesKeepTestMerges = false, + AutoUpdatesSynchronize = false, + CommitterEmail = "email@eample.com", + CommitterName = "blubluh", + CreateGitHubDeployments = false, + PostTestMergeComment = false, + PushTestMergeCommits = false, + ShowTestMergeCommitters = false, + }, + }; + + context.Users.Add(user); + context.Groups.Add(group); + context.Instances.Add(instance); + await context.Save(default); + var dbServiceProvider = ((IInfrastructure)context.Database).Instance; var migrator = dbServiceProvider.GetRequiredService(); await migrator.MigrateAsync(migrationName, default); - await Delete(context); + await context.Database.EnsureDeletedAsync(); } #endif @@ -275,11 +357,12 @@ namespace Tgstation.Server.Tests var rootTest = FailFast(new RootTest().Run(clientFactory, adminClient, cancellationToken)); var adminTest = FailFast(new AdministrationTest(adminClient.Administration).Run(cancellationToken)); - var usersTest = FailFast(new UsersTest(adminClient.Users).Run(cancellationToken)); instance = await new InstanceManagerTest(adminClient.Instances, adminClient.Users, server.Directory).RunPreInstanceTest(cancellationToken); - Assert.IsTrue(Directory.Exists(instance.Path)); var instanceClient = adminClient.Instances.CreateClient(instance); + + var usersTest = FailFast(new UsersTest(adminClient, instanceClient).Run(cancellationToken)); + Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances).RunTests(cancellationToken)); diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 3459c2a11a..6a0788af86 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -93,7 +93,8 @@ namespace Tgstation.Server.Tests { args.Add($"Security:OAuth:{I}:ClientId=Fake"); args.Add($"Security:OAuth:{I}:ClientSecret=Faker"); - args.Add($"Security:OAuth:{I}:Url=https://fakest.com"); + args.Add($"Security:OAuth:{I}:RedirectUrl=https://fakest.com"); + args.Add($"Security:OAuth:{I}:ServerUrl=https://fakestest.com"); } // SPECIFICALLY DELETE THE DEV APPSETTINGS, WE DON'T WANT IT IN THE WAY diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 0110d8a4dc..360f49a1af 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -5,18 +5,22 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; +using Tgstation.Server.Client.Components; using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests { sealed class UsersTest { - readonly IUsersClient client; + readonly IServerClient serverClient; + readonly IInstanceClient instanceClient; - public UsersTest(IUsersClient client) + public UsersTest(IServerClient serverClient, IInstanceClient instanceClient) { - this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.serverClient = serverClient ?? throw new ArgumentNullException(nameof(serverClient)); + this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); } public async Task Run(CancellationToken cancellationToken) @@ -29,24 +33,28 @@ namespace Tgstation.Server.Tests async Task BasicTests(CancellationToken cancellationToken) { - var user = await client.Read(cancellationToken).ConfigureAwait(false); + var user = await serverClient.Users.Read(cancellationToken).ConfigureAwait(false); Assert.IsNotNull(user); Assert.AreEqual("Admin", user.Name); Assert.IsNull(user.SystemIdentifier); Assert.AreEqual(true, user.Enabled); Assert.IsNotNull(user.OAuthConnections); + Assert.IsNotNull(user.PermissionSet); + Assert.IsNotNull(user.PermissionSet.Id); + Assert.IsNotNull(user.PermissionSet.InstanceManagerRights); + Assert.IsNotNull(user.PermissionSet.AdministrationRights); var systemUser = user.CreatedBy; Assert.IsNotNull(systemUser); Assert.AreEqual("TGS", systemUser.Name); Assert.AreEqual(false, systemUser.Enabled); - var users = await client.List(cancellationToken); + var users = await serverClient.Users.List(cancellationToken); Assert.IsTrue(users.Count > 0); Assert.IsFalse(users.Any(x => x.Id == systemUser.Id)); - await ApiAssert.ThrowsException(() => client.GetId(systemUser, cancellationToken), null); - await ApiAssert.ThrowsException(() => client.Update(new UserUpdate + await ApiAssert.ThrowsException(() => serverClient.Users.GetId(systemUser, cancellationToken), null); + await ApiAssert.ThrowsException(() => serverClient.Users.Update(new UserUpdate { Id = systemUser.Id }, cancellationToken), null); @@ -59,13 +67,13 @@ namespace Tgstation.Server.Tests Provider = OAuthProvider.Discord } }; - await ApiAssert.ThrowsException(() => client.Update(new UserUpdate + await ApiAssert.ThrowsException(() => serverClient.Users.Update(new UserUpdate { Id = user.Id, OAuthConnections = sampleOAuthConnections }, cancellationToken), ErrorCode.AdminUserCannotOAuth); - var testUser = await client.Create( + var testUser = await serverClient.Users.Create( new UserUpdate { Name = $"BasicTestUser", @@ -74,7 +82,7 @@ namespace Tgstation.Server.Tests cancellationToken).ConfigureAwait(false); Assert.IsNotNull(testUser.OAuthConnections); - testUser = await client.Update( + testUser = await serverClient.Users.Update( new UserUpdate { Id = testUser.Id, @@ -85,6 +93,96 @@ namespace Tgstation.Server.Tests Assert.AreEqual(1, testUser.OAuthConnections.Count); Assert.AreEqual(sampleOAuthConnections.First().ExternalUserId, testUser.OAuthConnections.First().ExternalUserId); Assert.AreEqual(sampleOAuthConnections.First().Provider, testUser.OAuthConnections.First().Provider); + + + var group = await serverClient.Groups.Create( + new UserGroup + { + Name = "TestGroup" + }, + cancellationToken); + Assert.AreEqual(group.Name, "TestGroup"); + Assert.IsNotNull(group.PermissionSet); + Assert.IsNotNull(group.PermissionSet.Id); + Assert.AreEqual(AdministrationRights.None, group.PermissionSet.AdministrationRights); + Assert.AreEqual(InstanceManagerRights.None, group.PermissionSet.InstanceManagerRights); + + var group2 = await serverClient.Groups.Create(new UserGroup + { + Name = "TestGroup2", + PermissionSet = new PermissionSet + { + InstanceManagerRights = InstanceManagerRights.List + } + }, cancellationToken); + Assert.AreEqual(AdministrationRights.None, group2.PermissionSet.AdministrationRights); + Assert.AreEqual(InstanceManagerRights.List, group2.PermissionSet.InstanceManagerRights); + + var groups = await serverClient.Groups.List(cancellationToken); + Assert.AreEqual(2, groups.Count); + + foreach (var igroup in groups) + { + Assert.IsNotNull(igroup.Users); + Assert.IsNotNull(igroup.PermissionSet); + } + + await serverClient.Groups.Delete(group2, cancellationToken); + + groups = await serverClient.Groups.List(cancellationToken); + Assert.AreEqual(1, groups.Count); + + group.PermissionSet.InstanceManagerRights = RightsHelper.AllRights(); + group.PermissionSet.AdministrationRights = RightsHelper.AllRights(); + group.Users = null; + + group = await serverClient.Groups.Update(group, cancellationToken); + + Assert.AreEqual(RightsHelper.AllRights(), group.PermissionSet.AdministrationRights); + Assert.AreEqual(RightsHelper.AllRights(), group.PermissionSet.InstanceManagerRights); + + await ApiAssert.ThrowsException(() => serverClient.Groups.Update(group, cancellationToken), ErrorCode.UserGroupControllerCantEditMembers); + + var userUpdate = new UserUpdate + { + Id = user.Id, + PermissionSet = user.PermissionSet, + Group = new Api.Models.Internal.UserGroup + { + Id = group.Id + }, + }; + await ApiAssert.ThrowsException( + () => serverClient.Users.Update( + userUpdate, + cancellationToken), + ErrorCode.UserGroupAndPermissionSet); + + userUpdate.PermissionSet = null; + + await instanceClient.PermissionSets.Create(new InstancePermissionSet + { + PermissionSetId = group.PermissionSet.Id.Value, + ByondRights = RightsHelper.AllRights(), + ChatBotRights = RightsHelper.AllRights(), + ConfigurationRights = RightsHelper.AllRights(), + DreamDaemonRights = RightsHelper.AllRights(), + DreamMakerRights = RightsHelper.AllRights(), + InstancePermissionSetRights = RightsHelper.AllRights(), + RepositoryRights = RightsHelper.AllRights(), + }, cancellationToken); + + user = await serverClient.Users.Update(userUpdate, cancellationToken); + + Assert.IsNull(user.PermissionSet); + Assert.IsNotNull(user.Group); + Assert.AreEqual(group.Id, user.Group.Id); + + group = await serverClient.Groups.GetId(group, cancellationToken); + Assert.IsNotNull(group.Users); + Assert.AreEqual(1, group.Users.Count); + Assert.AreEqual(user.Id, group.Users.First().Id); + Assert.IsNotNull(group.PermissionSet); } async Task TestCreateSysUser(CancellationToken cancellationToken) @@ -95,9 +193,9 @@ namespace Tgstation.Server.Tests SystemIdentifier = sysId }; if (new PlatformIdentifier().IsWindows) - await client.Create(update, cancellationToken); + await serverClient.Users.Create(update, cancellationToken); else - await ApiAssert.ThrowsException(() => client.Create(update, cancellationToken), ErrorCode.RequiresPosixSystemIdentity); + await ApiAssert.ThrowsException(() => serverClient.Users.Create(update, cancellationToken), ErrorCode.RequiresPosixSystemIdentity); } async Task TestSpamCreation(CancellationToken cancellationToken) @@ -115,7 +213,7 @@ namespace Tgstation.Server.Tests for (int i = 0; i < RepeatCount; ++i) { tasks.Add( - client.Create( + serverClient.Users.Create( new UserUpdate { Name = $"SpamTestUser_{i}", From 6747196acf86cbaf2dbaf77e3155f7004843f84a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 14 Dec 2020 18:01:21 -0500 Subject: [PATCH 053/154] UserGroups Migration Fixes - Fix referencing InstancePermissionSet.InstanceUserRights in serveral places. - Fix using non-case sensitive names in postgres --- .../Migrations/20201214181824_MSAddUserGroups.cs | 4 ++-- .../Migrations/20201214181914_MYAddUserGroups.cs | 4 ++-- .../Migrations/20201214182008_PGAddUserGroups.cs | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs index b023278303..3f41b592f4 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181824_MSAddUserGroups.cs @@ -278,10 +278,10 @@ namespace Tgstation.Server.Host.Database.Migrations "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); migrationBuilder.DropForeignKey( name: "FK_Users_Groups_GroupId", diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs index dbc3d98ebe..8a71555080 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs @@ -277,10 +277,10 @@ namespace Tgstation.Server.Host.Database.Migrations "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); migrationBuilder.DropForeignKey( name: "FK_Users_Groups_GroupId", diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs index 219912379a..72586ccabd 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214182008_PGAddUserGroups.cs @@ -163,10 +163,10 @@ namespace Tgstation.Server.Host.Database.Migrations onDelete: ReferentialAction.Restrict); migrationBuilder.Sql( - "INSERT INTO PermissionSets (UserId, AdministrationRights, InstanceManagerRights) SELECT Id, AdministrationRights, InstanceManagerRights FROM Users"); + "INSERT INTO \"PermissionSets\" (\"UserId\", \"AdministrationRights\", \"InstanceManagerRights\") SELECT \"Id\", \"AdministrationRights\", \"InstanceManagerRights\" FROM \"Users\""); migrationBuilder.Sql( - "INSERT INTO InstancePermissionSets (PermissionSetId, InstanceId, InstancePermissionSetRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.Id, iu.InstanceId, iu.InstanceUserRights, iu.ByondRights, iu.DreamDaemonRights, iu.DreamMakerRights, iu.RepositoryRights, iu.ChatBotRights, iu.ConfigurationRights FROM InstanceUsers iu JOIN PermissionSets p ON iu.UserId = p.UserId"); + "INSERT INTO \"InstancePermissionSets\" (\"PermissionSetId\", \"InstanceId\", \"InstancePermissionSetRights\", \"ByondRights\", \"DreamDaemonRights\", \"DreamMakerRights\", \"RepositoryRights\", \"ChatBotRights\", \"ConfigurationRights\") SELECT p.\"Id\", iu.\"InstanceId\", iu.\"InstanceUserRights\", iu.\"ByondRights\", iu.\"DreamDaemonRights\", iu.\"DreamMakerRights\", iu.\"RepositoryRights\", iu.\"ChatBotRights\", iu.\"ConfigurationRights\" FROM \"InstanceUsers\" iu JOIN \"PermissionSets\" p ON iu.\"UserId\" = p.\"UserId\""); migrationBuilder.DropTable( name: "InstanceUsers"); @@ -271,16 +271,16 @@ namespace Tgstation.Server.Host.Database.Migrations unique: true); migrationBuilder.Sql( - "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.Id = p.UserId WHERE p.UserId != NULL"); + "UPDATE \"Users\" SET \"AdministrationRights\" = p.\"AdministrationRights\", \"InstanceManagerRights\" = p.\"InstanceManagerRights\" FROM \"Users\" u JOIN \"PermissionSets\" p ON u.\"Id\" = p.\"UserId\" WHERE p.\"UserId\" != NULL"); migrationBuilder.Sql( - "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); + "UPDATE \"Users\" SET \"AdministrationRights\" = p.\"AdministrationRights\", \"InstanceManagerRights\" = p.\"InstanceManagerRights\" FROM \"Users\" u JOIN \"PermissionSets\" p ON u.\"GroupId\" = p.\"GroupId\" WHERE p.\"GroupId\" != NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + "INSERT INTO \"InstanceUsers\" (\"UserId\", \"InstanceId\", \"InstanceUserRights\", \"ByondRights\", \"DreamDaemonRights\", \"DreamMakerRights\", \"RepositoryRights\", \"ChatBotRights\", \"ConfigurationRights\") SELECT p.\"UserId\", ips.\"InstanceId\", ips.\"InstancePermissionSetRights\", ips.\"ByondRights\", ips.\"DreamDaemonRights\", ips.\"DreamMakerRights\", ips.\"RepositoryRights\", ips.\"ChatBotRights\", ips.\"ConfigurationRights\" FROM \"InstancePermissionSets\" ips JOIN \"PermissionSets\" p ON ips.\"PermissionSetId\" = p.\"Id\" WHERE p.\"UserId\" != NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstanceUserRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + "INSERT INTO \"InstanceUsers\" (\"UserId\", \"InstanceId\", \"InstanceUserRights\", \"ByondRights\", \"DreamDaemonRights\", \"DreamMakerRights\", \"RepositoryRights\", \"ChatBotRights\", \"ConfigurationRights\") SELECT u.\"Id\", ips.\"InstanceId\", ips.\"InstancePermissionSetRights\", ips.\"ByondRights\", ips.\"DreamDaemonRights\", ips.\"DreamMakerRights\", ips.\"RepositoryRights\", ips.\"ChatBotRights\", ips.\"ConfigurationRights\" FROM \"InstancePermissionSets\" ips JOIN \"PermissionSets\" p ON ips.\"PermissionSetId\" = p.\"Id\" JOIN \"Users\" u ON p.\"GroupId\" = u.\"GroupId\" WHERE p.\"GroupId\" != NULL"); migrationBuilder.DropForeignKey( name: "FK_Users_Groups_GroupId", From afc4523416b07879a01483c54149a0f5d9ce20f4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 14 Dec 2020 18:08:15 -0500 Subject: [PATCH 054/154] Fix POST /UserGroup returning 201 instead of 200 --- src/Tgstation.Server.Host/Controllers/UserGroupController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index 63ad368d29..ebec01d548 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Controllers /// The requested does not currently exist. [HttpPost] [TgsAuthorize(AdministrationRights.WriteUsers)] - [ProducesResponseType(typeof(UserGroup), 201)] + [ProducesResponseType(typeof(UserGroup), 200)] public async Task Update([FromBody] UserGroup model, CancellationToken cancellationToken) { if (model == null) @@ -119,7 +119,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - return Created(currentGroup.ToApi(true)); + return Json(currentGroup.ToApi(true)); } /// From b7fb9481f702e1ec6c011f5a9cab94ba80989b26 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 14 Dec 2020 18:47:59 -0500 Subject: [PATCH 055/154] Fix where PermissionSet values could be null --- .../Controllers/UserController.cs | 4 ++-- src/Tgstation.Server.Host/Database/DatabaseSeeder.cs | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 6fdf0546e3..81f08a66e4 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -298,8 +298,8 @@ namespace Tgstation.Server.Host.Controllers originalUser.PermissionSet = new Models.PermissionSet(); } - originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights; - originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights; + originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? AdministrationRights.None; + originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? InstanceManagerRights.None; originalUser.Group = null; originalUser.GroupId = null; diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 39a25c3245..9b5816ab16 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -240,10 +240,14 @@ namespace Tgstation.Server.Host.Database { admin.Group = null; admin.GroupId = null; - admin.PermissionSet = new PermissionSet(); + admin.PermissionSet = new PermissionSet + { + InstanceManagerRights = InstanceManagerRights.None, + AdministrationRights = AdministrationRights.WriteUsers + }; } - - admin.PermissionSet.AdministrationRights |= AdministrationRights.WriteUsers; + else + admin.PermissionSet.AdministrationRights |= AdministrationRights.WriteUsers; cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false); } From 23205978f6b577602234de352c4b7c2a026f86dd Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 14 Dec 2020 22:14:08 -0500 Subject: [PATCH 056/154] Fix integration test permission issues --- .../InstanceManagerTest.cs | 3 ++- .../Tgstation.Server.Tests/IntegrationTest.cs | 2 +- tests/Tgstation.Server.Tests/UsersTest.cs | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs index 783d6b45ee..77f309763b 100644 --- a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs @@ -176,7 +176,8 @@ namespace Tgstation.Server.Tests Id = current.Id, PermissionSet = new PermissionSet { - InstanceManagerRights = InstanceManagerRights.SetChatBotLimit + InstanceManagerRights = InstanceManagerRights.SetChatBotLimit, + AdministrationRights = RightsHelper.AllRights() } }; await usersClient.Update(update, cancellationToken); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index abb4e6ba1d..f109070639 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -361,7 +361,7 @@ namespace Tgstation.Server.Tests Assert.IsTrue(Directory.Exists(instance.Path)); var instanceClient = adminClient.Instances.CreateClient(instance); - var usersTest = FailFast(new UsersTest(adminClient, instanceClient).Run(cancellationToken)); + var usersTest = FailFast(new UsersTest(adminClient).Run(cancellationToken)); Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 360f49a1af..1922902dc8 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -15,12 +15,10 @@ namespace Tgstation.Server.Tests sealed class UsersTest { readonly IServerClient serverClient; - readonly IInstanceClient instanceClient; - public UsersTest(IServerClient serverClient, IInstanceClient instanceClient) + public UsersTest(IServerClient serverClient) { this.serverClient = serverClient ?? throw new ArgumentNullException(nameof(serverClient)); - this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); } public async Task Run(CancellationToken cancellationToken) @@ -160,7 +158,8 @@ namespace Tgstation.Server.Tests userUpdate.PermissionSet = null; - await instanceClient.PermissionSets.Create(new InstancePermissionSet + var allInstances = await serverClient.Instances.List(cancellationToken).ConfigureAwait(false); + var instancePermissionSet = new InstancePermissionSet { PermissionSetId = group.PermissionSet.Id.Value, ByondRights = RightsHelper.AllRights(), @@ -170,7 +169,16 @@ namespace Tgstation.Server.Tests DreamMakerRights = RightsHelper.AllRights(), InstancePermissionSetRights = RightsHelper.AllRights(), RepositoryRights = RightsHelper.AllRights(), - }, cancellationToken); + }; + await Task.WhenAll( + allInstances + .Where(x => x.Online.Value) + .Select( + instance => serverClient + .Instances + .CreateClient(instance) + .PermissionSets + .Create(instancePermissionSet, cancellationToken))); user = await serverClient.Users.Update(userUpdate, cancellationToken); From b94eec081bcc87824a2fd95ea03a2bb21471e4f3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 14 Dec 2020 23:34:53 -0500 Subject: [PATCH 057/154] Fix MySQL custom JOINs --- .../Database/Migrations/20201214181914_MYAddUserGroups.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs index 8a71555080..d6957a88e5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201214181914_MYAddUserGroups.cs @@ -271,16 +271,16 @@ namespace Tgstation.Server.Host.Database.Migrations unique: true); migrationBuilder.Sql( - "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.Id = p.UserId WHERE p.UserId != NULL"); + "UPDATE Users AS u JOIN PermissionSets p ON u.Id = p.UserId SET u.AdministrationRights = p.AdministrationRights, u.InstanceManagerRights = p.InstanceManagerRights WHERE p.UserId IS NOT NULL"); migrationBuilder.Sql( - "UPDATE Users SET AdministrationRights = p.AdministrationRights, InstanceManagerRights = p.InstanceManagerRights FROM Users u JOIN PermissionSets p ON u.GroupId = p.GroupId WHERE p.GroupId != NULL"); + "UPDATE Users AS u JOIN PermissionSets p ON u.GroupId = p.GroupId SET u.AdministrationRights = p.AdministrationRights, u.InstanceManagerRights = p.InstanceManagerRights WHERE p.GroupId IS NOT NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId != NULL"); + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT p.UserId, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id WHERE p.UserId IS NOT NULL"); migrationBuilder.Sql( - "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId != NULL"); + "INSERT INTO InstanceUsers (UserId, InstanceId, InstanceUserRights, ByondRights, DreamDaemonRights, DreamMakerRights, RepositoryRights, ChatBotRights, ConfigurationRights) SELECT u.Id, ips.InstanceId, ips.InstancePermissionSetRights, ips.ByondRights, ips.DreamDaemonRights, ips.DreamMakerRights, ips.RepositoryRights, ips.ChatBotRights, ips.ConfigurationRights FROM InstancePermissionSets ips JOIN PermissionSets p ON ips.PermissionSetId = p.Id JOIN Users u ON p.GroupId = u.GroupId WHERE p.GroupId IS NOT NULL"); migrationBuilder.DropForeignKey( name: "FK_Users_Groups_GroupId", From faf81f906ddf99dd0c480bed72770ab775825276 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 15 Dec 2020 13:18:51 -0500 Subject: [PATCH 058/154] Calls into IChatManager now queue messages - Better timeout handling and hopefully kills the last spurious test failure --- .../Components/Chat/ChatManager.cs | 82 ++++++++++++++--- .../Components/Chat/IChatManager.cs | 28 +++--- .../Components/Deployment/DreamMaker.cs | 33 ++++--- .../Components/Session/SessionController.cs | 91 ++++++++++--------- .../Components/Watchdog/BasicWatchdog.cs | 8 +- .../Components/Watchdog/WatchdogBase.cs | 49 +++++----- tools/ReleaseNotes/Program.cs | 1 - 7 files changed, 174 insertions(+), 118 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 25807b4d2f..93e8971229 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -111,6 +111,11 @@ namespace Tgstation.Server.Host.Components.Chat /// Task initialProviderConnectionsTask; + /// + /// A that represents all sent messages. + /// + Task messageSendTask; + /// /// The that completes when s change /// @@ -159,6 +164,8 @@ namespace Tgstation.Server.Host.Components.Chat trackingContexts = new List(); handlerCts = new CancellationTokenSource(); connectionsUpdated = new TaskCompletionSource(); + + messageSendTask = Task.CompletedTask; channelIdCounter = 1; } @@ -170,6 +177,8 @@ namespace Tgstation.Server.Host.Components.Chat handlerCts.Dispose(); foreach (var I in providers) await I.Value.DisposeAsync().ConfigureAwait(false); + + await messageSendTask.ConfigureAwait(false); } /// @@ -614,16 +623,29 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public async Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken) + public void QueueMessage(string message, IEnumerable channelIds) { if (message == null) throw new ArgumentNullException(nameof(message)); if (channelIds == null) throw new ArgumentNullException(nameof(channelIds)); + var task = SendMessage(message, channelIds, handlerCts.Token); + AddMessageTask(task); + } + + /// + /// Asynchronously send a given to a set of . + /// + /// The message to send. + /// The s of the s to send to. + /// The for the operation. + /// A representing the running operation. + Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken) + { logger.LogTrace("Chat send \"{0}\" to channels: {1}", message, String.Join(", ", channelIds)); - await Task.WhenAll( + return Task.WhenAll( channelIds.Select(x => { ChannelMapping channelMapping; @@ -635,12 +657,11 @@ namespace Tgstation.Server.Host.Components.Chat if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) return Task.CompletedTask; return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken); - })) - .ConfigureAwait(false); + })); } /// - public async Task SendWatchdogMessage(string message, CancellationToken cancellationToken) + public async Task QueueWatchdogMessage(string message, CancellationToken cancellationToken) { List wdChannels = null; message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message); @@ -654,18 +675,17 @@ namespace Tgstation.Server.Host.Components.Chat lock (mappedChannels) wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList(); - await SendMessage(message, wdChannels, cancellationToken).ConfigureAwait(false); + QueueMessage(message, wdChannels); } /// - public async Task> SendDeploymentMessage( + public Action QueueDeploymentMessage( Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, - bool localCommitPushed, - CancellationToken cancellationToken) + bool localCommitPushed) { List wdChannels; lock (mappedChannels) // so it doesn't change while we're using it @@ -675,7 +695,7 @@ namespace Tgstation.Server.Host.Components.Chat var callbacks = new List>(); - await Task.WhenAll( + var task = Task.WhenAll( wdChannels.Select( async x => { @@ -697,7 +717,7 @@ namespace Tgstation.Server.Host.Components.Chat gitHubRepo, channelMapping.ProviderChannelId, localCommitPushed, - cancellationToken) + handlerCts.Token) .ConfigureAwait(false); callbacks.Add(callback); @@ -709,10 +729,16 @@ namespace Tgstation.Server.Host.Components.Chat "Error sending deploy message to provider {0}!", channelMapping.ProviderId); } - })) - .ConfigureAwait(false); + })); - return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput))); + AddMessageTask(task); + + return (errorMessage, dreamMakerOutput) => AddMessageTask( + Task.WhenAll( + callbacks.Select( + x => x( + errorMessage, + dreamMakerOutput)))); } /// @@ -744,6 +770,7 @@ namespace Tgstation.Server.Host.Components.Chat if (chatHandler != null) await chatHandler.ConfigureAwait(false); await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false); + await messageSendTask.ConfigureAwait(false); } /// @@ -800,7 +827,32 @@ namespace Tgstation.Server.Host.Components.Chat List wdChannels; lock (mappedChannels) // so it doesn't change while we're using it wdChannels = mappedChannels.Select(x => x.Key).ToList(); - return SendMessage(message, wdChannels, cancellationToken); + + QueueMessage(message, wdChannels); + return Task.CompletedTask; + } + + /// + /// Adds a given to . + /// + /// The to add. + void AddMessageTask(Task task) + { + async Task Wrap(Task originalTask) + { + await originalTask.ConfigureAwait(false); + try + { + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error in asynchronous chat message!"); + } + } + + lock (handlerCts) + messageSendTask = Wrap(messageSendTask); } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 3cfe424951..367274d8b2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -44,21 +44,19 @@ namespace Tgstation.Server.Host.Components.Chat Task ChangeChannels(long connectionId, IEnumerable newChannels, CancellationToken cancellationToken); /// - /// Send a chat to a given set of + /// Queue a chat to a given set of . /// - /// The message being sent - /// The s of the s to send to - /// The for the operation - /// A representing the running operation - Task SendMessage(string message, IEnumerable channelIds, CancellationToken cancellationToken); + /// The message being sent. + /// The s of the s to send to. + void QueueMessage(string message, IEnumerable channelIds); /// - /// Send a chat to configured watchdog channels + /// Queue a chat to configured watchdog channels. /// - /// The message being sent - /// The for the operation - /// A representing the running operation - Task SendWatchdogMessage(string message, CancellationToken cancellationToken); + /// The message being sent. + /// The for the operation. + /// A representing the running operation. + Task QueueWatchdogMessage(string message, CancellationToken cancellationToken); /// /// Send the message for a deployment to configured deployment channels. @@ -69,16 +67,14 @@ namespace Tgstation.Server.Host.Components.Chat /// The repository GitHub owner, if any. /// The repository GitHub name, if any. /// if the local deployment commit was pushed to the remote repository. - /// The for the operation. - /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. - Task> SendDeploymentMessage( + /// An to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. + Action QueueDeploymentMessage( Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, - bool localCommitPushed, - CancellationToken cancellationToken); + bool localCommitPushed); /// /// Start tracking s and s. diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index a7fb801b9e..c031f55a4d 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -101,8 +101,14 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly object deploymentLock; - Func currentChatCallback; + /// + /// The active callback from . + /// + Action currentChatCallback; + /// + /// Cached for . + /// string currentDreamMakerOutput; /// @@ -716,25 +722,26 @@ namespace Tgstation.Server.Host.Components.Deployment var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken); - var chatTask = currentChatCallback(null, compileJob.Output); - currentChatCallback = null; - try { - await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false); + currentChatCallback(null, compileJob.Output); + + await Task.WhenAll(commentsTask, eventTask).ConfigureAwait(false); } catch (Exception ex) { throw new JobException(ErrorCode.PostDeployFailure, ex); } + finally + { + currentChatCallback = null; + } } catch (Exception ex) { - if (currentChatCallback != null) - await currentChatCallback( - FormatExceptionForUsers(ex), - currentDreamMakerOutput) - .ConfigureAwait(false); + currentChatCallback?.Invoke( + FormatExceptionForUsers(ex), + currentDreamMakerOutput); throw; } @@ -797,15 +804,13 @@ namespace Tgstation.Server.Host.Components.Deployment try { using var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false); - currentChatCallback = await chatManager.SendDeploymentMessage( + currentChatCallback = chatManager.QueueDeploymentMessage( revisionInformation, byondLock.Version, DateTimeOffset.Now + estimatedDuration, repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, - localCommitExistsOnRemote, - cancellationToken) - .ConfigureAwait(false); + localCommitExistsOnRemote); var job = new Models.CompileJob { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 79b17c930b..1e068f0188 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -343,7 +343,7 @@ namespace Tgstation.Server.Host.Components.Session } /// - public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); @@ -358,34 +358,36 @@ namespace Tgstation.Server.Host.Components.Session { case BridgeCommandType.ChatSend: if (parameters.ChatMessage == null) - return new BridgeResponse - { - ErrorMessage = "Missing chatMessage field!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Missing chatMessage field!" + }); if (parameters.ChatMessage.ChannelIds == null) - return new BridgeResponse - { - ErrorMessage = "Missing channelIds field in chatMessage!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Missing channelIds field in chatMessage!" + }); if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _))) - return new BridgeResponse - { - ErrorMessage = "Invalid channelIds in chatMessage!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Invalid channelIds in chatMessage!" + }); if (parameters.ChatMessage.Text == null) - return new BridgeResponse - { - ErrorMessage = "Missing message field in chatMessage!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Missing message field in chatMessage!" + }); - await chat.SendMessage( + chat.QueueMessage( parameters.ChatMessage.Text, - parameters.ChatMessage.ChannelIds.Select(UInt64.Parse), - cancellationToken) - .ConfigureAwait(false); + parameters.ChatMessage.ChannelIds.Select(UInt64.Parse)); break; case BridgeCommandType.Prime: var oldPrimeTcs = primeTcs; @@ -404,10 +406,11 @@ namespace Tgstation.Server.Host.Components.Session { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); - return new BridgeResponse - { - ErrorMessage = "Missing stringified port as data parameter!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Missing stringified port as data parameter!" + }); } var currentPort = parameters.CurrentPort.Value; @@ -434,19 +437,21 @@ namespace Tgstation.Server.Host.Components.Session case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; if (parameters.Version == null) - return new BridgeResponse - { - ErrorMessage = "Missing dmApiVersion field!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Missing dmApiVersion field!" + }); DMApiVersion = parameters.Version; if (DMApiVersion.Major != DMApiConstants.Version.Major) { apiValidationStatus = ApiValidationStatus.Incompatible; - return new BridgeResponse - { - ErrorMessage = "Incompatible dmApiVersion!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Incompatible dmApiVersion!" + }); } switch (parameters.MinimumSecurityLevel) @@ -461,15 +466,17 @@ namespace Tgstation.Server.Host.Components.Session apiValidationStatus = ApiValidationStatus.RequiresTrusted; break; case null: - return new BridgeResponse - { - ErrorMessage = "Missing minimumSecurityLevel field!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Missing minimumSecurityLevel field!" + }); default: - return new BridgeResponse - { - ErrorMessage = "Invalid minimumSecurityLevel!" - }; + return Task.FromResult( + new BridgeResponse + { + ErrorMessage = "Invalid minimumSecurityLevel!" + }); } response.RuntimeInformation = new RuntimeInformation( @@ -504,7 +511,7 @@ namespace Tgstation.Server.Host.Components.Session break; } - return response; + return Task.FromResult(response); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 48098d98de..c364a8cf65 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (Server.RebootState == Session.RebootState.Shutdown) { // the time for graceful shutdown is now - await Chat.SendWatchdogMessage( + await Chat.QueueWatchdogMessage( String.Format( CultureInfo.InvariantCulture, "Server {0}! Shutting down due to graceful termination request...", @@ -105,7 +105,7 @@ namespace Tgstation.Server.Host.Components.Watchdog return MonitorAction.Exit; } - await Chat.SendWatchdogMessage( + await Chat.QueueWatchdogMessage( String.Format( CultureInfo.InvariantCulture, "Server {0}! Rebooting...", @@ -132,7 +132,7 @@ namespace Tgstation.Server.Host.Components.Watchdog return MonitorAction.Restart; case Session.RebootState.Shutdown: // graceful shutdown time - await Chat.SendWatchdogMessage( + await Chat.QueueWatchdogMessage( "Active server rebooted! Shutting down due to graceful termination request...", cancellationToken) .ConfigureAwait(false); @@ -265,7 +265,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { gracefulRebootRequired = true; if (Server.CompileJob.DMApiVersion == null) - return Chat.SendWatchdogMessage( + return Chat.QueueWatchdogMessage( "A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update.", cancellationToken); return Server.SetRebootState(Session.RebootState.Restart, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 8275f6df83..cf873601f4 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -269,7 +269,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { var eventTask = eventConsumer.HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken); - var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", cancellationToken) : Task.CompletedTask; + var chatTask = announce ? Chat.QueueWatchdogMessage("Shutting down...", cancellationToken) : Task.CompletedTask; await eventTask.ConfigureAwait(false); @@ -314,7 +314,7 @@ namespace Tgstation.Server.Host.Components.Watchdog case 2: var message2 = "DEFCON 3: DreamDaemon has missed 2 heartbeats!"; Logger.LogInformation(message2); - await Chat.SendWatchdogMessage(message2, cancellationToken).ConfigureAwait(false); + await Chat.QueueWatchdogMessage(message2, cancellationToken).ConfigureAwait(false); break; case 3: var actionToTake = shouldShutdown @@ -322,7 +322,7 @@ namespace Tgstation.Server.Host.Components.Watchdog : "be restarted"; var message3 = $"DEFCON 2: DreamDaemon has missed 3 heartbeats! If it does not respond to the next one, the watchdog will {actionToTake}!"; Logger.LogWarning(message3); - await Chat.SendWatchdogMessage(message3, cancellationToken).ConfigureAwait(false); + await Chat.QueueWatchdogMessage(message3, cancellationToken).ConfigureAwait(false); break; case 4: var actionTaken = shouldShutdown @@ -330,7 +330,7 @@ namespace Tgstation.Server.Host.Components.Watchdog : "Restarting"; var message4 = $"DEFCON 1: Four heartbeats have been missed! {actionTaken}..."; Logger.LogWarning(message4); - await Chat.SendWatchdogMessage(message4, cancellationToken).ConfigureAwait(false); + await Chat.QueueWatchdogMessage(message4, cancellationToken).ConfigureAwait(false); await DisposeAndNullControllers(cancellationToken).ConfigureAwait(false); return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart; default: @@ -371,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Task announceTask; if (announce) { - announceTask = Chat.SendWatchdogMessage( + announceTask = Chat.QueueWatchdogMessage( reattachInfo == null ? "Launching..." : "Reattaching...", @@ -405,7 +405,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { await originalChatTask.ConfigureAwait(false); if (announceFailure) - await Chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false); + await Chat.QueueWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false); } announceTask = ChainChatTaskWithErrorMessage(); @@ -487,7 +487,7 @@ namespace Tgstation.Server.Host.Components.Watchdog const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog..."; Logger.LogWarning(FailReattachMessage); - var chatTask = Chat.SendWatchdogMessage(FailReattachMessage, cancellationToken); + var chatTask = Chat.QueueWatchdogMessage(FailReattachMessage, cancellationToken); await InitControllers(chatTask, null, cancellationToken).ConfigureAwait(false); } @@ -591,7 +591,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Math.Pow(2, retryAttempts)), TimeSpan.FromHours(1).TotalSeconds); // max of one hour, increasing by a power of 2 each time - chatTask = Chat.SendWatchdogMessage( + chatTask = Chat.QueueWatchdogMessage( $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}s...", cancellationToken); @@ -733,7 +733,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var nextActionMessage = nextAction != MonitorAction.Exit ? "Recovering" : "Shutting down"; - var chatTask = Chat.SendWatchdogMessage( + var chatTask = Chat.QueueWatchdogMessage( $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}", cancellationToken); @@ -811,22 +811,19 @@ namespace Tgstation.Server.Host.Components.Watchdog .ConfigureAwait(false); if (result?.InteropResponse?.ChatResponses != null) - await Task.WhenAll( - result.InteropResponse.ChatResponses.Select( - x => Chat.SendMessage( - x.Text, - x.ChannelIds - .Select(channelIdString => - { - if (UInt64.TryParse(channelIdString, out var channelId)) - return (ulong?)channelId; + foreach (var response in result.InteropResponse.ChatResponses) + Chat.QueueMessage( + response.Text, + response.ChannelIds + .Select(channelIdString => + { + if (UInt64.TryParse(channelIdString, out var channelId)) + return (ulong?)channelId; - return null; - }) - .Where(nullableChannelId => nullableChannelId.HasValue) - .Select(nullableChannelId => nullableChannelId.Value), - cancellationToken))) - .ConfigureAwait(false); + return null; + }) + .Where(nullableChannelId => nullableChannelId.HasValue) + .Select(nullableChannelId => nullableChannelId.Value)); } /// @@ -888,7 +885,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { if (!graceful) { - var chatTask = Chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken); + var chatTask = Chat.QueueWatchdogMessage("Manual restart triggered...", cancellationToken); await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false); await LaunchNoLock(true, false, true, null, cancellationToken).ConfigureAwait(false); await chatTask.ConfigureAwait(false); @@ -966,7 +963,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { releaseServers = true; if (Status == WatchdogStatus.Online) - await Chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false); + await Chat.QueueWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false); else Logger.LogTrace("Not sending detach chat message as status is: {0}", Status); } diff --git a/tools/ReleaseNotes/Program.cs b/tools/ReleaseNotes/Program.cs index 28cafa17a7..65760f4b8d 100644 --- a/tools/ReleaseNotes/Program.cs +++ b/tools/ReleaseNotes/Program.cs @@ -60,7 +60,6 @@ namespace ReleaseNotes { Milestone = $"v{versionString}", Type = IssueTypeQualifier.PullRequest, - State = ItemState.Closed, Repos = { { RepoOwner, RepoName } } }).ConfigureAwait(false); From 29e1d21659720eb88eaf734132977f38596211df Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 15 Dec 2020 15:15:18 -0500 Subject: [PATCH 059/154] Fix API validation race condition --- .../Components/Session/SessionController.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 1e068f0188..340cb0f783 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -212,6 +212,17 @@ namespace Tgstation.Server.Host.Components.Session this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + portClosedForReboot = false; + disposed = false; + apiValidationStatus = ApiValidationStatus.NeverValidated; + released = false; + + rebootTcs = new TaskCompletionSource(); + primeTcs = new TaskCompletionSource(); + initialBridgeRequestTcs = new TaskCompletionSource(); + reattachTopicCts = new CancellationTokenSource(); + synchronizationLock = new object(); + if (apiValidate || DMApiAvailable) { bridgeRegistration = bridgeRegistrar.RegisterHandler(this); @@ -224,17 +235,6 @@ namespace Tgstation.Server.Host.Components.Session ? "no" : $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})"); - portClosedForReboot = false; - disposed = false; - apiValidationStatus = ApiValidationStatus.NeverValidated; - released = false; - - rebootTcs = new TaskCompletionSource(); - primeTcs = new TaskCompletionSource(); - initialBridgeRequestTcs = new TaskCompletionSource(); - reattachTopicCts = new CancellationTokenSource(); - synchronizationLock = new object(); - async Task WrapLifetime() { var exitCode = await process.Lifetime.ConfigureAwait(false); From f5013fff8cb2681ac1f29b384e9af55e94bb96c1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 15 Dec 2020 15:40:31 -0500 Subject: [PATCH 060/154] Log ApiValidationStatus after setting it --- .../Components/Session/SessionController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 340cb0f783..b79046ab32 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -479,6 +479,8 @@ namespace Tgstation.Server.Host.Components.Session }); } + logger.LogTrace("ApiValidationStatus set to {0}", apiValidationStatus); + response.RuntimeInformation = new RuntimeInformation( chatTrackingContext, reattachInformation.Dmb, From bcff5440c4224b3c70b32e00c4a97dc365ccad5b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 15 Dec 2020 18:05:10 -0500 Subject: [PATCH 061/154] Add additional logging to LaunchResult processing --- .../Components/Session/SessionController.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index b79046ab32..ac8eaa9b9e 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -301,7 +301,8 @@ namespace Tgstation.Server.Host.Components.Session bool apiValidate) { var startTime = DateTimeOffset.Now; - var startupTask = !reattached && (apiValidate || DMApiAvailable) + var useBridgeRequestForLaunchResult = !reattached && (apiValidate || DMApiAvailable); + var startupTask = useBridgeRequestForLaunchResult ? initialBridgeRequestTcs.Task : process.Startup; var toAwait = Task.WhenAny(startupTask, process.Lifetime); @@ -309,6 +310,11 @@ namespace Tgstation.Server.Host.Components.Session if (startupTimeout.HasValue) toAwait = Task.WhenAny(toAwait, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime)); + logger.LogTrace( + "Waiting for LaunchResult based on {0}{1}...", + useBridgeRequestForLaunchResult ? "initial bridge request" : "process startup", + startupTimeout.HasValue ? $" with a timeout of {startupTimeout.Value}s" : String.Empty); + await toAwait.ConfigureAwait(false); var result = new LaunchResult From 7daeaed13c34888e6b447717dd94090d5437a53c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 11:12:07 -0500 Subject: [PATCH 062/154] Better make sure users with no password can be created --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 ++++++ src/Tgstation.Server.Host/Controllers/UserController.cs | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index abe3def22d..0b3906074f 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -618,5 +618,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The " + Routes.UserGroup + " endpoint cannot edit group members. Please update each member user individually.")] UserGroupControllerCantEditMembers, + + /// + /// Tried to remove the last for a passwordless . + /// + [Description("This user is passwordless and removing their oAuthConnections would leave them with no authentication method!")] + CannotRemoveLastAuthenticationOption, } } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 81f08a66e4..579106c9c3 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -121,7 +121,8 @@ namespace Tgstation.Server.Host.Controllers if (model.OAuthConnections?.Any(x => x == null) == true) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); - if (!(model.Password == null ^ model.SystemIdentifier == null)) + if ((model.Password != null && model.SystemIdentifier != null) + || (model.Password == null && model.SystemIdentifier == null && model.OAuthConnections?.Any() != true)) return BadRequest(new ErrorMessage(ErrorCode.UserMismatchPasswordSid)); if (model.Group != null && model.PermissionSet != null) @@ -155,7 +156,7 @@ namespace Tgstation.Server.Host.Controllers { return RequiresPosixSystemIdentity(); } - else if (!(model.Password?.Length == 0 && model.OAuthConnections.Count != 0)) + else if (!(model.Password?.Length == 0 && model.OAuthConnections?.Any() == true)) { var result = TrySetPassword(dbUser, model.Password, true); if (result != null) @@ -261,6 +262,9 @@ namespace Tgstation.Server.Host.Controllers if (originalUser.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName)) return BadRequest(new ErrorMessage(ErrorCode.AdminUserCannotOAuth)); + if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null) + return BadRequest(new ErrorMessage(ErrorCode.CannotRemoveLastAuthenticationOption)); + originalUser.OAuthConnections.Clear(); foreach (var updatedConnection in model.OAuthConnections) originalUser.OAuthConnections.Add(new Models.OAuthConnection From 6ead63922a9a675d9fcb5d6aa9582bebb4a44370 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 11:12:25 -0500 Subject: [PATCH 063/154] Documentation comment update --- .../Components/Session/LaunchResult.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs b/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs index 10132f32ce..c55880b09a 100644 --- a/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs +++ b/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; namespace Tgstation.Server.Host.Components.Session @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Components.Session public sealed class LaunchResult { /// - /// The time it took for to return. If the startup timed out + /// The time it took for to return or the initial bridge request to process. If the startup timed out /// public TimeSpan? StartupTime { get; set; } @@ -21,4 +21,4 @@ namespace Tgstation.Server.Host.Components.Session /// public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Exit Code: {0}, Time {1}ms", ExitCode, StartupTime?.TotalMilliseconds); } -} \ No newline at end of file +} From 8c3a1702203a9214d85574280e5c96ab0c961882 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 11:12:49 -0500 Subject: [PATCH 064/154] Up repository clone timeout in integration test --- tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index 8f32300ef7..36f2da7900 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -64,7 +64,7 @@ namespace Tgstation.Server.Tests.Instance clone = await repositoryClient.Clone(initalRepo, cancellationToken).ConfigureAwait(false); - await WaitForJob(clone.ActiveJob, 600, false, null, cancellationToken).ConfigureAwait(false); + await WaitForJob(clone.ActiveJob, 900, false, null, cancellationToken).ConfigureAwait(false); var readAfterClone = await repositoryClient.Read(cancellationToken); Assert.AreEqual(initalRepo.Origin, readAfterClone.Origin); From 19cd1aac1969c3cc9447d49bde4b38803e45fa68 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 11:14:05 -0500 Subject: [PATCH 065/154] Stop using a groups user in integration test We know it works. If we really need to test it we should make a dedicated paralled instance test. Currently it just causes permissions issues. --- .../Tgstation.Server.Tests/IntegrationTest.cs | 3 +- tests/Tgstation.Server.Tests/UsersTest.cs | 62 +++++++++---------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index f109070639..d5aca8d5a9 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -357,12 +357,11 @@ namespace Tgstation.Server.Tests var rootTest = FailFast(new RootTest().Run(clientFactory, adminClient, cancellationToken)); var adminTest = FailFast(new AdministrationTest(adminClient.Administration).Run(cancellationToken)); + var usersTest = FailFast(new UsersTest(adminClient).Run(cancellationToken)); instance = await new InstanceManagerTest(adminClient.Instances, adminClient.Users, server.Directory).RunPreInstanceTest(cancellationToken); Assert.IsTrue(Directory.Exists(instance.Path)); var instanceClient = adminClient.Instances.CreateClient(instance); - var usersTest = FailFast(new UsersTest(adminClient).Run(cancellationToken)); - Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances).RunTests(cancellationToken)); diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 1922902dc8..6710aaebc3 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; -using Tgstation.Server.Client.Components; using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests @@ -141,10 +140,29 @@ namespace Tgstation.Server.Tests await ApiAssert.ThrowsException(() => serverClient.Groups.Update(group, cancellationToken), ErrorCode.UserGroupControllerCantEditMembers); - var userUpdate = new UserUpdate + var testUserUpdate = new UserUpdate { - Id = user.Id, - PermissionSet = user.PermissionSet, + Name = "TestUserWithNoPassword", + Password = String.Empty + }; + + await ApiAssert.ThrowsException(() => serverClient.Users.Create(testUserUpdate, cancellationToken), ErrorCode.UserPasswordLength); + + testUserUpdate.OAuthConnections = new List + { + new OAuthConnection + { + ExternalUserId = "asdf", + Provider = OAuthProvider.GitHub + } + }; + + var testUser2 = await serverClient.Users.Create(testUserUpdate, cancellationToken); + + testUserUpdate = new UserUpdate + { + Id = testUser2.Id, + PermissionSet = testUser2.PermissionSet, Group = new Api.Models.Internal.UserGroup { Id = group.Id @@ -152,44 +170,22 @@ namespace Tgstation.Server.Tests }; await ApiAssert.ThrowsException( () => serverClient.Users.Update( - userUpdate, + testUserUpdate, cancellationToken), ErrorCode.UserGroupAndPermissionSet); - userUpdate.PermissionSet = null; + testUserUpdate.PermissionSet = null; - var allInstances = await serverClient.Instances.List(cancellationToken).ConfigureAwait(false); - var instancePermissionSet = new InstancePermissionSet - { - PermissionSetId = group.PermissionSet.Id.Value, - ByondRights = RightsHelper.AllRights(), - ChatBotRights = RightsHelper.AllRights(), - ConfigurationRights = RightsHelper.AllRights(), - DreamDaemonRights = RightsHelper.AllRights(), - DreamMakerRights = RightsHelper.AllRights(), - InstancePermissionSetRights = RightsHelper.AllRights(), - RepositoryRights = RightsHelper.AllRights(), - }; - await Task.WhenAll( - allInstances - .Where(x => x.Online.Value) - .Select( - instance => serverClient - .Instances - .CreateClient(instance) - .PermissionSets - .Create(instancePermissionSet, cancellationToken))); + testUser2 = await serverClient.Users.Update(testUserUpdate, cancellationToken); - user = await serverClient.Users.Update(userUpdate, cancellationToken); - - Assert.IsNull(user.PermissionSet); - Assert.IsNotNull(user.Group); - Assert.AreEqual(group.Id, user.Group.Id); + Assert.IsNull(testUser2.PermissionSet); + Assert.IsNotNull(testUser2.Group); + Assert.AreEqual(group.Id, testUser2.Group.Id); group = await serverClient.Groups.GetId(group, cancellationToken); Assert.IsNotNull(group.Users); Assert.AreEqual(1, group.Users.Count); - Assert.AreEqual(user.Id, group.Users.First().Id); + Assert.AreEqual(testUser2.Id, group.Users.First().Id); Assert.IsNotNull(group.PermissionSet); } From 47fe8306bf27e91848d7b19dac810569c2814b13 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 11:54:38 -0500 Subject: [PATCH 066/154] Small amount of test logging --- tests/Tgstation.Server.Tests/IntegrationTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index d5aca8d5a9..47378cc93e 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -123,10 +123,11 @@ namespace Tgstation.Server.Tests async Task CreateAdminClient(Uri url, CancellationToken cancellationToken) { var giveUpAt = DateTimeOffset.Now.AddSeconds(60); - do + for(var I = 1; ; ++I) { try { + Console.WriteLine($"TEST: CreateAdminClient attempt {I}..."); return await clientFactory.CreateFromLogin( url, User.AdminName, @@ -149,7 +150,7 @@ namespace Tgstation.Server.Tests throw; await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } - } while (true); + } } #if DEBUG From 22809822e54914b9f5b2a992483b2f950b377643 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 17:45:54 -0500 Subject: [PATCH 067/154] Pin CI to .NET 3.1.X See https://github.com/actions/virtual-environments/issues/1891 --- .github/workflows/ci-suite.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 6d20bbfda5..90c3b85a8d 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -11,6 +11,7 @@ on: - master env: + TGS4_DOTNET_VERSION: 3.1.x TGS4_TEST_DISCORD_CHANNEL: ${{ secrets.DISCORD_CHANNEL_ID }} TGS4_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} TGS4_TEST_IRC_CHANNEL: ${{ secrets.IRC_CHANNEL }} @@ -130,10 +131,10 @@ jobs: with: node-version: 12.x - - name: Setup dotnet 3.1.X + - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: ${{ env.TGS4_DOTNET_VERSION }} - name: Checkout uses: actions/checkout@v1 @@ -157,6 +158,11 @@ jobs: configuration: [ 'Debug', 'Release' ] runs-on: windows-latest steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.TGS4_DOTNET_VERSION }} + - name: Checkout uses: actions/checkout@v1 @@ -184,6 +190,11 @@ jobs: configuration: [ 'Debug', 'Release' ] runs-on: windows-latest steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.TGS4_DOTNET_VERSION }} + - name: Set General__UseBasicWatchdog if: ${{ matrix.watchdog-type == 'Basic' }} run: echo "General__UseBasicWatchdog=true" >> $env:GITHUB_ENV @@ -308,7 +319,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ matrix.dotnet }} + dotnet-version: ${{ env.TGS4_DOTNET_VERSION }} - name: Set Sqlite Connection Info if: ${{ matrix.database-type == 'Sqlite' }} From 01d4a05c0d4ad79354629e4a3671b85c453d6b90 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 18:46:40 -0500 Subject: [PATCH 068/154] Mass Package Upgrade - Bump all packages as high as possible - ASP .NET Core packages to 3.1.10 - Add a warning about the annoying LoggerFactory issue in host watchdog project - Add missing exception constructors to client - Switch to using the correct .NET analyzers --- .../Tgstation.Server.Api.csproj | 13 ++++----- src/Tgstation.Server.Client/ApiClient.cs | 27 +++++++++++++------ src/Tgstation.Server.Client/ApiException.cs | 4 +-- .../InsufficientPermissionsException.cs | 9 +++++-- .../MethodNotSupportedException.cs | 9 +++++-- .../RateLimitException.cs | 13 +++++++-- .../RequestTimeoutException.cs | 9 +++++-- .../ServerClientFactory.cs | 4 +-- .../ServerErrorException.cs | 10 +++++-- .../ServiceUnavailableException.cs | 9 +++++-- .../Tgstation.Server.Client.csproj | 9 ++++--- .../UnauthorizedException.cs | 9 +++++-- src/Tgstation.Server.Host.Console/Program.cs | 2 +- .../Tgstation.Server.Host.Console.csproj | 9 ++++--- .../Tgstation.Server.Host.Service.csproj | 11 +++----- .../Tgstation.Server.Host.Watchdog.csproj | 12 ++++----- .../Tgstation.Server.Host.csproj | 17 ++++-------- .../Tgstation.Server.Api.Tests.csproj | 2 +- .../Tgstation.Server.Client.Tests.csproj | 4 +-- ...Tgstation.Server.Host.Console.Tests.csproj | 6 ++--- ...Tgstation.Server.Host.Service.Tests.csproj | 2 +- .../Tgstation.Server.Host.Tests.csproj | 4 +-- ...gstation.Server.Host.Watchdog.Tests.csproj | 4 +-- .../Tgstation.Server.Tests.csproj | 4 +-- 24 files changed, 120 insertions(+), 82 deletions(-) diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index f971e5b548..933cb4cb6c 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -1,4 +1,4 @@ - + @@ -23,6 +23,7 @@ latest enable bin\$(Configuration)\netstandard2.1\Tgstation.Server.Api.xml + true CA1028 @@ -33,18 +34,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - - + - + diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 84e339954f..7c8460b27e 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -363,14 +363,25 @@ namespace Tgstation.Server.Client memoryStream = new MemoryStream(); using (memoryStream) - await RunRequest( - $"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}", - new StreamContent(uploadStream ?? memoryStream), - HttpMethod.Put, - null, - false, - cancellationToken) - .ConfigureAwait(false); + { + var streamContent = new StreamContent(uploadStream ?? memoryStream); + try + { + await RunRequest( + $"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}", + streamContent, + HttpMethod.Put, + null, + false, + cancellationToken) + .ConfigureAwait(false); + streamContent = null; //CA2000 + } + finally + { + streamContent?.Dispose(); + } + } } } } diff --git a/src/Tgstation.Server.Client/ApiException.cs b/src/Tgstation.Server.Client/ApiException.cs index 00b103b80b..5dc4e1ab6b 100644 --- a/src/Tgstation.Server.Client/ApiException.cs +++ b/src/Tgstation.Server.Client/ApiException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using Tgstation.Server.Api.Models; @@ -31,7 +31,7 @@ namespace Tgstation.Server.Client /// The . protected ApiException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base( responseMessage, - errorMessage?.Message ?? $"HTTP {responseMessage.StatusCode}. Unknown API error, ErrorMessage payload not present!") + errorMessage?.Message ?? $"HTTP {responseMessage?.StatusCode ?? throw new ArgumentNullException(nameof(responseMessage))}. Unknown API error, ErrorMessage payload not present!") { ServerApiVersion = errorMessage?.ServerApiVersion; AdditionalServerData = errorMessage?.AdditionalData; diff --git a/src/Tgstation.Server.Client/InsufficientPermissionsException.cs b/src/Tgstation.Server.Client/InsufficientPermissionsException.cs index da1b511c8e..cb0b11f410 100644 --- a/src/Tgstation.Server.Client/InsufficientPermissionsException.cs +++ b/src/Tgstation.Server.Client/InsufficientPermissionsException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; namespace Tgstation.Server.Client @@ -17,6 +17,11 @@ namespace Tgstation.Server.Client "The current user has insufficient permissions to perform the requested operation!") { } + /// + /// Intializes a new instance of the . + /// + public InsufficientPermissionsException() { } + /// /// Construct an with a /// @@ -30,4 +35,4 @@ namespace Tgstation.Server.Client /// The inner for the base public InsufficientPermissionsException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/MethodNotSupportedException.cs b/src/Tgstation.Server.Client/MethodNotSupportedException.cs index 8c837d29ab..88774c4053 100644 --- a/src/Tgstation.Server.Client/MethodNotSupportedException.cs +++ b/src/Tgstation.Server.Client/MethodNotSupportedException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using Tgstation.Server.Api.Models; @@ -17,6 +17,11 @@ namespace Tgstation.Server.Client public MethodNotSupportedException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage) { } + /// + /// Intializes a new instance of the . + /// + public MethodNotSupportedException() { } + /// /// Construct an with a /// @@ -30,4 +35,4 @@ namespace Tgstation.Server.Client /// The inner for the base public MethodNotSupportedException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/RateLimitException.cs b/src/Tgstation.Server.Client/RateLimitException.cs index b6968576b7..c42a9b69af 100644 --- a/src/Tgstation.Server.Client/RateLimitException.cs +++ b/src/Tgstation.Server.Client/RateLimitException.cs @@ -1,4 +1,4 @@ -using Microsoft.Net.Http.Headers; +using Microsoft.Net.Http.Headers; using System; using System.Linq; using System.Net.Http; @@ -23,6 +23,9 @@ namespace Tgstation.Server.Client /// The for the . public RateLimitException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage) { + if (responseMessage == null) + throw new ArgumentNullException(nameof(responseMessage)); + if (!responseMessage.Headers.TryGetValues(HeaderNames.RetryAfter, out var values)) return; @@ -36,6 +39,12 @@ namespace Tgstation.Server.Client /// public RateLimitException() { } + /// + /// Initializes a new instance of the . + /// + /// The message for the . + public RateLimitException(string message) : base(message) { } + /// /// Construct an with a and /// @@ -43,4 +52,4 @@ namespace Tgstation.Server.Client /// The inner for the base public RateLimitException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/RequestTimeoutException.cs b/src/Tgstation.Server.Client/RequestTimeoutException.cs index b979f2428c..5acbcd633f 100644 --- a/src/Tgstation.Server.Client/RequestTimeoutException.cs +++ b/src/Tgstation.Server.Client/RequestTimeoutException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; namespace Tgstation.Server.Client @@ -15,6 +15,11 @@ namespace Tgstation.Server.Client public RequestTimeoutException(HttpResponseMessage responseMessage) : base(responseMessage, "The request timed out!") { } + /// + /// Intializes a new instance of the . + /// + public RequestTimeoutException() { } + /// /// Construct an with a /// @@ -28,4 +33,4 @@ namespace Tgstation.Server.Client /// The inner for the base public RequestTimeoutException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/ServerClientFactory.cs b/src/Tgstation.Server.Client/ServerClientFactory.cs index 211645a9cb..588112e057 100644 --- a/src/Tgstation.Server.Client/ServerClientFactory.cs +++ b/src/Tgstation.Server.Client/ServerClientFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; @@ -38,7 +38,7 @@ namespace Tgstation.Server.Client string password, IEnumerable? requestLoggers = null, TimeSpan? timeout = null, - bool attemptRefreshLogin = true, + bool attemptLoginRefresh = true, CancellationToken cancellationToken = default) { if (host == null) diff --git a/src/Tgstation.Server.Client/ServerErrorException.cs b/src/Tgstation.Server.Client/ServerErrorException.cs index 31f6ec2837..c2feecdeaa 100644 --- a/src/Tgstation.Server.Client/ServerErrorException.cs +++ b/src/Tgstation.Server.Client/ServerErrorException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using Tgstation.Server.Api.Models; @@ -23,6 +23,12 @@ namespace Tgstation.Server.Client { } + /// + /// Initializes a new instance of the . + /// + /// The message for the . + public ServerErrorException(string message) : base(message) { } + /// /// Construct an with a and /// @@ -30,4 +36,4 @@ namespace Tgstation.Server.Client /// The inner for the base public ServerErrorException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/ServiceUnavailableException.cs b/src/Tgstation.Server.Client/ServiceUnavailableException.cs index c8a26bb0e8..e471416c9e 100644 --- a/src/Tgstation.Server.Client/ServiceUnavailableException.cs +++ b/src/Tgstation.Server.Client/ServiceUnavailableException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; namespace Tgstation.Server.Client @@ -15,6 +15,11 @@ namespace Tgstation.Server.Client public ServiceUnavailableException(HttpResponseMessage responseMessage) : base(responseMessage, "The service is unavailable!") { } + /// + /// Intializes a new instance of the . + /// + public ServiceUnavailableException() { } + /// /// Construct an with a /// @@ -28,4 +33,4 @@ namespace Tgstation.Server.Client /// The inner for the base public ServiceUnavailableException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 4ceb1f0844..e1449db6b1 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -1,4 +1,4 @@ - + @@ -23,17 +23,18 @@ latest enable bin\$(Configuration)\netstandard2.1\Tgstation.Server.Client.xml + true true - + - + all - runtime; build; native; contentfiles; analyzers + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tgstation.Server.Client/UnauthorizedException.cs b/src/Tgstation.Server.Client/UnauthorizedException.cs index 206f0b39b6..07228d299b 100644 --- a/src/Tgstation.Server.Client/UnauthorizedException.cs +++ b/src/Tgstation.Server.Client/UnauthorizedException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using Tgstation.Server.Api.Models; @@ -17,6 +17,11 @@ namespace Tgstation.Server.Client public UnauthorizedException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage) { } + /// + /// Intializes a new instance of the . + /// + public UnauthorizedException() { } + /// /// Construct an with a /// @@ -30,4 +35,4 @@ namespace Tgstation.Server.Client /// The inner for the base public UnauthorizedException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host.Console/Program.cs b/src/Tgstation.Server.Host.Console/Program.cs index ab8c475b50..9f3314dcaa 100644 --- a/src/Tgstation.Server.Host.Console/Program.cs +++ b/src/Tgstation.Server.Host.Console/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading; diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index d71f4dfdcd..9f237031c3 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -1,6 +1,6 @@ - + - + Exe netcoreapp3.1 @@ -9,6 +9,7 @@ ../../build/analyzers.ruleset latest false + true @@ -23,9 +24,9 @@ - + all - compile; build; native; contentfiles; analyzers + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index be8411533d..d2bb63a992 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -1,4 +1,4 @@ - + @@ -11,6 +11,7 @@ 7.3 bin\Debug\Tgstation.Server.Host.Console.xml false + true @@ -24,17 +25,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - - + diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index 82c5faff69..dc6ff60c72 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -1,4 +1,4 @@ - + @@ -9,6 +9,7 @@ ../../build/analyzers.ruleset latest false + true @@ -23,15 +24,12 @@ - + all - compile; build; native; contentfiles; analyzers + runtime; build; native; contentfiles; analyzers; buildtransitive + - - all - runtime; build; native; contentfiles; analyzers - diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index b434ac6a61..e29b19b44e 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -10,7 +10,8 @@ false true bin\$(Configuration)\netcoreapp3.1\Tgstation.Server.Host.xml - API1000,CA1508 + true + API1000 @@ -69,9 +70,9 @@ - + all - runtime; build; native; contentfiles; analyzers + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -80,10 +81,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -94,10 +91,6 @@ - - all - runtime; build; native; contentfiles; analyzers - @@ -105,7 +98,7 @@ - + diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj index 4ac3e5c0c5..01fb79d54d 100644 --- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj +++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index 1ce9b9982d..a9ec4e77b5 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -12,8 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj index c37b05c82f..5a8865fcb8 100644 --- a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj +++ b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -12,8 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj index c47523526f..9fbb5c8fc7 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj +++ b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index 60aba1cdb0..8ebbc1c373 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -12,8 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj index c4cfd002c5..8043952ec1 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj @@ -18,8 +18,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index ebea335bde..c6cde39a21 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -12,8 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 68e426bb818be4dcc2557f736180cc847601da40 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 18:47:42 -0500 Subject: [PATCH 069/154] Fix unsetting attemptLoginRefresh - It previously did nothing --- src/Tgstation.Server.Client/ServerClientFactory.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tgstation.Server.Client/ServerClientFactory.cs b/src/Tgstation.Server.Client/ServerClientFactory.cs index 588112e057..5dd86c3189 100644 --- a/src/Tgstation.Server.Client/ServerClientFactory.cs +++ b/src/Tgstation.Server.Client/ServerClientFactory.cs @@ -62,6 +62,9 @@ namespace Tgstation.Server.Client token = await api.Update(Routes.Root, cancellationToken).ConfigureAwait(false); } + if (!attemptLoginRefresh) + loginHeaders = null; + var apiHeaders = new ApiHeaders(productHeaderValue, token.Bearer!); var client = new ServerClient(ApiClientFactory.CreateApiClient(host, apiHeaders, loginHeaders), token); From 43bcd96b33797c13a6de4cb1e44991386705da22 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 16 Dec 2020 20:52:52 -0500 Subject: [PATCH 070/154] Increase some test timeouts slightly --- tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 35dddd9d35..7203d1c646 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -36,7 +36,7 @@ namespace Tgstation.Server.Tests.Instance // Increase startup timeout, disable heartbeats var initialSettings = await instanceClient.DreamDaemon.Update(new DreamDaemon { - StartupTimeout = 30, + StartupTimeout = 60, HeartbeatSeconds = 0, Port = IntegrationTest.DDPort }, cancellationToken); @@ -389,7 +389,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await StartDD(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 40, false, null, cancellationToken); + await WaitForJob(startJob, 70, false, null, cancellationToken); var byondInstallJobTask = instanceClient.Byond.SetActiveVersion( new Api.Models.Byond From 713987c251878ea6ce217415a700348deb0680e9 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 17 Dec 2020 10:22:19 -0500 Subject: [PATCH 071/154] Fix some weirdness in calculating LaunchResult --- .../Components/Session/SessionController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index ac8eaa9b9e..ec9b1301cc 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -308,7 +308,7 @@ namespace Tgstation.Server.Host.Components.Session var toAwait = Task.WhenAny(startupTask, process.Lifetime); if (startupTimeout.HasValue) - toAwait = Task.WhenAny(toAwait, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime)); + toAwait = Task.WhenAny(toAwait, Task.Delay(TimeSpan.FromSeconds(startupTimeout.Value))); logger.LogTrace( "Waiting for LaunchResult based on {0}{1}...", From 0ced14764f009d97e69ae0ce311fd6114ed775f3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 17 Dec 2020 12:07:55 -0500 Subject: [PATCH 072/154] Explicitly build before running integration tests --- .github/workflows/ci-suite.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 90c3b85a8d..6db1ce86fe 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -223,11 +223,14 @@ jobs: TEMP_GITHUB_REF="${{ github.event.ref }}" echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV + - name: Build + run: dotnet build -c ${{ matrix.configuration }} + - name: Run Integration Test run: | cd tests/Tgstation.Server.Tests Start-Sleep -Seconds 10 - dotnet test -c ${{ matrix.configuration }} -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults + dotnet test -c ${{ matrix.configuration }} --no-build "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults - name: Store Code Coverage uses: actions/upload-artifact@v2 @@ -367,11 +370,14 @@ jobs: TEMP_GITHUB_REF="${{ github.event.ref }}" echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV + - name: Build + run: dotnet build -c ${{ matrix.configuration }} + - name: Run Integration Test run: | cd tests/Tgstation.Server.Tests sleep 10 - dotnet test -c ${{ matrix.configuration }} -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults + dotnet test -c ${{ matrix.configuration }} --no-build -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults - name: Store Code Coverage uses: actions/upload-artifact@v2 From eb709d136b6e0d171e19083dff947a571b9391af Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 17 Dec 2020 23:49:06 -0500 Subject: [PATCH 073/154] Adds pagination - Add generic Paginated for pagination responses. - Added pagination helper to ApiController. - Implemented pagination for all previously IEnumerable response endpoints. - Added IApiTransformable for host models finally. - Added PaginationSettings to client List endpoints. - Use two deprecated ErrorCodes for page parameter validation. - Re-enabled to OpenAPI root array response lint. - Add some integration tests with users. --- build/OpenApiValidationSettings.json | 2 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 14 +- src/Tgstation.Server.Api/Models/Paginated.cs | 28 ++++ .../AdministrationClient.cs | 27 ++-- .../Components/ByondClient.cs | 20 +-- .../Components/ChatBotsClient.cs | 26 ++-- .../Components/ConfigurationClient.cs | 33 ++-- .../Components/DreamMakerClient.cs | 26 ++-- .../Components/IByondClient.cs | 3 +- .../Components/IChatBotsClient.cs | 5 +- .../Components/IConfigurationClient.cs | 6 +- .../Components/IDreamMakerClient.cs | 7 +- .../Components/IInstanceUserClient.cs | 7 +- .../Components/IJobsClient.cs | 10 +- .../Components/InstanceUserClient.cs | 28 ++-- .../Components/JobsClient.cs | 25 ++- .../IAdministrationClient.cs | 3 +- .../IInstanceManagerClient.cs | 5 +- src/Tgstation.Server.Client/IUsersClient.cs | 7 +- .../InstanceManagerClient.cs | 33 ++-- .../PaginatedClient.cs | 122 +++++++++++++++ .../PaginationSettings.cs | 24 +++ src/Tgstation.Server.Client/UsersClient.cs | 26 ++-- .../Controllers/AdministrationController.cs | 79 +++++----- .../Controllers/ApiController.cs | 142 +++++++++++++++++- .../Controllers/ByondController.cs | 33 ++-- .../Controllers/ChatController.cs | 38 ++--- .../Controllers/ConfigurationController.cs | 82 ++++++---- .../Controllers/DreamMakerController.cs | 57 ++++--- .../Controllers/InstanceController.cs | 33 ++-- .../Controllers/InstanceUserController.cs | 31 ++-- .../Controllers/JobController.cs | 67 +++++---- .../Controllers/PaginatableResult.cs | 41 +++++ .../Controllers/UserController.cs | 29 ++-- .../Core/SwaggerConfiguration.cs | 5 +- src/Tgstation.Server.Host/Models/ChatBot.cs | 9 +- .../Models/ChatChannel.cs | 9 +- .../Models/CompileJob.cs | 7 +- .../Models/DreamMakerSettings.cs | 9 +- .../Models/IApiTransformable.cs | 15 ++ src/Tgstation.Server.Host/Models/Instance.cs | 12 +- .../Models/InstanceUser.cs | 9 +- src/Tgstation.Server.Host/Models/Job.cs | 7 +- .../Models/OAuthConnection.cs | 7 +- .../Models/RepositorySettings.cs | 7 +- .../Models/RevisionInformation.cs | 9 +- src/Tgstation.Server.Host/Models/TestMerge.cs | 9 +- src/Tgstation.Server.Host/Models/User.cs | 5 +- .../AdministrationTest.cs | 2 +- .../Instance/ByondTest.cs | 2 +- .../Instance/ChatTest.cs | 10 +- .../InstanceManagerTest.cs | 4 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 8 +- tests/Tgstation.Server.Tests/UsersTest.cs | 54 ++++++- 54 files changed, 884 insertions(+), 434 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/Paginated.cs create mode 100644 src/Tgstation.Server.Client/PaginatedClient.cs create mode 100644 src/Tgstation.Server.Client/PaginationSettings.cs create mode 100644 src/Tgstation.Server.Host/Controllers/PaginatableResult.cs create mode 100644 src/Tgstation.Server.Host/Models/IApiTransformable.cs diff --git a/build/OpenApiValidationSettings.json b/build/OpenApiValidationSettings.json index 731082653b..af87e9cf4b 100644 --- a/build/OpenApiValidationSettings.json +++ b/build/OpenApiValidationSettings.json @@ -4,7 +4,7 @@ "no_operation_id": "error", "operation_id_case_convention": "off", "no_summary": "error", - "no_array_responses": "off", + "no_array_responses": "error", "parameter_order": "error", "undefined_tag": "off", "unused_tag": "error", diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index cb9a77a786..1b206b6ab3 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -250,18 +250,16 @@ namespace Tgstation.Server.Api.Models RepoWhitespaceCommitterEmail, /// - /// Deprecated. + /// A paginated request asked for too large a page. /// - [Description("Deprecated error code.")] - [Obsolete("With API v7 ", true)] - DreamDaemonDuplicatePorts, + [Description("Requested pageSize is too large!")] + ApiPageTooLarge, /// - /// Deprecated. + /// A paginated request asked for page 0. /// - [Description("Deprecated error code.")] - [Obsolete("With DMAPI-5.0.0, ultrasafe security is now supported.", true)] - InvalidSecurityLevel, + [Description("Cannot request page or pageSize <= 0.")] + ApiInvalidPageOrPageSize, /// /// A requested 's data does not match with its . diff --git a/src/Tgstation.Server.Api/Models/Paginated.cs b/src/Tgstation.Server.Api/Models/Paginated.cs new file mode 100644 index 0000000000..ba4e187494 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Paginated.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Api.Models +{ + /// + /// Represents a paginated set of models. + /// + /// The of the returned model. + public sealed class Paginated + { + /// + /// The of the returned s. + /// + [Required] + public ICollection? Content { get; set; } + + /// + /// The total number of pages in the query. + /// + public int TotalPages { get; set; } + + /// + /// The current size of pages in the query. + /// + public int PageSize { get; set; } + } +} diff --git a/src/Tgstation.Server.Client/AdministrationClient.cs b/src/Tgstation.Server.Client/AdministrationClient.cs index ca00100e8b..e941337ebf 100644 --- a/src/Tgstation.Server.Client/AdministrationClient.cs +++ b/src/Tgstation.Server.Client/AdministrationClient.cs @@ -10,45 +10,40 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client { /// - sealed class AdministrationClient : IAdministrationClient + sealed class AdministrationClient : PaginatedClient, IAdministrationClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// Construct an /// - /// The value of + /// The for the . public AdministrationClient(IApiClient apiClient) - { - this.apiClient = apiClient; - } + : base(apiClient) + { } /// - public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.Administration, cancellationToken); + public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.Administration, cancellationToken); /// - public Task Update(Administration administration, CancellationToken cancellationToken) => apiClient.Update(Routes.Administration, administration ?? throw new ArgumentNullException(nameof(administration)), cancellationToken); + public Task Update(Administration administration, CancellationToken cancellationToken) => ApiClient.Update(Routes.Administration, administration ?? throw new ArgumentNullException(nameof(administration)), cancellationToken); /// - public Task Restart(CancellationToken cancellationToken) => apiClient.Delete(Routes.Administration, cancellationToken); + public Task Restart(CancellationToken cancellationToken) => ApiClient.Delete(Routes.Administration, cancellationToken); /// - public Task> ListLogs(CancellationToken cancellationToken) => apiClient.Read>(Routes.Logs, cancellationToken); + public Task> ListLogs(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.Logs, null, cancellationToken); /// public async Task> GetLog(LogFile logFile, CancellationToken cancellationToken) { - var resultFile = await apiClient.Read( + var resultFile = await ApiClient.Read( Routes.Logs + Routes.SanitizeGetPath( HttpUtility.UrlEncode( logFile?.Name ?? throw new ArgumentNullException(nameof(logFile)))), cancellationToken) .ConfigureAwait(false); - var stream = await apiClient.Download(resultFile, cancellationToken).ConfigureAwait(false); + var stream = await ApiClient.Download(resultFile, cancellationToken).ConfigureAwait(false); try { return Tuple.Create(resultFile, stream); diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index 15eb50bb7d..0a1847b35a 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -9,13 +9,8 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { /// - sealed class ByondClient : IByondClient + sealed class ByondClient : PaginatedClient, IByondClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// The for the /// @@ -24,24 +19,25 @@ namespace Tgstation.Server.Client.Components /// /// Construct a /// - /// The value of + /// The for the . /// The value of public ByondClient(IApiClient apiClient, Instance instance) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public Task ActiveVersion(CancellationToken cancellationToken) => apiClient.Read(Routes.Byond, instance.Id, cancellationToken); + public Task ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read(Routes.Byond, instance.Id, cancellationToken); /// - public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken); + public Task> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken); /// public async Task SetActiveVersion(Byond byond, Stream zipFileStream, CancellationToken cancellationToken) { - var result = await apiClient.Update( + var result = await ApiClient.Update( Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, @@ -49,7 +45,7 @@ namespace Tgstation.Server.Client.Components .ConfigureAwait(false); if (byond.UploadCustomZip == true) - await apiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false); + await ApiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false); return result; } diff --git a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs index 5a05e8c930..fa58da1c8a 100644 --- a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs +++ b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { /// - sealed class ChatBotsClient : IChatBotsClient + sealed class ChatBotsClient : PaginatedClient, IChatBotsClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// The for the /// @@ -23,27 +18,28 @@ namespace Tgstation.Server.Client.Components /// /// Construct a /// - /// The value of + /// The for the . /// The value of public ChatBotsClient(IApiClient apiClient, Instance instance) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public Task Create(ChatBot settings, CancellationToken cancellationToken) => apiClient.Create(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken); + public Task Create(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Create(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken); /// - 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 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.ListRoute(Routes.Chat), instance.Id, cancellationToken); + public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, 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); + public Task Update(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Update(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken); /// - public Task GetId(ChatBot settings, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken); + public Task GetId(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index 25f8a0060d..e0fdb69db6 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -9,13 +9,8 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { /// - sealed class ConfigurationClient : IConfigurationClient + sealed class ConfigurationClient : PaginatedClient, IConfigurationClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// The for the /// @@ -24,34 +19,42 @@ namespace Tgstation.Server.Client.Components /// /// Construct a /// - /// The value of + /// The for the . /// The value of public ConfigurationClient(IApiClient apiClient, Instance instance) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken); + public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => ApiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken); /// - public Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create(Routes.Configuration, directory, instance.Id, cancellationToken); + 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.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory), instance.Id, cancellationToken); + public Task> List( + PaginationSettings? paginationSettings, + string directory, + CancellationToken cancellationToken) + => ReadPaged( + paginationSettings, + Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory), + instance.Id, + cancellationToken); /// public async Task> Read(ConfigurationFile file, CancellationToken cancellationToken) { if (file == null) throw new ArgumentNullException(nameof(file)); - var configFile = await apiClient.Read( + var configFile = await ApiClient.Read( Routes.ConfigurationFile + Routes.SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))), instance.Id, cancellationToken) .ConfigureAwait(false); - var downloadStream = await apiClient.Download(configFile, cancellationToken).ConfigureAwait(false); + var downloadStream = await ApiClient.Download(configFile, cancellationToken).ConfigureAwait(false); try { return Tuple.Create(configFile, downloadStream); @@ -75,7 +78,7 @@ namespace Tgstation.Server.Client.Components using (memoryStream) { - var configFileTask = apiClient.Update( + var configFileTask = ApiClient.Update( Routes.Configuration, file ?? throw new ArgumentNullException(nameof(file)), instance.Id, @@ -88,7 +91,7 @@ namespace Tgstation.Server.Client.Components var streamUsed = memoryStream ?? uploadStream; streamUsed?.Seek(initialStreamPosition, SeekOrigin.Begin); - await apiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false); + await ApiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false); return configFile; } diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs index dafa049834..fdce033ef8 100644 --- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { /// - sealed class DreamMakerClient : IDreamMakerClient + sealed class DreamMakerClient : PaginatedClient, IDreamMakerClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// The for the /// @@ -23,27 +18,28 @@ namespace Tgstation.Server.Client.Components /// /// Construct a /// - /// The value of + /// The for the . /// The value of public DreamMakerClient(IApiClient apiClient, Instance instance) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public Task Compile(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamMaker, instance.Id, cancellationToken); + public Task Compile(CancellationToken cancellationToken) => ApiClient.Create(Routes.DreamMaker, 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 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); + public Task> ListCompileJobs(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken); /// - public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken); + public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken); /// - public Task Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => apiClient.Update(Routes.DreamMaker, dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)), instance.Id, cancellationToken); + public Task Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => ApiClient.Update(Routes.DreamMaker, dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)), instance.Id, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs index 44c90f64c4..ee1befa5c7 100644 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ b/src/Tgstation.Server.Client/Components/IByondClient.cs @@ -21,9 +21,10 @@ namespace Tgstation.Server.Client.Components /// /// Get all installed s /// + /// The optional for the operation. /// The for the operation /// A resulting in an of installed s - Task> InstalledVersions(CancellationToken cancellationToken); + Task> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Updates the information diff --git a/src/Tgstation.Server.Client/Components/IChatBotsClient.cs b/src/Tgstation.Server.Client/Components/IChatBotsClient.cs index c3f223938e..92485c0800 100644 --- a/src/Tgstation.Server.Client/Components/IChatBotsClient.cs +++ b/src/Tgstation.Server.Client/Components/IChatBotsClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -13,9 +13,10 @@ namespace Tgstation.Server.Client.Components /// /// List the s /// + /// The optional for the operation. /// The for the operation /// A resulting in a of the of the server - Task> List(CancellationToken cancellationToken); + Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Create a diff --git a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs index e82220c028..9e022fe084 100644 --- a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs @@ -15,10 +15,14 @@ namespace Tgstation.Server.Client.Components /// /// List configuration files /// + /// The optional for the operation. /// The path to the directory to list files in /// The for the operation /// A of s in the - Task> List(string directory, CancellationToken cancellationToken); + Task> List( + PaginationSettings? paginationSettings, + string directory, + CancellationToken cancellationToken); /// /// Read a file diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs index 174634bdb4..24d91af12a 100644 --- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -33,11 +33,12 @@ namespace Tgstation.Server.Client.Components Task Compile(CancellationToken cancellationToken); /// - /// Gets the s of all s for the instance + /// Gets the s for the instance /// + /// The optional for the operation. /// The for the operation /// A resulting in a of s. - Task> GetJobIds(CancellationToken cancellationToken); + Task> ListCompileJobs(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Get a diff --git a/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs b/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs index 3d1d54e8d7..8035783b12 100644 --- a/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs +++ b/src/Tgstation.Server.Client/Components/IInstanceUserClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -28,9 +28,10 @@ namespace Tgstation.Server.Client.Components /// /// Get the s in the /// + /// The optional for the operation. /// The for the operation /// A resulting in a of s in the instance - Task> List(CancellationToken cancellationToken); + Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Update a @@ -56,4 +57,4 @@ namespace Tgstation.Server.Client.Components /// A representing the running operation Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/IJobsClient.cs b/src/Tgstation.Server.Client/Components/IJobsClient.cs index ae879f485e..b3d800b51b 100644 --- a/src/Tgstation.Server.Client/Components/IJobsClient.cs +++ b/src/Tgstation.Server.Client/Components/IJobsClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -11,18 +11,20 @@ namespace Tgstation.Server.Client.Components public interface IJobsClient { /// - /// List the s in the + /// List the s in the /// + /// The optional for the operation. /// The for the operation /// A resulting in a of the s in the - Task> List(CancellationToken cancellationToken); + Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// List the active s in the /// + /// The optional for the operation. /// The for the operation /// A resulting in a of the active s in the - Task> ListActive(CancellationToken cancellationToken); + Task> ListActive(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Get a diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs index 1dd1659ed7..740d94e4e7 100644 --- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs +++ b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { /// - sealed class InstanceUserClient : IInstanceUserClient + sealed class InstanceUserClient : PaginatedClient, IInstanceUserClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// The for the /// @@ -23,19 +18,19 @@ namespace Tgstation.Server.Client.Components /// /// Construct an /// - /// The value of + /// The for the . /// The value of public InstanceUserClient(IApiClient apiClient, Instance instance) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public Task Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken); + public Task Create(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Create(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken); /// - public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete( + public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Delete( Routes.SetID( Routes.InstanceUser, instanceUser.UserId), @@ -43,15 +38,16 @@ namespace Tgstation.Server.Client.Components cancellationToken); /// - public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.InstanceUser, instance.Id, cancellationToken); + public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.InstanceUser, instance.Id, cancellationToken); /// - public Task Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken); + 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.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken); + public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, 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); + public Task GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs index 228a2c4a9b..cf6a8875ec 100644 --- a/src/Tgstation.Server.Client/Components/JobsClient.cs +++ b/src/Tgstation.Server.Client/Components/JobsClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client.Components { /// - sealed class JobsClient : IJobsClient + sealed class JobsClient : PaginatedClient, IJobsClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// The for the /// @@ -23,24 +18,26 @@ namespace Tgstation.Server.Client.Components /// /// Construct a /// - /// The value of + /// The for the . /// The value of public JobsClient(IApiClient apiClient, Instance instance) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - 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 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(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken); /// - public Task> ListActive(CancellationToken cancellationToken) => apiClient.Read>(Routes.Jobs, instance.Id, cancellationToken); + public Task> ListActive(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.Jobs, 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); + 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 +} diff --git a/src/Tgstation.Server.Client/IAdministrationClient.cs b/src/Tgstation.Server.Client/IAdministrationClient.cs index b77e7475f7..b8abccfc91 100644 --- a/src/Tgstation.Server.Client/IAdministrationClient.cs +++ b/src/Tgstation.Server.Client/IAdministrationClient.cs @@ -37,9 +37,10 @@ namespace Tgstation.Server.Client /// /// Lists the log files available for download. /// + /// The optional for the operation. /// The for the operation /// A resulting in an of metadata. - Task> ListLogs(CancellationToken cancellationToken); + Task> ListLogs(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Download a given . diff --git a/src/Tgstation.Server.Client/IInstanceManagerClient.cs b/src/Tgstation.Server.Client/IInstanceManagerClient.cs index 52bc42e769..4e53f8f32b 100644 --- a/src/Tgstation.Server.Client/IInstanceManagerClient.cs +++ b/src/Tgstation.Server.Client/IInstanceManagerClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -14,9 +14,10 @@ namespace Tgstation.Server.Client /// /// Get all s for s the user can view /// + /// The optional for the operation. /// The for the operation /// A resulting in a of all s the user can view - Task> List(CancellationToken cancellationToken); + Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Create or attach an diff --git a/src/Tgstation.Server.Client/IUsersClient.cs b/src/Tgstation.Server.Client/IUsersClient.cs index 8cf253f1bd..f024eb38ef 100644 --- a/src/Tgstation.Server.Client/IUsersClient.cs +++ b/src/Tgstation.Server.Client/IUsersClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -28,9 +28,10 @@ namespace Tgstation.Server.Client /// /// List all s /// + /// The optional for the operation. /// The for the operation /// A resulting in a of all s - Task> List(CancellationToken cancellationToken); + Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Create a new @@ -48,4 +49,4 @@ namespace Tgstation.Server.Client /// The updated Task Update(UserUpdate user, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/InstanceManagerClient.cs b/src/Tgstation.Server.Client/InstanceManagerClient.cs index 9bd9b4846f..910f09896e 100644 --- a/src/Tgstation.Server.Client/InstanceManagerClient.cs +++ b/src/Tgstation.Server.Client/InstanceManagerClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -9,41 +9,36 @@ using Tgstation.Server.Client.Components; namespace Tgstation.Server.Client { /// - sealed class InstanceManagerClient : IInstanceManagerClient + sealed class InstanceManagerClient : PaginatedClient, IInstanceManagerClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// Construct an /// - /// The value of + /// The for the . public InstanceManagerClient(IApiClient apiClient) - { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); - } + : base(apiClient) + { } /// - public Task CreateOrAttach(Instance instance, CancellationToken cancellationToken) => apiClient.Create(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken); + public Task CreateOrAttach(Instance instance, CancellationToken cancellationToken) => ApiClient.Create(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken); /// - public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); + 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.ListRoute(Routes.InstanceManager), cancellationToken); + public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.InstanceManager), null, cancellationToken); /// - public Task Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken); + public Task Update(Instance instance, CancellationToken cancellationToken) => ApiClient.Update(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken); /// - public Task GetId(Instance instance, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); + public Task GetId(Instance instance, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); /// - public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => apiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); + public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => ApiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); /// - public IInstanceClient CreateClient(Instance instance) => new InstanceClient(apiClient, instance); + public IInstanceClient CreateClient(Instance instance) => new InstanceClient(ApiClient, instance); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Client/PaginatedClient.cs b/src/Tgstation.Server.Client/PaginatedClient.cs new file mode 100644 index 0000000000..fa9191c3cd --- /dev/null +++ b/src/Tgstation.Server.Client/PaginatedClient.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client +{ + /// + /// Client that deals with getting paginated results. + /// + abstract class PaginatedClient + { + /// + /// The for the + /// + protected IApiClient ApiClient { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public PaginatedClient(IApiClient apiClient) + { + ApiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + } + + /// + /// Reads a given with paged results. + /// + /// The of result model. + /// The if any. + /// The route. + /// The optional . + /// The for the operation. + /// A resulting in an of the paginated s. + protected async Task> ReadPaged( + PaginationSettings? paginationSettings, + string route, + long? instanceId, + CancellationToken cancellationToken) + { + if (route == null) + throw new ArgumentNullException(nameof(route)); + + var routeFormatter = $"{route}?page={{0}}"; + var currentPage = 1; + if (paginationSettings != null) + { + if (paginationSettings.RetrieveCount == 0) + return new List(); // that was easy + else if (paginationSettings.RetrieveCount < 0) + throw new ArgumentOutOfRangeException("RetrieveCount cannot be less than 0!", nameof(paginationSettings)); + + int? pageSize = null; + if (paginationSettings.PageSize.HasValue) + { + // pagesize validates itself on first request + pageSize = paginationSettings.PageSize.Value; + routeFormatter += $"&pageSize={pageSize}"; + } + + if (paginationSettings.Offset.HasValue) + { + if(paginationSettings.Offset.Value < 0) + throw new ArgumentOutOfRangeException("Offset cannot be less than 0!", nameof(paginationSettings)); + + pageSize ??= paginationSettings.Offset.Value; + currentPage = (paginationSettings.Offset.Value / pageSize.Value) + 1; + } + } + + Task> GetPage() => instanceId.HasValue + ? ApiClient.Read>( + String.Format(routeFormatter, currentPage), + instanceId.Value, + cancellationToken) + : ApiClient.Read>( + String.Format(routeFormatter, currentPage), + cancellationToken); + + var firstPage = await GetPage().ConfigureAwait(false); + + var totalAvailable = firstPage.TotalPages * firstPage.PageSize; + var maximumItems = paginationSettings?.RetrieveCount.HasValue == true + ? Math.Min(paginationSettings.RetrieveCount!.Value, totalAvailable) + : totalAvailable; + + var results = new List(maximumItems); + var currentResults = firstPage; + do + { + // check if first page + if(currentPage > 1) + currentResults = await GetPage().ConfigureAwait(false); + + if (currentResults.Content == null) + throw new ApiConflictException("Paginated results missing content!"); + + IEnumerable rangeToAdd = currentResults.Content; + if (paginationSettings?.Offset.HasValue == true) + { + rangeToAdd = rangeToAdd + .Skip(paginationSettings.Offset!.Value % currentResults.PageSize); + } + + var itemsAvailableInPage = rangeToAdd.Count(); + var itemsStillRequired = maximumItems - results.Count; + if (itemsAvailableInPage > itemsStillRequired) + rangeToAdd = rangeToAdd + .Take(itemsStillRequired); + + results.AddRange(rangeToAdd); + ++currentPage; + } + while (results.Count < maximumItems && currentPage <= currentResults.TotalPages); + + return results; + } + } +} diff --git a/src/Tgstation.Server.Client/PaginationSettings.cs b/src/Tgstation.Server.Client/PaginationSettings.cs new file mode 100644 index 0000000000..8c03734f20 --- /dev/null +++ b/src/Tgstation.Server.Client/PaginationSettings.cs @@ -0,0 +1,24 @@ +namespace Tgstation.Server.Client +{ + /// + /// Settings for a paginated request. + /// + public sealed class PaginationSettings + { + /// + /// The size of a page. Defaults to server settings. + /// + public int? PageSize { get; set; } + + /// + /// The offset to take from. Default 0. + /// + public int? Offset { get; set; } + + /// + /// The maximum amount of items to retrieve. Default everything. + /// + /// Results will be truncated if overflow occurs due to . + public int? RetrieveCount { get; set; } + } +} diff --git a/src/Tgstation.Server.Client/UsersClient.cs b/src/Tgstation.Server.Client/UsersClient.cs index 25ff956856..8d53a487dc 100644 --- a/src/Tgstation.Server.Client/UsersClient.cs +++ b/src/Tgstation.Server.Client/UsersClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,35 +8,31 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client { /// - sealed class UsersClient : IUsersClient + sealed class UsersClient : PaginatedClient, IUsersClient { - /// - /// The for the - /// - readonly IApiClient apiClient; - /// /// Construct an /// - /// The value of + /// The for the . public UsersClient(IApiClient apiClient) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); } /// - public Task Create(UserUpdate user, CancellationToken cancellationToken) => apiClient.Create(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken); + public Task Create(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Create(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken); /// - public Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken); + public Task GetId(Api.Models.Internal.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.ListRoute(Routes.User), cancellationToken); + public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.User), null, cancellationToken); /// - public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.User, cancellationToken); + public Task Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.User, cancellationToken); /// - public Task Update(UserUpdate user, CancellationToken cancellationToken) => apiClient.Update(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken); + public Task Update(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Update(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 779fa02d99..b309585c15 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -313,48 +313,59 @@ namespace Tgstation.Server.Host.Controllers /// /// List s present. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the request. /// Listed logs successfully. /// An IO error occurred while listing. [HttpGet(Routes.Logs)] [TgsAuthorize(AdministrationRights.DownloadLogs)] - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(Paginated), 200)] [ProducesResponseType(typeof(ErrorMessage), 409)] - public async Task ListLogs(CancellationToken cancellationToken) - { - var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier); - try - { - var files = await ioManager.GetFiles(path, cancellationToken).ConfigureAwait(false); - var tasks = files.Select( - async file => new LogFile - { - Name = ioManager.GetFileName(file), - LastModified = await ioManager.GetLastModified( - ioManager.ConcatPath(path, file), - cancellationToken) - .ConfigureAwait(false) - }) - .ToList(); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - var result = tasks - .Select(x => x.Result) - .OrderByDescending(x => x.Name) - .ToList(); - - return Ok(result); - } - catch (IOException ex) - { - return Conflict(new ErrorMessage(ErrorCode.IOError) + public Task ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + async () => { - AdditionalData = ex.ToString() - }); - } - } + var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier); + try + { + var files = await ioManager.GetFiles(path, cancellationToken).ConfigureAwait(false); + var tasks = files.Select( + async file => new LogFile + { + Name = ioManager.GetFileName(file), + LastModified = await ioManager + .GetLastModified( + ioManager.ConcatPath(path, file), + cancellationToken) + .ConfigureAwait(false) + }) + .ToList(); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + var result = tasks + .Select(x => x.Result) + .OrderByDescending(x => x.Name) + .ToList(); + + return new PaginatableResult( + result.AsQueryable()); + } + catch (IOException ex) + { + return new PaginatableResult( + Conflict(new ErrorMessage(ErrorCode.IOError) + { + AdditionalData = ex.ToString() + })); + } + }, + null, + page, + pageSize, + cancellationToken); /// /// Download a . diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 0a772be319..6b07fbd898 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -1,11 +1,14 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Octokit; using Serilog.Context; using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -15,6 +18,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers @@ -26,6 +30,16 @@ namespace Tgstation.Server.Host.Controllers [ApiController] public abstract class ApiController : Controller { + /// + /// Default size of results. + /// + private const ushort DefaultPageSize = 10; + + /// + /// Maximum size of results. + /// + private const ushort MaximumPageSize = 100; + /// /// The for the operation /// @@ -275,6 +289,132 @@ namespace Tgstation.Server.Host.Controllers await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); } } - #pragma warning restore CA1506 +#pragma warning restore CA1506 + + /// + /// Generates a paginated response. + /// + /// The of model being generated and returned. + /// A resulting in a resulting in the generated . + /// An to transform the s after being queried. + /// The requested page from the query. + /// The requested page size from the query. + /// The for the operation. + /// A resulting in the for the operation. + protected Task Paginated( + Func>> queryGenerator, + Action resultTransformer, + int? pageQuery, + int? pageSizeQuery, + CancellationToken cancellationToken) => PaginatedImpl( + queryGenerator, + resultTransformer, + pageQuery, + pageSizeQuery, + cancellationToken); + + /// + /// Generates a paginated response. + /// + /// The of model being generated. + /// The of model being returned. + /// A resulting in a resulting in the generated . + /// An to transform the s after being queried. + /// The requested page from the query. + /// The requested page size from the query. + /// The for the operation. + /// A resulting in the for the operation. + protected Task Paginated( + Func>> queryGenerator, + Action resultTransformer, + int? pageQuery, + int? pageSizeQuery, + CancellationToken cancellationToken) + where TModel : IApiTransformable + => PaginatedImpl( + queryGenerator, + resultTransformer, + pageQuery, + pageSizeQuery, + cancellationToken); + + /// + /// Generates a paginated response. + /// + /// The of model being generated. If different from , must implement for . + /// The of model being returned. + /// A resulting in a resulting in the generated . + /// An to transform the s after being queried. + /// The requested page from the query. + /// The requested page size from the query. + /// The for the operation. + /// A resulting in the for the operation. + async Task PaginatedImpl( + Func>> queryGenerator, + Action resultTransformer, + int? pageQuery, + int? pageSizeQuery, + CancellationToken cancellationToken) + { + if (queryGenerator == null) + throw new ArgumentNullException(nameof(queryGenerator)); + + if (pageQuery <= 0 || pageSizeQuery <= 0) + return BadRequest(new ErrorMessage(ErrorCode.ApiInvalidPageOrPageSize)); + + var pageSize = pageSizeQuery ?? DefaultPageSize; + if (pageSize > MaximumPageSize) + return BadRequest(new ErrorMessage(ErrorCode.ApiPageTooLarge) + { + AdditionalData = $"Maximum page size: {MaximumPageSize}" + }); + + var page = pageQuery ?? 1; + + var paginationResult = await queryGenerator().ConfigureAwait(false); + if (paginationResult.EarlyOut != null) + return paginationResult.EarlyOut; + + var queriedResults = paginationResult + .Results + .Skip((page - 1) * pageSize) + .Take(pageSize); + + int totalResults; + List pagedResults; + if (queriedResults.Provider is IAsyncQueryProvider) + { + totalResults = await paginationResult.Results.CountAsync(cancellationToken).ConfigureAwait(false); + pagedResults = await queriedResults + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + else + { + totalResults = paginationResult.Results.Count(); + pagedResults = queriedResults.ToList(); + } + + if (resultTransformer != null) + foreach (var I in pagedResults) + resultTransformer(I); + + ICollection finalResults; + if (typeof(TModel) == typeof(TResultModel)) + finalResults = (List)(object)pagedResults; // clearly a safe cast + else + finalResults = pagedResults + .OfType>() + .Select(x => x.ToApi()) + .ToList(); + + return Json( + new Paginated + { + Content = pagedResults, + PageSize = pageSize, + TotalPages = (ushort)((totalResults % pageSize) + 1) + }); + } } } diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 8c503ba47b..a03bb563f2 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -79,21 +78,31 @@ namespace Tgstation.Server.Host.Controllers /// /// Lists installed versions. /// + /// The current page. + /// The page size. + /// The for the operation. /// A resulting in the for the operation. /// Retrieved version information successfully. [HttpGet(Routes.List)] [TgsAuthorize(ByondRights.ListInstalled)] - [ProducesResponseType(typeof(IEnumerable), 200)] - public Task List() - => WithComponentInstance(instance => - Task.FromResult( - Json(instance - .ByondManager - .InstalledVersions - .Select(x => new Api.Models.Byond - { - Version = x - })))); + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => WithComponentInstance( + instance => Paginated( + () => Task.FromResult( + new PaginatableResult( + instance + .ByondManager + .InstalledVersions + .Select(x => new Api.Models.Byond + { + Version = x + }) + .AsQueryable())), + null, + page, + pageSize, + cancellationToken)); /// /// Changes the active BYOND version to the one specified in a given . diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 45c2db5d74..d328a300eb 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -168,29 +168,33 @@ namespace Tgstation.Server.Host.Controllers /// /// List s. /// + /// The current page. + /// The page size. /// 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)] - public async Task List(CancellationToken cancellationToken) + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) { - var query = DatabaseContext - .ChatBots - .AsQueryable() - .Where(x => x.InstanceId == Instance.Id) - .Include(x => x.Channels); - - var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false); - var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0; - - if (!connectionStrings) - foreach (var I in results) - I.ConnectionString = null; - - return Json(results.Select(x => x.ToApi())); + return Paginated( + () => Task.FromResult( + new PaginatableResult( + DatabaseContext + .ChatBots + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .Include(x => x.Channels))), + chatBot => + { + if (connectionStrings) + chatBot.ConnectionString = null; + }, + page, + pageSize, + cancellationToken); } /// diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 3d82b23fe8..1e07d538ce 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -172,54 +171,75 @@ namespace Tgstation.Server.Host.Controllers /// Get the contents of a directory at a /// /// The path of the directory to get + /// The current page. + /// The page size. /// The for the operation /// A resulting in the for the operation /// Directory listed successfully.> /// Directory does not currently exist. [HttpGet(Routes.List + "/{*directoryPath}")] [TgsAuthorize(ConfigurationRights.List)] - [ProducesResponseType(typeof(IReadOnlyList), 200)] + [ProducesResponseType(typeof(Paginated), 200)] [ProducesResponseType(typeof(ErrorMessage), 410)] - public async Task Directory(string directoryPath, CancellationToken cancellationToken) - { - if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity)) - return Forbid(); + public Task Directory( + string directoryPath, + [FromQuery] int? page, + [FromQuery] int? pageSize, + CancellationToken cancellationToken) + => Paginated( + async () => + { + if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity)) + return new PaginatableResult( + Forbid()); - try - { - return await WithComponentInstance( - async instance => + try { - var result = await instance - .Configuration - .ListDirectory(directoryPath, systemIdentity, cancellationToken) - .ConfigureAwait(false); - if (result == null) - return Gone(); + return new PaginatableResult( + await WithComponentInstance( + async instance => + { + var result = await instance + .Configuration + .ListDirectory(directoryPath, systemIdentity, cancellationToken) + .ConfigureAwait(false); + if (result == null) + return Gone(); - return Json(result); - }) - .ConfigureAwait(false); - } - catch (NotImplementedException) - { - return RequiresPosixSystemIdentity(); - } - catch (UnauthorizedAccessException) - { - return Forbid(); - } - } + return Json(result); + }) + .ConfigureAwait(false)); + } + catch (NotImplementedException) + { + return new PaginatableResult( + RequiresPosixSystemIdentity()); + } + catch (UnauthorizedAccessException) + { + return new PaginatableResult( + Forbid()); + } + }, + null, + page, + pageSize, + cancellationToken); /// /// Get the contents of the root configuration directory. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the for the operation. [HttpGet(Routes.List)] [TgsAuthorize(ConfigurationRights.List)] - [ProducesResponseType(typeof(IReadOnlyList), 200)] - public Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken); + [ProducesResponseType(typeof(Paginated), 200)] + public Task List( + [FromQuery] int? page, + [FromQuery] int? pageSize, + CancellationToken cancellationToken) => Directory(null, page, pageSize, cancellationToken); /// /// Create a configuration directory. diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index b1bc9b6432..3ba5e89f1a 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -95,43 +94,53 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessage), 404)] public async Task GetId(long id, CancellationToken cancellationToken) { - var compileJob = await DatabaseContext - .CompileJobs - .AsQueryable() - .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id) - .Include(x => x.Job).ThenInclude(x => x.StartedBy) - .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) - .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) + var compileJob = await BaseCompileJobsQuery() + .Where(x => x.Id == id) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (compileJob == default) return NotFound(); return Json(compileJob.ToApi()); } + /// + /// Base query for pulling in all required fields. + /// + /// An of with all the inclusions. + IQueryable BaseCompileJobsQuery() => DatabaseContext + .CompileJobs + .AsQueryable() + .Include(x => x.Job) + .ThenInclude(x => x.StartedBy) + .Include(x => x.RevisionInformation) + .ThenInclude(x => x.PrimaryTestMerge) + .ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation) + .ThenInclude(x => x.ActiveTestMerges) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy) + .Where(x => x.Job.Instance.Id == Instance.Id); + /// /// List all s for the instance. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the request. /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(DreamMakerRights.CompileJobs)] - [ProducesResponseType(typeof(List), 200)] - public async Task List(CancellationToken cancellationToken) - { - var compileJobs = await DatabaseContext - .CompileJobs - .AsQueryable() - .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); - } + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + () => Task.FromResult( + new PaginatableResult( + BaseCompileJobsQuery() + .OrderByDescending(x => x.Job.StoppedAt))), + null, + page, + pageSize, + cancellationToken); /// /// Begin deploying repository code. diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index cf475e6836..5b2467380a 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -586,13 +586,18 @@ namespace Tgstation.Server.Host.Controllers /// /// List s. /// + /// The current page. + /// The page size. /// 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)] - public async Task List(CancellationToken cancellationToken) + [ProducesResponseType(typeof(Paginated), 200)] + public async Task List( + [FromQuery] int? page, + [FromQuery] int? pageSize, + CancellationToken cancellationToken) { IQueryable GetBaseQuery() { @@ -622,21 +627,25 @@ namespace Tgstation.Server.Host.Controllers .ToListAsync(cancellationToken) .ConfigureAwait(false); - var instances = await GetBaseQuery() - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - var needsUpdate = false; - foreach (var instance in instances) - needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance); + var result = await Paginated( + () => Task.FromResult( + new PaginatableResult( + GetBaseQuery())), + instance => + { + needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance); + instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id).ToApi(); + }, + page, + pageSize, + cancellationToken) + .ConfigureAwait(false); if (needsUpdate) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - var apis = instances.Select(x => x.ToApi()); - foreach(var I in moveJobs) - apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible - return Json(apis); + return result; } /// diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 34586ea5cf..ca27e446b8 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -1,8 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -147,23 +146,27 @@ namespace Tgstation.Server.Host.Controllers /// /// Lists s for the instance. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the request. /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(InstanceUserRights.ReadUsers)] - [ProducesResponseType(typeof(IEnumerable), 200)] - public async Task List(CancellationToken cancellationToken) - { - var users = await DatabaseContext - .Instances - .AsQueryable() - .Where(x => x.Id == Instance.Id) - .SelectMany(x => x.InstanceUsers) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - return Json(users.Select(x => x.ToApi())); - } + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + () => Task.FromResult( + new PaginatableResult( + DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .SelectMany(x => x.InstanceUsers))), + null, + page, + pageSize, + cancellationToken); /// /// Gets a specific . diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 6347617699..62f657d9e3 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -1,8 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -53,50 +52,54 @@ namespace Tgstation.Server.Host.Controllers /// /// Get active s for the instance. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the request. /// Retrieved active s successfully. [HttpGet] [TgsAuthorize] - [ProducesResponseType(typeof(IEnumerable), 200)] - public async Task Read(CancellationToken cancellationToken) - { - var result = await DatabaseContext - .Jobs - .AsQueryable() - .Include(x => x.StartedBy) - .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())); - } + [ProducesResponseType(typeof(Paginated), 200)] + public Task Read([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + () => Task.FromResult( + new PaginatableResult( + DatabaseContext + .Jobs + .AsQueryable() + .Include(x => x.StartedBy) + .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue) + .OrderByDescending(x => x.StartedAt))), + null, + page, + pageSize, + cancellationToken); /// /// List all s for the instance in reverse creation order. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the request. /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize] - [ProducesResponseType(typeof(List), 200)] - public async Task List(CancellationToken cancellationToken) - { - // you KNOW this will need pagination eventually right? - var jobs = await DatabaseContext - .Jobs - .AsQueryable() - .Where(x => x.Instance.Id == Instance.Id) - .OrderByDescending(x => x.StartedAt) - .Select(x => new EntityId - { - Id = x.Id - }) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - return Json(jobs); - } + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + () => Task.FromResult( + new PaginatableResult( + DatabaseContext + .Jobs + .AsQueryable() + .Include(x => x.StartedBy) + .Where(x => x.Instance.Id == Instance.Id) + .OrderByDescending(x => x.StartedAt))), + null, + page, + pageSize, + cancellationToken); /// /// Cancel a running . diff --git a/src/Tgstation.Server.Host/Controllers/PaginatableResult.cs b/src/Tgstation.Server.Host/Controllers/PaginatableResult.cs new file mode 100644 index 0000000000..4efdb614a5 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/PaginatableResult.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// Helper for returning paginated models. + /// + /// The of model intended to be returned. + public sealed class PaginatableResult + { + /// + /// The results. + /// + public IQueryable Results { get; } + + /// + /// An to return immediately. + /// + public IActionResult EarlyOut { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public PaginatableResult(IQueryable results) + { + Results = results ?? throw new ArgumentNullException(nameof(results)); + } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public PaginatableResult(IActionResult earlyOut) + { + EarlyOut = earlyOut ?? throw new ArgumentNullException(nameof(earlyOut)); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 3b5a8c5a9d..673a322cb5 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -298,23 +298,28 @@ namespace Tgstation.Server.Host.Controllers /// /// List all s in the server. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the operation. /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(AdministrationRights.ReadUsers)] - [ProducesResponseType(typeof(IEnumerable), 200)] - public async Task List(CancellationToken cancellationToken) - { - var users = await DatabaseContext - .Users - .AsQueryable() - .Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) - .Include(x => x.CreatedBy) - .Include(x => x.OAuthConnections) - .ToListAsync(cancellationToken).ConfigureAwait(false); - return Json(users.Select(x => x.ToApi(true))); - } + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + () => Task.FromResult( + new PaginatableResult( + DatabaseContext + .Users + .AsQueryable() + .Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections))), + null, + page, + pageSize, + cancellationToken); /// /// Get a specific . diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index 67d2dccc61..47646b93b9 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -164,6 +164,9 @@ namespace Tgstation.Server.Host.Core if (type == typeof(Api.Models.Internal.User)) return "ShallowUser"; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Paginated<>)) + return $"Paginated{type.GenericTypeArguments.First().Name}"; + return type.Name; }); @@ -241,7 +244,7 @@ namespace Tgstation.Server.Host.Core }; if (typeof(InstanceRequiredController).IsAssignableFrom(context.MethodInfo.DeclaringType)) - operation.Parameters.Add(new OpenApiParameter + operation.Parameters.Insert(0, new OpenApiParameter { Reference = new OpenApiReference { diff --git a/src/Tgstation.Server.Host/Models/ChatBot.cs b/src/Tgstation.Server.Host/Models/ChatBot.cs index e3565be4e8..f190f2f043 100644 --- a/src/Tgstation.Server.Host/Models/ChatBot.cs +++ b/src/Tgstation.Server.Host/Models/ChatBot.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; namespace Tgstation.Server.Host.Models { /// - public sealed class ChatBot : Api.Models.Internal.ChatBot + public sealed class ChatBot : Api.Models.Internal.ChatBot, IApiTransformable { /// /// Default for . @@ -28,10 +28,7 @@ namespace Tgstation.Server.Host.Models /// public ICollection Channels { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.ChatBot ToApi() => new Api.Models.ChatBot { Channels = Channels.Select(x => x.ToApi()).ToList(), diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs index d4b7bca702..e92eb59884 100644 --- a/src/Tgstation.Server.Host/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs @@ -1,7 +1,7 @@ -namespace Tgstation.Server.Host.Models +namespace Tgstation.Server.Host.Models { /// - public sealed class ChatChannel : Api.Models.ChatChannel + public sealed class ChatChannel : Api.Models.ChatChannel, IApiTransformable { /// /// The row Id @@ -18,10 +18,7 @@ /// public ChatBot ChatSettings { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.ChatChannel ToApi() => new Api.Models.ChatChannel { DiscordChannelId = DiscordChannelId, diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 58949d3251..5a45c0a85d 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { /// - public sealed class CompileJob : Api.Models.Internal.CompileJob + public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformable { /// /// See @@ -78,10 +78,7 @@ namespace Tgstation.Server.Host.Models } } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.CompileJob ToApi() => new Api.Models.CompileJob { DirectoryName = DirectoryName, diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 7ae2ef5c79..eb3ed1bb19 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -1,9 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { /// - public sealed class DreamMakerSettings : Api.Models.DreamMaker + public sealed class DreamMakerSettings : Api.Models.DreamMaker, IApiTransformable { /// /// The row Id @@ -21,10 +21,7 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.DreamMaker ToApi() => new Api.Models.DreamMaker { ProjectName = ProjectName, diff --git a/src/Tgstation.Server.Host/Models/IApiTransformable.cs b/src/Tgstation.Server.Host/Models/IApiTransformable.cs new file mode 100644 index 0000000000..282e0d2442 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/IApiTransformable.cs @@ -0,0 +1,15 @@ +namespace Tgstation.Server.Host.Models +{ + /// + /// Represents a host-side model that may be transformed into a . + /// + /// The API form of the model. + public interface IApiTransformable + { + /// + /// Convert the to it's . + /// + /// A new based on the . + TApiModel ToApi(); + } +} diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 1827a9511d..d22c5f70b3 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Tgstation.Server.Host.Models { /// /// Represents an in the database /// - public sealed class Instance : Api.Models.Instance + public sealed class Instance : Api.Models.Instance, IApiTransformable { /// /// Default for . @@ -47,10 +47,7 @@ namespace Tgstation.Server.Host.Models /// public ICollection Jobs { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.Instance ToApi() => new Api.Models.Instance { AutoUpdateInterval = AutoUpdateInterval, @@ -59,7 +56,8 @@ namespace Tgstation.Server.Host.Models Name = Name, Path = Path, Online = Online, - ChatBotLimit = ChatBotLimit + ChatBotLimit = ChatBotLimit, + MoveJob = MoveJob, }; } } diff --git a/src/Tgstation.Server.Host/Models/InstanceUser.cs b/src/Tgstation.Server.Host/Models/InstanceUser.cs index 2aa74e5042..7b7aea0d4c 100644 --- a/src/Tgstation.Server.Host/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Host/Models/InstanceUser.cs @@ -1,9 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { /// - public sealed class InstanceUser : Api.Models.InstanceUser + public sealed class InstanceUser : Api.Models.InstanceUser, IApiTransformable { /// /// The row Id @@ -21,10 +21,7 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.InstanceUser ToApi() => new Api.Models.InstanceUser { ByondRights = ByondRights, diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index d2ba919450..861981efda 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -4,7 +4,7 @@ namespace Tgstation.Server.Host.Models { /// #pragma warning disable CA1724 // naming conflict with gitlab package - public sealed class Job : Api.Models.Internal.Job + public sealed class Job : Api.Models.Internal.Job, IApiTransformable #pragma warning restore CA1724 { /// @@ -24,10 +24,7 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.Job ToApi() => new Api.Models.Job { Id = Id, diff --git a/src/Tgstation.Server.Host/Models/OAuthConnection.cs b/src/Tgstation.Server.Host/Models/OAuthConnection.cs index 590849a538..441ac0e312 100644 --- a/src/Tgstation.Server.Host/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Host/Models/OAuthConnection.cs @@ -1,7 +1,7 @@ namespace Tgstation.Server.Host.Models { /// - public sealed class OAuthConnection : Api.Models.OAuthConnection + public sealed class OAuthConnection : Api.Models.OAuthConnection, IApiTransformable { /// /// The row Id. @@ -13,10 +13,7 @@ namespace Tgstation.Server.Host.Models /// public User User { get; set; } - /// - /// Convert the to it's API form. - /// - /// A new . + /// public Api.Models.OAuthConnection ToApi() => new Api.Models.OAuthConnection { Provider = Provider, diff --git a/src/Tgstation.Server.Host/Models/RepositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositorySettings.cs index 6cc2cf436e..25fb37edd7 100644 --- a/src/Tgstation.Server.Host/Models/RepositorySettings.cs +++ b/src/Tgstation.Server.Host/Models/RepositorySettings.cs @@ -4,7 +4,7 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { /// - public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings + public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings, IApiTransformable { /// /// The row Id @@ -22,10 +22,7 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Repository ToApi() => new Repository { // AccessToken = AccessToken, // never show this diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index 6c28d3864a..3b99e8c26e 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; namespace Tgstation.Server.Host.Models { /// - public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation + public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, IApiTransformable { /// /// The row Id @@ -38,10 +38,7 @@ namespace Tgstation.Server.Host.Models /// public ICollection CompileJobs { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation { CommitSha = CommitSha, diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index 6e9fd81880..3ef25c0e91 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { /// - public sealed class TestMerge : Api.Models.Internal.TestMerge + public sealed class TestMerge : Api.Models.Internal.TestMerge, IApiTransformable { /// /// See @@ -28,10 +28,7 @@ namespace Tgstation.Server.Host.Models /// public ICollection RevisonInformations { get; set; } - /// - /// Convert the to it's API form - /// - /// A new + /// public Api.Models.TestMerge ToApi() => new Api.Models.TestMerge { Author = Author, diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 263112f193..1b896a4284 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Tgstation.Server.Host.Models { /// - public sealed class User : Api.Models.Internal.User + public sealed class User : Api.Models.Internal.User, IApiTransformable { /// /// Username used when creating jobs automatically. @@ -88,5 +88,8 @@ namespace Tgstation.Server.Host.Models /// If rights and system identifier should be shown /// A new public Api.Models.User ToApi(bool showDetails) => ToApi(true, showDetails); + + /// + public Api.Models.User ToApi() => ToApi(true); } } diff --git a/tests/Tgstation.Server.Tests/AdministrationTest.cs b/tests/Tgstation.Server.Tests/AdministrationTest.cs index cddd0b0aa4..38b3f81224 100644 --- a/tests/Tgstation.Server.Tests/AdministrationTest.cs +++ b/tests/Tgstation.Server.Tests/AdministrationTest.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Tests async Task TestLogs(CancellationToken cancellationToken) { - var logs = await client.ListLogs(cancellationToken); + var logs = await client.ListLogs(null, cancellationToken); Assert.AreNotEqual(0, logs.Count); var logFile = logs.First(); Assert.IsNotNull(logFile); diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs index 11a24484e4..c3eb6e84fe 100644 --- a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs @@ -75,7 +75,7 @@ namespace Tgstation.Server.Tests.Instance async Task TestNoVersion(CancellationToken cancellationToken) { - var allVersionsTask = byondClient.InstalledVersions(cancellationToken); + var allVersionsTask = byondClient.InstalledVersions(null, cancellationToken); var currentShit = await byondClient.ActiveVersion(cancellationToken).ConfigureAwait(false); Assert.IsNotNull(currentShit); Assert.IsNull(currentShit.InstallJob); diff --git a/tests/Tgstation.Server.Tests/Instance/ChatTest.cs b/tests/Tgstation.Server.Tests/Instance/ChatTest.cs index f41c1016d5..ef6b8837e7 100644 --- a/tests/Tgstation.Server.Tests/Instance/ChatTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ChatTest.cs @@ -52,7 +52,7 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(csb.ToString(), firstBot.ConnectionString); Assert.AreNotEqual(0, firstBot.Id); - var bots = await chatClient.List(cancellationToken); + var bots = await chatClient.List(null, cancellationToken); Assert.AreEqual(firstBot.Id, bots.First(x => x.Provider.Value == ChatProvider.Irc).Id); var retrievedBot = await chatClient.GetId(firstBot, cancellationToken); @@ -115,7 +115,7 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(csb.ToString(), firstBot.ConnectionString); Assert.AreNotEqual(0, firstBot.Id); - var bots = await chatClient.List(cancellationToken); + var bots = await chatClient.List(null, cancellationToken); Assert.AreEqual(firstBot.Id, bots.First(x => x.Provider.Value == ChatProvider.Discord).Id); var retrievedBot = await chatClient.GetId(firstBot, cancellationToken); @@ -154,13 +154,13 @@ namespace Tgstation.Server.Tests.Instance public async Task RunPostTest(CancellationToken cancellationToken) { - var activeBots = await chatClient.List(cancellationToken); + var activeBots = await chatClient.List(null, cancellationToken); Assert.AreEqual(2, activeBots.Count); await Task.WhenAll(activeBots.Select(bot => chatClient.Delete(bot, cancellationToken))); - var nowBots = await chatClient.List(cancellationToken); + var nowBots = await chatClient.List(null, cancellationToken); Assert.AreEqual(0, nowBots.Count); } @@ -173,7 +173,7 @@ namespace Tgstation.Server.Tests.Instance Provider = ChatProvider.Irc }, cancellationToken), ErrorCode.ChatBotMax); - var bots = await chatClient.List(cancellationToken); + var bots = await chatClient.List(null, cancellationToken); var discordBot = bots.First(bot => bot.Provider.Value == ChatProvider.Discord); // We limited chat bots and channels to 1 and 2 respectively, try violating them diff --git a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs index fbab7dec20..ad2c585d9d 100644 --- a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; using System.Linq; @@ -137,7 +137,7 @@ namespace Tgstation.Server.Tests public async Task RunPostTest(CancellationToken cancellationToken) { - var instances = await instanceManagerClient.List(cancellationToken); + var instances = await instanceManagerClient.List(null, cancellationToken); var firstTest = instances.Single(x => x.Name == TestInstanceName); var instanceClient = instanceManagerClient.CreateClient(firstTest); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index d0d50482eb..4e6ed50068 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -320,10 +320,10 @@ namespace Tgstation.Server.Tests { var instanceClient = adminClient.Instances.CreateClient(instance); - var jobs = await instanceClient.Jobs.ListActive(cancellationToken); + var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken); if (!jobs.Any()) { - var entities = await instanceClient.Jobs.List(cancellationToken); + var entities = await instanceClient.Jobs.List(null, cancellationToken); var getTasks = entities .Select(e => instanceClient.Jobs.GetId(e, cancellationToken)) .ToList(); @@ -363,10 +363,10 @@ namespace Tgstation.Server.Tests { var instanceClient = adminClient.Instances.CreateClient(instance); - var jobs = await instanceClient.Jobs.ListActive(cancellationToken); + var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken); if (!jobs.Any()) { - var entities = await instanceClient.Jobs.List(cancellationToken); + var entities = await instanceClient.Jobs.List(null, cancellationToken); var getTasks = entities .Select(e => instanceClient.Jobs.GetId(e, cancellationToken)) .ToList(); diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 0110d8a4dc..0cdb21ebc2 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -25,6 +25,8 @@ namespace Tgstation.Server.Tests BasicTests(cancellationToken), TestCreateSysUser(cancellationToken), TestSpamCreation(cancellationToken)).ConfigureAwait(false); + + await TestPagination(cancellationToken); } async Task BasicTests(CancellationToken cancellationToken) @@ -41,7 +43,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual("TGS", systemUser.Name); Assert.AreEqual(false, systemUser.Enabled); - var users = await client.List(cancellationToken); + var users = await client.List(null, cancellationToken); Assert.IsTrue(users.Count > 0); Assert.IsFalse(users.Any(x => x.Id == systemUser.Id)); @@ -133,5 +135,55 @@ namespace Tgstation.Server.Tests Assert.AreEqual(RepeatCount, tasks.Select(task => task.Result.Id).Distinct().Count(), "Did not receive expected number of unique user IDs!"); } + + async Task TestPagination(CancellationToken cancellationToken) + { + // we test pagination here b/c it's the only spot we have a decent amount of entities + var nullSettings = await client.List(null, cancellationToken); + var emptySettings = await client.List( + new PaginationSettings + { + }, cancellationToken); + + Assert.AreEqual(nullSettings.Count, emptySettings.Count); + Assert.IsTrue(nullSettings.All(x => emptySettings.SingleOrDefault(y => x.Id == y.Id) != null)); + + await ApiAssert.ThrowsException(() => client.List( + new PaginationSettings + { + PageSize = -2143 + }, cancellationToken), ErrorCode.ApiInvalidPageOrPageSize); + await ApiAssert.ThrowsException(() => client.List( + new PaginationSettings + { + PageSize = Int32.MaxValue + }, cancellationToken), ErrorCode.ApiPageTooLarge); + + await client.List( + new PaginationSettings + { + PageSize = 50 + }, + cancellationToken); + + var skipped = await client.List(new PaginationSettings + { + Offset = 50, + RetrieveCount = 5 + }, cancellationToken); + Assert.AreEqual(5, skipped.Count); + + var allAfterSkipped = await client.List(new PaginationSettings + { + Offset = 50, + }, cancellationToken); + Assert.IsTrue(5 < allAfterSkipped.Count); + + var limited = await client.List(new PaginationSettings + { + RetrieveCount = 12, + }, cancellationToken); + Assert.AreEqual(12, limited.Count); + } } } From 30113fc59f37a5d126f19c65df68665d3a7d0065 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 17 Dec 2020 23:56:20 -0500 Subject: [PATCH 074/154] Fix issue listing instances without move jobs --- src/Tgstation.Server.Host/Controllers/InstanceController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 5b2467380a..64f5a1b76b 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -635,7 +635,7 @@ namespace Tgstation.Server.Host.Controllers instance => { needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance); - instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id).ToApi(); + instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id)?.ToApi(); }, page, pageSize, From 1058c9662774f953613b8d9f61b9028154d359a3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 00:33:39 -0500 Subject: [PATCH 075/154] Finish using up deprecated ErrorCodes --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 26 +++++--------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index f29da9363c..2f6821868d 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -286,11 +286,10 @@ namespace Tgstation.Server.Api.Models ChatBotProviderMissing, /// - /// Attempted to update a or without its ID. + /// Tried to edit membership using . /// - [Description("Missing user ID!")] - [Obsolete("Deprecated in favor of code 2", true)] - UserMissingId, + [Description("The " + Routes.UserGroup + " endpoint cannot edit group members. Please update each member user individually.")] + UserGroupControllerCantEditMembers, /// /// Attempted to add a when at or above the or it was set to something lower than the existing amount of . @@ -335,11 +334,10 @@ namespace Tgstation.Server.Api.Models DreamMakerInvalidValidation, /// - /// DMAPI validation timeout. + /// Tried to remove the last for a passwordless . /// - [Description("The DreamDaemon startup timeout was hit before the DMAPI validated!")] - [Obsolete("Deprecated in favor of error code 52", true)] - DreamMakerValidationTimeout, + [Description("This user is passwordless and removing their oAuthConnections would leave them with no authentication method!")] + CannotRemoveLastAuthenticationOption, /// /// No .dme could be found for deployment. @@ -610,17 +608,5 @@ namespace Tgstation.Server.Api.Models /// [Description("Cannot delete the user group as it is not empty!")] UserGroupNotEmpty, - - /// - /// Tried to edit membership using . - /// - [Description("The " + Routes.UserGroup + " endpoint cannot edit group members. Please update each member user individually.")] - UserGroupControllerCantEditMembers, - - /// - /// Tried to remove the last for a passwordless . - /// - [Description("This user is passwordless and removing their oAuthConnections would leave them with no authentication method!")] - CannotRemoveLastAuthenticationOption, } } From e474810cc0f8d6366ca3fa9a672dd9ad72d72e88 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:05:59 -0500 Subject: [PATCH 076/154] Use bash when setting TGS4_GITHUB_REF for push --- .github/workflows/ci-suite.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 6d20bbfda5..3502345ed9 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -352,6 +352,7 @@ jobs: - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} + shell: bash run: | TEMP_GITHUB_REF="${{ github.event.ref }}" echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV From 7c13785969aba530aad3d2234fd6e27f91d73c0b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:14:54 -0500 Subject: [PATCH 077/154] Properly page UserGroups --- .../IUserGroupsClient.cs | 3 +- .../UserGroupsClient.cs | 22 ++++++-------- .../Controllers/UserGroupController.cs | 29 ++++++++++--------- src/Tgstation.Server.Host/Models/UserGroup.cs | 5 +++- tests/Tgstation.Server.Tests/UsersTest.cs | 4 +-- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Tgstation.Server.Client/IUserGroupsClient.cs b/src/Tgstation.Server.Client/IUserGroupsClient.cs index 8072cf0dc1..02141cad54 100644 --- a/src/Tgstation.Server.Client/IUserGroupsClient.cs +++ b/src/Tgstation.Server.Client/IUserGroupsClient.cs @@ -21,9 +21,10 @@ namespace Tgstation.Server.Client /// /// List all s. /// + /// The optional for the operation. /// The for the operation. /// A resulting in a of all s. - Task> List(CancellationToken cancellationToken); + Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// /// Create a new . diff --git a/src/Tgstation.Server.Client/UserGroupsClient.cs b/src/Tgstation.Server.Client/UserGroupsClient.cs index 72b937f283..dadb68c7b4 100644 --- a/src/Tgstation.Server.Client/UserGroupsClient.cs +++ b/src/Tgstation.Server.Client/UserGroupsClient.cs @@ -8,35 +8,31 @@ using Tgstation.Server.Api.Models; namespace Tgstation.Server.Client { /// - sealed class UserGroupsClient : IUserGroupsClient + sealed class UserGroupsClient : PaginatedClient, IUserGroupsClient { - /// - /// The for the . - /// - readonly IApiClient apiClient; - /// /// Initializes a new instance of the . /// - /// The value of . + /// The for the . public UserGroupsClient(IApiClient apiClient) + : base(apiClient) { - this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); } /// - public Task Create(UserGroup group, CancellationToken cancellationToken) => apiClient.Create(Routes.UserGroup, group ?? throw new ArgumentNullException(nameof(group)), cancellationToken); + public Task Create(UserGroup group, CancellationToken cancellationToken) => ApiClient.Create(Routes.UserGroup, group ?? throw new ArgumentNullException(nameof(group)), cancellationToken); /// - public Task GetId(EntityId group, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.UserGroup, group?.Id ?? throw new ArgumentNullException(nameof(group))), cancellationToken); + public Task GetId(EntityId group, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.UserGroup, group?.Id ?? throw new ArgumentNullException(nameof(group))), cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.UserGroup), cancellationToken); + public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.UserGroup), null, cancellationToken); /// - public Task Update(UserGroup group, CancellationToken cancellationToken) => apiClient.Update(Routes.UserGroup, group ?? throw new ArgumentNullException(nameof(group)), cancellationToken); + public Task Update(UserGroup group, CancellationToken cancellationToken) => ApiClient.Update(Routes.UserGroup, group ?? throw new ArgumentNullException(nameof(group)), cancellationToken); /// - public Task Delete(EntityId group, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.UserGroup, group?.Id ?? throw new ArgumentNullException(nameof(group))), cancellationToken); + public Task Delete(EntityId group, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.UserGroup, group?.Id ?? throw new ArgumentNullException(nameof(group))), cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index ebec01d548..c1be2818ad 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -153,23 +152,27 @@ namespace Tgstation.Server.Host.Controllers /// /// Lists s for the instance. /// + /// The current page. + /// The page size. /// The for the operation. /// A resulting in the of the request. /// Retrieved s successfully. [HttpGet(Routes.List)] [TgsAuthorize(InstancePermissionSetRights.Read)] - [ProducesResponseType(typeof(IEnumerable), 200)] - public async Task List(CancellationToken cancellationToken) - { - var users = await DatabaseContext - .Groups - .AsQueryable() - .Include(x => x.Users) - .Include(x => x.PermissionSet) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - return Json(users.Select(x => x.ToApi(true))); - } + [ProducesResponseType(typeof(Paginated), 200)] + public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => Paginated( + () => Task.FromResult( + new PaginatableResult( + DatabaseContext + .Groups + .AsQueryable() + .Include(x => x.Users) + .Include(x => x.PermissionSet))), + null, + page, + pageSize, + cancellationToken); /// /// Delete an . diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index c81fae1708..52b8e8a4ff 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Tgstation.Server.Host.Models { /// - public sealed class UserGroup : Api.Models.Internal.UserGroup + public sealed class UserGroup : Api.Models.Internal.UserGroup, IApiTransformable { /// /// The the has. @@ -32,5 +32,8 @@ namespace Tgstation.Server.Host.Models ? Users?.Select(x => x.ToApi(false)).OfType().ToList() ?? new List() : null, }; + + /// + public Api.Models.UserGroup ToApi() => ToApi(true); } } diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 5d3c93b72b..df9ee17c58 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -117,7 +117,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(AdministrationRights.None, group2.PermissionSet.AdministrationRights); Assert.AreEqual(InstanceManagerRights.List, group2.PermissionSet.InstanceManagerRights); - var groups = await serverClient.Groups.List(cancellationToken); + var groups = await serverClient.Groups.List(null, cancellationToken); Assert.AreEqual(2, groups.Count); foreach (var igroup in groups) @@ -128,7 +128,7 @@ namespace Tgstation.Server.Tests await serverClient.Groups.Delete(group2, cancellationToken); - groups = await serverClient.Groups.List(cancellationToken); + groups = await serverClient.Groups.List(null, cancellationToken); Assert.AreEqual(1, groups.Count); group.PermissionSet.InstanceManagerRights = RightsHelper.AllRights(); From 89b1ff6402cc1fbc9401cbff9401c017e6ece5c4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:19:55 -0500 Subject: [PATCH 078/154] Use SendMessage not QueueMessage for restarts --- src/Tgstation.Server.Host/Components/Chat/ChatManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 93e8971229..5380171269 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -828,8 +828,7 @@ namespace Tgstation.Server.Host.Components.Chat lock (mappedChannels) // so it doesn't change while we're using it wdChannels = mappedChannels.Select(x => x.Key).ToList(); - QueueMessage(message, wdChannels); - return Task.CompletedTask; + return SendMessage(message, wdChannels, cancellationToken); } /// From b061ed3d1ecf5ef6201d3e7ca8cd173eeb7563ef Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:26:00 -0500 Subject: [PATCH 079/154] Force SessionController.ProcessBridgeRequest async --- .../Components/Session/SessionController.cs | 90 +++++++++---------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index ec9b1301cc..390d8e3612 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -349,11 +349,16 @@ namespace Tgstation.Server.Host.Components.Session } /// - public Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); + // I don't fully understand why, but it seems to be REALLY important that this function remains async + // I'm sure if i think about it hard enough I'll realize there's some race condition between this processing + // and the deployment process, but this has been blocking me all week and I'm tired of giving it energy + await Task.Yield(); + using (LogContext.PushProperty("Instance", metadata.Id)) { logger.LogTrace("Handling bridge request..."); @@ -364,32 +369,28 @@ namespace Tgstation.Server.Host.Components.Session { case BridgeCommandType.ChatSend: if (parameters.ChatMessage == null) - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Missing chatMessage field!" - }); + return new BridgeResponse + { + ErrorMessage = "Missing chatMessage field!" + }; if (parameters.ChatMessage.ChannelIds == null) - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Missing channelIds field in chatMessage!" - }); + return new BridgeResponse + { + ErrorMessage = "Missing channelIds field in chatMessage!" + }; if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _))) - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Invalid channelIds in chatMessage!" - }); + return new BridgeResponse + { + ErrorMessage = "Invalid channelIds in chatMessage!" + }; if (parameters.ChatMessage.Text == null) - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Missing message field in chatMessage!" - }); + return new BridgeResponse + { + ErrorMessage = "Missing message field in chatMessage!" + }; chat.QueueMessage( parameters.ChatMessage.Text, @@ -412,11 +413,10 @@ namespace Tgstation.Server.Host.Components.Session { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Missing stringified port as data parameter!" - }); + return new BridgeResponse + { + ErrorMessage = "Missing stringified port as data parameter!" + }; } var currentPort = parameters.CurrentPort.Value; @@ -443,21 +443,19 @@ namespace Tgstation.Server.Host.Components.Session case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; if (parameters.Version == null) - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Missing dmApiVersion field!" - }); + return new BridgeResponse + { + ErrorMessage = "Missing dmApiVersion field!" + }; DMApiVersion = parameters.Version; if (DMApiVersion.Major != DMApiConstants.Version.Major) { apiValidationStatus = ApiValidationStatus.Incompatible; - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Incompatible dmApiVersion!" - }); + return new BridgeResponse + { + ErrorMessage = "Incompatible dmApiVersion!" + }; } switch (parameters.MinimumSecurityLevel) @@ -472,17 +470,15 @@ namespace Tgstation.Server.Host.Components.Session apiValidationStatus = ApiValidationStatus.RequiresTrusted; break; case null: - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Missing minimumSecurityLevel field!" - }); + return new BridgeResponse + { + ErrorMessage = "Missing minimumSecurityLevel field!" + }; default: - return Task.FromResult( - new BridgeResponse - { - ErrorMessage = "Invalid minimumSecurityLevel!" - }); + return new BridgeResponse + { + ErrorMessage = "Invalid minimumSecurityLevel!" + }; } logger.LogTrace("ApiValidationStatus set to {0}", apiValidationStatus); @@ -519,7 +515,7 @@ namespace Tgstation.Server.Host.Components.Session break; } - return Task.FromResult(response); + return response; } } From 136f5c9d9116a3613eb9722f3e8b0f1ff9eea49a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:29:42 -0500 Subject: [PATCH 080/154] FUCKING FIX BRANCH CI FOR THE LAST TIME --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 3502345ed9..fdc09c7586 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -355,7 +355,7 @@ jobs: shell: bash run: | TEMP_GITHUB_REF="${{ github.event.ref }}" - echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV + echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $GITHUB_ENV - name: Run Integration Test run: | From d8573d818e86de0dcc07d10f0b34bcac8bd60898 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:32:49 -0500 Subject: [PATCH 081/154] Readd "NoService" specifier to Linux build configs --- .github/workflows/ci-suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index a6a8e8dc43..3a34442ea6 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -372,13 +372,13 @@ jobs: echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $GITHUB_ENV - name: Build - run: dotnet build -c ${{ matrix.configuration }} + run: dotnet build -c ${{ matrix.configuration }}NoService - name: Run Integration Test run: | cd tests/Tgstation.Server.Tests sleep 10 - dotnet test -c ${{ matrix.configuration }} --no-build -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults + dotnet test -c ${{ matrix.configuration }}NoService --no-build -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults - name: Store Code Coverage uses: actions/upload-artifact@v2 From 0336a61929652ceeb5c925b96a4612fb7af670c0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:35:04 -0500 Subject: [PATCH 082/154] Allow non-TGS deploys from dev --- .github/workflows/ci-suite.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 3a34442ea6..8cfdab57d3 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -609,7 +609,7 @@ jobs: name: Deploy HTTP API needs: [upload-code-coverage, validate-openapi-spec] runs-on: windows-latest - if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[APIDeploy]') + if: github.event_name == 'push' && contains(github.event.head_commit.message, '[APIDeploy]') steps: - name: Checkout uses: actions/checkout@v1 @@ -652,7 +652,7 @@ jobs: name: Deploy DreamMaker API needs: [upload-code-coverage, validate-openapi-spec] runs-on: windows-latest - if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[DMDeploy]') + if: github.event_name == 'push' && contains(github.event.head_commit.message, '[DMDeploy]') steps: - name: Checkout @@ -695,7 +695,7 @@ jobs: name: Deploy Nuget Packages needs: [upload-code-coverage, validate-openapi-spec] runs-on: ubuntu-latest - if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[NugetDeploy]') + if: github.event_name == 'push' && contains(github.event.head_commit.message, '[NugetDeploy]') steps: - name: Checkout uses: actions/checkout@v1 From 03bf84886a88c3958f005a194e985a828e58d896 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 01:50:06 -0500 Subject: [PATCH 083/154] Fix dotnet test commands - Explicit build was a painful mistake --- .github/workflows/ci-suite.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 8cfdab57d3..02156ff449 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -223,14 +223,11 @@ jobs: TEMP_GITHUB_REF="${{ github.event.ref }}" echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV - - name: Build - run: dotnet build -c ${{ matrix.configuration }} - - name: Run Integration Test run: | cd tests/Tgstation.Server.Tests Start-Sleep -Seconds 10 - dotnet test -c ${{ matrix.configuration }} --no-build "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults + dotnet test -c ${{ matrix.configuration }} -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults - name: Store Code Coverage uses: actions/upload-artifact@v2 @@ -371,14 +368,11 @@ jobs: TEMP_GITHUB_REF="${{ github.event.ref }}" echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Build - run: dotnet build -c ${{ matrix.configuration }}NoService - - name: Run Integration Test run: | cd tests/Tgstation.Server.Tests sleep 10 - dotnet test -c ${{ matrix.configuration }}NoService --no-build -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults + dotnet test -c ${{ matrix.configuration }}NoService -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults - name: Store Code Coverage uses: actions/upload-artifact@v2 From e3f33610679c36b2be6cefb6f18110fd9c7fd7fd Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 10:39:26 -0500 Subject: [PATCH 084/154] Fix Linux publishing --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 02156ff449..60f6cdabcc 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -386,7 +386,7 @@ jobs: cd src/Tgstation.Server.Host.Console dotnet publish -c ${{ matrix.configuration }} -o ../../Artifacts/Console cd ../Tgstation.Server.Host - dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/Console/lib/Default + dotnet publish -c ${{ matrix.configuration }}NoService --no-build -o ../../Artifacts/Console/lib/Default - name: Package Server Update Package if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }} From 6cf80ca65c5511d5212b25a1d6cf4a60faa39764 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 11:20:45 -0500 Subject: [PATCH 085/154] Check validation status instead of assuming not --- .../Components/Deployment/DreamMaker.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index c031f55a4d..4d9f1aa9b4 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -238,13 +238,13 @@ namespace Tgstation.Server.Host.Components.Deployment await controller.Lifetime.WithToken(cancellationToken).ConfigureAwait(false); if (!controller.Lifetime.IsCompleted) - { - if (requireValidate) - throw new JobException(ErrorCode.DreamMakerNeverValidated); await controller.DisposeAsync().ConfigureAwait(false); - } validationStatus = controller.ApiValidationStatus; + + if (requireValidate && validationStatus == ApiValidationStatus.NeverValidated) + throw new JobException(ErrorCode.DreamMakerNeverValidated); + logger.LogTrace("API validation status: {0}", validationStatus); job.DMApiVersion = controller.DMApiVersion; From b5dd8d018467fec9297a6443c706151e037ec9d9 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 12:11:27 -0500 Subject: [PATCH 086/154] Add yet another missing "NoService" config --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 60f6cdabcc..1baf050890 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -392,7 +392,7 @@ jobs: if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }} run: | cd src/Tgstation.Server.Host - dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/ServerUpdate + dotnet publish -c ${{ matrix.configuration }}NoService --no-build -o ../../Artifacts/ServerUpdate - name: Store Server Console if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }} From 6bd8f00f15e71d221acad59b5ed4ffdd363431e0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 14:23:37 -0500 Subject: [PATCH 087/154] Bump test BYOND version to 513.1536 --- .github/workflows/ci-suite.yml | 2 +- tests/Tgstation.Server.Tests/Instance/ByondTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 1baf050890..cb9188f4fc 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -24,7 +24,7 @@ jobs: name: Build DMAPI env: BYOND_MAJOR: 513 - BYOND_MINOR: 1527 + BYOND_MINOR: 1536 runs-on: ubuntu-latest steps: - name: Install x86 libc Dependencies diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs index c3eb6e84fe..4c50aab14f 100644 --- a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Tests.Instance { sealed class ByondTest : JobsRequiredTest { - public static readonly Version TestVersion = new Version(513, 1527); + public static readonly Version TestVersion = new Version(513, 1536); readonly IByondClient byondClient; From 3385c3aac884faba1fceb2812f1cbcac3d4bee65 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 14:24:08 -0500 Subject: [PATCH 088/154] I am done with this delayed bridge request shit --- .../Components/Session/SessionController.cs | 57 +++++-------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 390d8e3612..b064a63c73 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -349,15 +349,15 @@ namespace Tgstation.Server.Host.Components.Session } /// - public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); - // I don't fully understand why, but it seems to be REALLY important that this function remains async - // I'm sure if i think about it hard enough I'll realize there's some race condition between this processing - // and the deployment process, but this has been blocking me all week and I'm tired of giving it energy - await Task.Yield(); + static Task Error(string message) => Task.FromResult(new BridgeResponse + { + ErrorMessage = message + }); using (LogContext.PushProperty("Instance", metadata.Id)) { @@ -369,28 +369,16 @@ namespace Tgstation.Server.Host.Components.Session { case BridgeCommandType.ChatSend: if (parameters.ChatMessage == null) - return new BridgeResponse - { - ErrorMessage = "Missing chatMessage field!" - }; + return Error("Missing chatMessage field!"); if (parameters.ChatMessage.ChannelIds == null) - return new BridgeResponse - { - ErrorMessage = "Missing channelIds field in chatMessage!" - }; + return Error("Missing channelIds field in chatMessage!"); if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _))) - return new BridgeResponse - { - ErrorMessage = "Invalid channelIds in chatMessage!" - }; + return Error("Invalid channelIds in chatMessage!"); if (parameters.ChatMessage.Text == null) - return new BridgeResponse - { - ErrorMessage = "Missing message field in chatMessage!" - }; + return Error("Missing message field in chatMessage!"); chat.QueueMessage( parameters.ChatMessage.Text, @@ -413,10 +401,7 @@ namespace Tgstation.Server.Host.Components.Session { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); - return new BridgeResponse - { - ErrorMessage = "Missing stringified port as data parameter!" - }; + return Error("Missing stringified port as data parameter!"); } var currentPort = parameters.CurrentPort.Value; @@ -443,19 +428,13 @@ namespace Tgstation.Server.Host.Components.Session case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; if (parameters.Version == null) - return new BridgeResponse - { - ErrorMessage = "Missing dmApiVersion field!" - }; + return Error("Missing dmApiVersion field!"); DMApiVersion = parameters.Version; if (DMApiVersion.Major != DMApiConstants.Version.Major) { apiValidationStatus = ApiValidationStatus.Incompatible; - return new BridgeResponse - { - ErrorMessage = "Incompatible dmApiVersion!" - }; + return Error("Incompatible dmApiVersion!"); } switch (parameters.MinimumSecurityLevel) @@ -470,15 +449,9 @@ namespace Tgstation.Server.Host.Components.Session apiValidationStatus = ApiValidationStatus.RequiresTrusted; break; case null: - return new BridgeResponse - { - ErrorMessage = "Missing minimumSecurityLevel field!" - }; + return Error("Missing minimumSecurityLevel field!"); default: - return new BridgeResponse - { - ErrorMessage = "Invalid minimumSecurityLevel!" - }; + return Error("Invalid minimumSecurityLevel!"); } logger.LogTrace("ApiValidationStatus set to {0}", apiValidationStatus); @@ -515,7 +488,7 @@ namespace Tgstation.Server.Host.Components.Session break; } - return response; + return Task.FromResult(response); } } From b050f95744114d95343ec93b964f374e482966ae Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 14:53:39 -0500 Subject: [PATCH 089/154] Running out of options, I turn to dark magic - Set MicrosoftLogLevel to Debug for Linux integration tests --- .github/workflows/ci-suite.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index cb9188f4fc..fe108595ce 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -260,6 +260,8 @@ jobs: linux-integration-tests: name: Linux Integration Test needs: dmapi-build + env: + FileLogging__MicrosoftLogLevel: Debug services: # We start all dbs here so we can just code the stuff once postgres: image: postgres From 29ea9042808854a5046e348c0db1d6a4ad26bc84 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 18 Dec 2020 15:29:50 -0500 Subject: [PATCH 090/154] Remove git checkout as recommended for CodeQL --- .github/workflows/codeql-analysis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4110b68ad6..9c2ef53894 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,9 +27,6 @@ jobs: with: fetch-depth: 2 - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: From 0d5d70aff9204678399feb89381bdc902ce209a1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 19 Dec 2020 10:32:34 -0500 Subject: [PATCH 092/154] Trying a workaraound for dotnet bug https://github.com/actions/setup-dotnet/issues/155 --- .github/workflows/ci-suite.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index fe108595ce..bcf1d2ba62 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -166,6 +166,9 @@ jobs: - name: Checkout uses: actions/checkout@v1 + - name: Clean package cache as a temporary workaround for actions/setup-dotnet#155 + run: dotnet clean && dotnet nuget locals all --clear + - name: Build run: dotnet build -c ${{ matrix.configuration }} @@ -223,6 +226,9 @@ jobs: TEMP_GITHUB_REF="${{ github.event.ref }}" echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV + - name: Clean package cache as a temporary workaround for actions/setup-dotnet#155 + run: dotnet clean && dotnet nuget locals all --clear + - name: Run Integration Test run: | cd tests/Tgstation.Server.Tests From ff91354a11ea743d6d68c8b7b9a15fea1f405c3a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 19 Dec 2020 12:10:15 -0500 Subject: [PATCH 094/154] Fix GITHUB_ENV [APIDeploy][DMDeploy][NugetDeploy] --- .github/workflows/ci-suite.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index bcf1d2ba62..5517ded8a5 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -200,7 +200,7 @@ jobs: - name: Set General__UseBasicWatchdog if: ${{ matrix.watchdog-type == 'Basic' }} - run: echo "General__UseBasicWatchdog=true" >> $env:GITHUB_ENV + run: echo "General__UseBasicWatchdog=true" >> $Env:GITHUB_ENV - name: Set TGS4_TEST_CONNECTION_STRING shell: bash @@ -213,18 +213,18 @@ jobs: - name: Set TGS4_TEST_PULL_REQUEST_NUMBER if: ${{ github.event_name == 'pull_request' }} - run: echo "TGS4_TEST_PULL_REQUEST_NUMBER=${{ github.event.number }}" >> $env:GITHUB_ENV + run: echo "TGS4_TEST_PULL_REQUEST_NUMBER=${{ github.event.number }}" >> $Env:GITHUB_ENV - name: Set TGS4_GITHUB_REF for PR if: ${{ github.event_name == 'pull_request' }} - run: echo "TGS4_GITHUB_REF=${{ github.base_ref }}" >> $env:GITHUB_ENV + run: echo "TGS4_GITHUB_REF=${{ github.base_ref }}" >> $Env:GITHUB_ENV - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} shell: bash run: | TEMP_GITHUB_REF="${{ github.event.ref }}" - echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $env:GITHUB_ENV + echo "TGS4_GITHUB_REF=${TEMP_GITHUB_REF##*/}" >> $GITHUB_ENV - name: Clean package cache as a temporary workaround for actions/setup-dotnet#155 run: dotnet clean && dotnet nuget locals all --clear From d7736fa1a409427778b13117d19ac96d5c631bab Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 19 Dec 2020 12:30:00 -0500 Subject: [PATCH 095/154] Turn off Microsoft logging [APIDeploy][DMDeploy][NugetDeploy] --- .github/workflows/ci-suite.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 5517ded8a5..8e119c41bb 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -266,8 +266,6 @@ jobs: linux-integration-tests: name: Linux Integration Test needs: dmapi-build - env: - FileLogging__MicrosoftLogLevel: Debug services: # We start all dbs here so we can just code the stuff once postgres: image: postgres From 61d504a9e45e7d7fc174cb5df0733ce36d571de0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 19 Dec 2020 20:24:37 -0500 Subject: [PATCH 096/154] Move WindowsHost from Administration to ServerInformation Lock GET /Administration behind ChangeVersion right [APIDeploy][DMDeploy][NugetDeploy] --- src/Tgstation.Server.Api/Models/Administration.cs | 7 +------ src/Tgstation.Server.Api/Models/ServerInformation.cs | 5 +++++ src/Tgstation.Server.Api/Rights/AdministrationRights.cs | 2 +- .../Controllers/AdministrationController.cs | 4 +--- src/Tgstation.Server.Host/Controllers/HomeController.cs | 9 +++++++++ tests/Tgstation.Server.Tests/AdministrationTest.cs | 3 --- tests/Tgstation.Server.Tests/RootTest.cs | 2 ++ 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/Administration.cs b/src/Tgstation.Server.Api/Models/Administration.cs index 9febaa977a..918930f30a 100644 --- a/src/Tgstation.Server.Api/Models/Administration.cs +++ b/src/Tgstation.Server.Api/Models/Administration.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Tgstation.Server.Api.Models { @@ -7,11 +7,6 @@ namespace Tgstation.Server.Api.Models /// public sealed class Administration { - /// - /// If the server is running on a windows operating system - /// - public bool WindowsHost { get; set; } - /// /// The GitHub repository the server is built to recieve updates from /// diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs index 2111302237..4c9572a1bd 100644 --- a/src/Tgstation.Server.Api/Models/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs @@ -23,6 +23,11 @@ namespace Tgstation.Server.Api.Models /// public Version? DMApiVersion { get; set; } + /// + /// If the server is running on a windows operating system. + /// + public bool WindowsHost { get; set; } + /// /// Map of to the for them. /// diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs index 5d3daf7b99..f134c7d1c9 100644 --- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs +++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs @@ -24,7 +24,7 @@ namespace Tgstation.Server.Api.Rights RestartHost = 2, /// - /// User can upgrade or downgrade TGS through the API. + /// User can read and upgrade/downgrade TGS through the API. /// ChangeVersion = 4, diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index b309585c15..292228833b 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -166,7 +166,6 @@ namespace Tgstation.Server.Host.Controllers return Conflict(new ErrorMessage(ErrorCode.ServerUpdateInProgress)); return Accepted(new Administration { - WindowsHost = platformIdentifier.IsWindows, NewVersion = newVersion }); // gtfo of here before all the cancellation tokens fire } @@ -183,7 +182,7 @@ namespace Tgstation.Server.Host.Controllers /// The GitHub API rate limit was hit. See response header Retry-After. /// A GitHub API error occurred. See error message for details. [HttpGet] - [TgsAuthorize] + [TgsAuthorize(AdministrationRights.ChangeVersion)] [ProducesResponseType(typeof(Administration), 200)] [ProducesResponseType(typeof(ErrorMessage), 424)] [ProducesResponseType(typeof(ErrorMessage), 429)] @@ -226,7 +225,6 @@ namespace Tgstation.Server.Host.Controllers { LatestVersion = greatestVersion, TrackedRepositoryUrl = repoUrl, - WindowsHost = platformIdentifier.IsWindows }); } catch (RateLimitExceededException e) diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 69ed0cc0d7..335a9cbd58 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -62,6 +62,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly IOAuthProviders oAuthProviders; + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + /// /// The for the /// @@ -88,6 +93,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of . + /// The value of . /// The value of /// The containing the value of . /// The containing the value of @@ -101,6 +107,7 @@ namespace Tgstation.Server.Host.Controllers IAssemblyInformationProvider assemblyInformationProvider, IIdentityCache identityCache, IOAuthProviders oAuthProviders, + IPlatformIdentifier platformIdentifier, IBrowserResolver browserResolver, IOptions generalConfigurationOptions, IOptions controlPanelConfigurationOptions, @@ -116,6 +123,7 @@ namespace Tgstation.Server.Host.Controllers this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders)); this.browserResolver = browserResolver; generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); @@ -166,6 +174,7 @@ namespace Tgstation.Server.Host.Controllers InstanceLimit = generalConfiguration.InstanceLimit, UserLimit = generalConfiguration.UserLimit, ValidInstancePaths = generalConfiguration.ValidInstancePaths, + WindowsHost = platformIdentifier.IsWindows, OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken).ConfigureAwait(false) }); } diff --git a/tests/Tgstation.Server.Tests/AdministrationTest.cs b/tests/Tgstation.Server.Tests/AdministrationTest.cs index 38b3f81224..cae7bc8763 100644 --- a/tests/Tgstation.Server.Tests/AdministrationTest.cs +++ b/tests/Tgstation.Server.Tests/AdministrationTest.cs @@ -1,8 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using SQLitePCL; using System; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -68,7 +66,6 @@ namespace Tgstation.Server.Tests // CI fails all the time b/c of this, ignore it return; } - Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), model.WindowsHost); //we've released a few 4.x versions now, check the release checker is at least somewhat functional Assert.AreEqual(4, model.LatestVersion.Major); diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs index eccf9c0889..4d2c023c62 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RootTest.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -161,6 +162,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(10U, serverInfo.MinimumPasswordLength); Assert.AreEqual(11U, serverInfo.InstanceLimit); Assert.AreEqual(150U, serverInfo.UserLimit); + Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), serverInfo.WindowsHost); //check that modifying the token even slightly fucks up the auth var newToken = new Token From 3d30d0cf9083af3a7a39aa0cd851b695926dcecd Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 22 Dec 2020 13:06:09 -0500 Subject: [PATCH 097/154] Adds the swarm system --- .../Models/Internal/SwarmServer.cs | 23 + .../Models/ServerInformation.cs | 5 + .../Models/SwarmServer.cs | 11 + .../Components/InstanceManager.cs | 133 +- .../Components/Interop/DMApiConstants.cs | 2 +- .../Configuration/SwarmConfiguration.cs | 26 + .../Controllers/AdministrationController.cs | 107 +- .../Controllers/BridgeController.cs | 1 + .../Controllers/HomeController.cs | 14 +- .../Controllers/InstanceController.cs | 25 +- .../Controllers/SwarmController.cs | 243 ++++ src/Tgstation.Server.Host/Core/Application.cs | 14 +- .../Core/IServerControl.cs | 2 +- .../Core/IServerUpdateInitiator.cs | 20 + .../Core/ServerUpdateInitiator.cs | 101 ++ .../Core/ServerUpdateResult.cs | 23 + .../Core/SwaggerConfiguration.cs | 1 + .../Database/DatabaseContext.cs | 139 +-- ...1222175310_MSAddSwarmIdentifer.Designer.cs | 899 ++++++++++++++ .../20201222175310_MSAddSwarmIdentifer.cs | 55 + ...1222175357_MYAddSwarmIdentifer.Designer.cs | 883 +++++++++++++ .../20201222175357_MYAddSwarmIdentifer.cs | 54 + ...1222175444_PGAddSwarmIdentifer.Designer.cs | 893 ++++++++++++++ .../20201222175444_PGAddSwarmIdentifer.cs | 54 + ...1222175532_SLAddSwarmIdentifer.Designer.cs | 882 +++++++++++++ .../20201222175532_SLAddSwarmIdentifer.cs | 67 + .../MySqlDatabaseContextModelSnapshot.cs | 5 +- ...PostgresSqlDatabaseContextModelSnapshot.cs | 5 +- .../SqlServerDatabaseContextModelSnapshot.cs | 8 +- .../SqliteDatabaseContextModelSnapshot.cs | 5 +- .../ApplicationBuilderExtensions.cs | 4 +- .../Converters/BoolConverter.cs | 4 +- .../Converters/VersionConverter.cs | 4 +- src/Tgstation.Server.Host/Models/Instance.cs | 5 + src/Tgstation.Server.Host/Server.cs | 67 +- .../Swarm/ISwarmOperations.cs | 59 + .../Swarm/ISwarmService.cs | 49 + .../Swarm/ISwarmServiceBase.cs | 18 + .../Swarm/SwarmConstants.cs | 35 + .../Swarm/SwarmRegistrationRequest.cs | 18 + .../Swarm/SwarmRegistrationResult.cs | 28 + .../Swarm/SwarmServersUpdateRequest.cs | 18 + .../Swarm/SwarmService.cs | 1091 +++++++++++++++++ .../Swarm/SwarmUpdateRequest.cs | 17 + 44 files changed, 5856 insertions(+), 261 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/Internal/SwarmServer.cs create mode 100644 src/Tgstation.Server.Api/Models/SwarmServer.cs create mode 100644 src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs create mode 100644 src/Tgstation.Server.Host/Controllers/SwarmController.cs create mode 100644 src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs create mode 100644 src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs create mode 100644 src/Tgstation.Server.Host/Core/ServerUpdateResult.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.Designer.cs create mode 100644 src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs rename src/Tgstation.Server.Host/{Components/Interop => Extensions}/Converters/BoolConverter.cs (87%) rename src/Tgstation.Server.Host/{Components/Interop => Extensions}/Converters/VersionConverter.cs (93%) create mode 100644 src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs create mode 100644 src/Tgstation.Server.Host/Swarm/ISwarmService.cs create mode 100644 src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs create mode 100644 src/Tgstation.Server.Host/Swarm/SwarmConstants.cs create mode 100644 src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs create mode 100644 src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs create mode 100644 src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs create mode 100644 src/Tgstation.Server.Host/Swarm/SwarmService.cs create mode 100644 src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs diff --git a/src/Tgstation.Server.Api/Models/Internal/SwarmServer.cs b/src/Tgstation.Server.Api/Models/Internal/SwarmServer.cs new file mode 100644 index 0000000000..788e04f850 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/SwarmServer.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Information about a server in the swarm. + /// + public abstract class SwarmServer + { + /// + /// The public address of the server. + /// + [Required] + public Uri? Address { get; set; } + + /// + /// The server's identifier. + /// + [Required] + public string? Identifier { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs index 4c9572a1bd..23f7f4cfa6 100644 --- a/src/Tgstation.Server.Api/Models/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs @@ -28,6 +28,11 @@ namespace Tgstation.Server.Api.Models /// public bool WindowsHost { get; set; } + /// + /// A of connected s. + /// + public ICollection? SwarmServers { get; set; } + /// /// Map of to the for them. /// diff --git a/src/Tgstation.Server.Api/Models/SwarmServer.cs b/src/Tgstation.Server.Api/Models/SwarmServer.cs new file mode 100644 index 0000000000..392d055518 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/SwarmServer.cs @@ -0,0 +1,11 @@ +namespace Tgstation.Server.Api.Models +{ + /// + public sealed class SwarmServer : Internal.SwarmServer + { + /// + /// If the is the controller. + /// + public bool Controller { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 3684cdb799..f50154764e 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -18,6 +18,7 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Swarm; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components @@ -26,7 +27,6 @@ namespace Tgstation.Server.Host.Components sealed class InstanceManager : IInstanceManager, IInstanceCoreProvider, - IRestartHandler, IHostedService, IBridgeRegistrar, IAsyncDisposable @@ -34,11 +34,6 @@ namespace Tgstation.Server.Host.Components /// public Task Ready => readyTcs.Task; - /// - /// The for the ; - /// - readonly Lazy lazyRestartRegistration; - /// /// The for the /// @@ -79,16 +74,16 @@ namespace Tgstation.Server.Host.Components /// readonly IAsyncDelayer asyncDelayer; - /// - /// The for the - /// - readonly IDatabaseSeeder databaseSeeder; - /// /// The for the /// readonly IServerPortProvider serverPortProvider; + /// + /// The for the . + /// + readonly ISwarmService swarmService; + /// /// The for the /// @@ -114,16 +109,16 @@ namespace Tgstation.Server.Host.Components /// readonly GeneralConfiguration generalConfiguration; + /// + /// The for the . + /// + readonly SwarmConfiguration swarmConfiguration; + /// /// The for . /// readonly TaskCompletionSource readyTcs; - /// - /// Used in to determine if database downgrades must be made - /// - Version downgradeVersion; - /// /// If the has been 'd /// @@ -140,9 +135,10 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of . /// The value of . - /// The value of . /// The value of . + /// The value of . /// The containing the value of . + /// The containing the value of . /// The value of public InstanceManager( IInstanceFactory instanceFactory, @@ -153,9 +149,10 @@ namespace Tgstation.Server.Host.Components IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, - IDatabaseSeeder databaseSeeder, IServerPortProvider serverPortProvider, + ISwarmService swarmService, IOptions generalConfigurationOptions, + IOptions swarmConfigurationOptions, ILogger logger) { this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); @@ -166,13 +163,12 @@ namespace Tgstation.Server.Host.Components this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); - this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); + this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - lazyRestartRegistration = new Lazy(() => serverControl.RegisterForRestart(this)); - instances = new Dictionary(); bridgeHandlers = new Dictionary(); readyTcs = new TaskCompletionSource(); @@ -192,7 +188,6 @@ namespace Tgstation.Server.Host.Components foreach (var I in instances) await I.Value.Instance.DisposeAsync().ConfigureAwait(false); - lazyRestartRegistration.Value.Dispose(); instanceStateChangeSemaphore.Dispose(); logger.LogInformation("Server shutdown"); @@ -385,35 +380,37 @@ namespace Tgstation.Server.Host.Components } /// - #pragma warning disable CA1506 // TODO: Decomplexify - public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext => + public async Task StartAsync(CancellationToken cancellationToken) { - logger.LogInformation(assemblyInformationProvider.VersionString); - - // we do this here because making the restart registration triggers a trace log message - // The above log message should be the first one one startup - var _ = lazyRestartRegistration.Value; - try { + logger.LogInformation(assemblyInformationProvider.VersionString); generalConfiguration.CheckCompatibility(logger); CheckSystemCompatibility(); + + await InitializeSwarm(cancellationToken).ConfigureAwait(false); + + + List dbInstances = null; + var instanceEnumeration = databaseContextFactory.UseContext( + async databaseContext => dbInstances = await databaseContext + .Instances + .AsQueryable() + .Where(x => x.Online.Value && x.SwarmIdentifer == swarmConfiguration.Identifier) + .Include(x => x.RepositorySettings) + .Include(x => x.ChatSettings) + .ThenInclude(x => x.Channels) + .Include(x => x.DreamDaemonSettings) + .ToListAsync(cancellationToken) + .ConfigureAwait(false)); + var factoryStartup = instanceFactory.StartAsync(cancellationToken); - await databaseSeeder.Initialize(databaseContext, cancellationToken).ConfigureAwait(false); - await jobManager.StartAsync(cancellationToken).ConfigureAwait(false); - var dbInstances = await databaseContext - .Instances - .AsQueryable() - .Where(x => x.Online.Value) - .Include(x => x.RepositorySettings) - .Include(x => x.ChatSettings) - .ThenInclude(x => x.Channels) - .Include(x => x.DreamDaemonSettings) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - await factoryStartup.ConfigureAwait(false); - var tasks = dbInstances.Select( + var jobManagerStartup = jobManager.StartAsync(cancellationToken); + + await Task.WhenAll(instanceEnumeration, factoryStartup, jobManagerStartup).ConfigureAwait(false); + + var instanceOnliningTasks = dbInstances.Select( async metadata => { try @@ -424,18 +421,18 @@ namespace Tgstation.Server.Host.Components { logger.LogError(ex, "Failed to online instance {0}!"); } - }) - .ToList(); - await Task.WhenAll(tasks).ConfigureAwait(false); + }); + + await Task.WhenAll(instanceOnliningTasks).ConfigureAwait(false); jobManager.Activate(); logger.LogInformation("Server ready!"); readyTcs.SetResult(null); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogInformation("Cancelled instance manager initialization!"); + logger.LogInformation(ex, "Cancelled instance manager initialization!"); } catch (Exception e) { @@ -452,21 +449,19 @@ namespace Tgstation.Server.Host.Components throw; } - }); - #pragma warning restore CA1506 // TODO: Decomplexify + } /// public async Task StopAsync(CancellationToken cancellationToken) { try { + var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken); await jobManager.StopAsync(cancellationToken).ConfigureAwait(false); await Task.WhenAll(instances.Select(x => x.Value.Instance.StopAsync(cancellationToken))).ConfigureAwait(false); - await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false); + await instanceFactoryStopTask.ConfigureAwait(false); - // downgrade the db if necessary - if (downgradeVersion != null) - await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false); + await swarmService.Shutdown(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -474,13 +469,6 @@ namespace Tgstation.Server.Host.Components } } - /// - public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) - { - downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null; - return Task.CompletedTask; - } - /// /// Check we have a valid system identity. /// @@ -559,5 +547,26 @@ namespace Tgstation.Server.Host.Components return container?.Instance; } } + + /// + /// Initializes the connection to the TGS swarm. + /// + /// The for the operation. + /// A representing the running operation. + async Task InitializeSwarm(CancellationToken cancellationToken) + { + SwarmRegistrationResult registrationResult; + do + { + registrationResult = await swarmService.Initialize(cancellationToken).ConfigureAwait(false); + + if (registrationResult == SwarmRegistrationResult.Unauthorized) + throw new InvalidOperationException("Swarm private key does not match the swarm controller's!"); + + if (registrationResult == SwarmRegistrationResult.VersionMismatch) + throw new InvalidOperationException("Swarm controller's TGS version does not match our own!"); + } + while (registrationResult != SwarmRegistrationResult.Success && !cancellationToken.IsCancellationRequested); + } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs index 9f3100ffa3..c910a8a1e2 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; -using Tgstation.Server.Host.Components.Interop.Converters; +using Tgstation.Server.Host.Extensions.Converters; using Tgstation.Server.Host.Properties; namespace Tgstation.Server.Host.Components.Interop diff --git a/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs new file mode 100644 index 0000000000..96a14283bc --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs @@ -0,0 +1,26 @@ +using System; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Host.Configuration +{ + /// + /// Configuration for the server swarm system. + /// + public sealed class SwarmConfiguration : SwarmServer + { + /// + /// The key for the the resides in. + /// + public const string Section = "Swarm"; + + /// + /// The of the swarm controller. If , the current server is considered the controller. + /// + public Uri ControllerAddress { get; set; } + + /// + /// The private key used for swarm communication. + /// + public string PrivateKey { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 292228833b..7c2f709fd3 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Octokit; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -40,7 +39,12 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the /// - readonly IServerControl serverUpdater; + readonly IServerControl serverControl; + + /// + /// The for the + /// + readonly IServerUpdateInitiator serverUpdater; /// /// The for the @@ -78,7 +82,8 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The value of - /// The value of + /// The value of . + /// The value of . /// The value of /// The value of /// The value of @@ -90,7 +95,8 @@ namespace Tgstation.Server.Host.Controllers IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, - IServerControl serverUpdater, + IServerControl serverControl, + IServerUpdateInitiator serverUpdater, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPlatformIdentifier platformIdentifier, @@ -105,6 +111,7 @@ namespace Tgstation.Server.Host.Controllers true) { this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -114,65 +121,6 @@ namespace Tgstation.Server.Host.Controllers fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } - /// - /// Try to download and apply an update with a given . - /// - /// The version of the server to update to. - /// The for the operation. - /// A resulting in the of the operation. - async Task CheckReleasesAndApplyUpdate(Version newVersion, CancellationToken cancellationToken) - { - Logger.LogDebug("Looking for GitHub releases version {0}...", newVersion); - IEnumerable releases; - try - { - var gitHubClient = gitHubClientFactory.CreateClient(); - releases = await gitHubClient - .Repository - .Release - .GetAll(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken) - .ConfigureAwait(false); - } - catch (RateLimitExceededException e) - { - return RateLimit(e); - } - catch (ApiException e) - { - Logger.LogWarning(e, OctokitException); - return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.RemoteApiError) - { - AdditionalData = e.Message - }); - } - - releases = releases.Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)); - - Logger.LogTrace("Release query complete!"); - - foreach (var release in releases) - if (Version.TryParse( - release.TagName.Replace( - updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), - out var version) - && version == newVersion) - { - var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault(); - if (asset == default) - continue; - - if (!serverUpdater.ApplyUpdate(version, new Uri(asset.BrowserDownloadUrl), ioManager)) - return Conflict(new ErrorMessage(ErrorCode.ServerUpdateInProgress)); - return Accepted(new Administration - { - NewVersion = newVersion - }); // gtfo of here before all the cancellation tokens fire - } - - return Gone(); - } - /// /// Get server information. /// @@ -273,10 +221,35 @@ namespace Tgstation.Server.Host.Controllers if (model.NewVersion.Major != assemblyInformationProvider.Version.Major) return BadRequest(new ErrorMessage(ErrorCode.CannotChangeServerSuite)); - if (!serverUpdater.WatchdogPresent) + if (!serverControl.WatchdogPresent) return UnprocessableEntity(new ErrorMessage(ErrorCode.MissingHostWatchdog)); - return await CheckReleasesAndApplyUpdate(model.NewVersion, cancellationToken).ConfigureAwait(false); + try + { + var updateResult = await serverUpdater.BeginUpdate(model.NewVersion, cancellationToken).ConfigureAwait(false); + if (updateResult == ServerUpdateResult.ReleaseMissing) + return Gone(); + + if (updateResult == ServerUpdateResult.UpdateInProgress) + return BadRequest(new ErrorMessage(ErrorCode.ServerUpdateInProgress)); + + return Accepted(new Administration + { + NewVersion = model.NewVersion + }); + } + catch (RateLimitExceededException e) + { + return RateLimit(e); + } + catch (ApiException e) + { + Logger.LogWarning(e, OctokitException); + return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.RemoteApiError) + { + AdditionalData = e.Message + }); + } } /// @@ -293,13 +266,13 @@ namespace Tgstation.Server.Host.Controllers { try { - if (!serverUpdater.WatchdogPresent) + if (!serverControl.WatchdogPresent) { Logger.LogDebug("Restart request failed due to lack of host watchdog!"); return UnprocessableEntity(new ErrorMessage(ErrorCode.MissingHostWatchdog)); } - await serverUpdater.Restart().ConfigureAwait(false); + await serverControl.Restart().ConfigureAwait(false); return NoContent(); } catch (InvalidOperationException) diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index d4adbe54a9..fd8cc46620 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -18,6 +18,7 @@ namespace Tgstation.Server.Host.Controllers /// [Route("Bridge")] [Produces(MediaTypeNames.Application.Json)] + [ApiController] public class BridgeController : Controller { /// diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 335a9cbd58..ec9bb367eb 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -21,6 +21,7 @@ using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; +using Tgstation.Server.Host.Swarm; using Tgstation.Server.Host.System; using Wangkanai.Detection; @@ -67,6 +68,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The for the . + /// + readonly ISwarmService swarmService; + /// /// The for the /// @@ -95,6 +101,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The value of + /// The value of . /// The containing the value of . /// The containing the value of /// The for the @@ -109,6 +116,7 @@ namespace Tgstation.Server.Host.Controllers IOAuthProviders oAuthProviders, IPlatformIdentifier platformIdentifier, IBrowserResolver browserResolver, + ISwarmService swarmService, IOptions generalConfigurationOptions, IOptions controlPanelConfigurationOptions, ILogger logger) @@ -125,7 +133,8 @@ namespace Tgstation.Server.Host.Controllers this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders)); - this.browserResolver = browserResolver; + this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver)); + this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); } @@ -141,6 +150,7 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] [AllowAnonymous] [ProducesResponseType(typeof(ServerInformation), 200)] + #pragma warning disable CA1506 public async Task Home(CancellationToken cancellationToken) { // if we are using a browser and the control panel, soft redirect to the app page @@ -175,9 +185,11 @@ namespace Tgstation.Server.Host.Controllers UserLimit = generalConfiguration.UserLimit, ValidInstancePaths = generalConfiguration.ValidInstancePaths, WindowsHost = platformIdentifier.IsWindows, + SwarmServers = swarmService.GetSwarmServers(), OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken).ConfigureAwait(false) }); } + #pragma warning restore CA1506 /// /// Attempt to authenticate a using diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 1b296f4c2c..8a6c9c62ba 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -72,6 +72,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly GeneralConfiguration generalConfiguration; + /// + /// The for the . + /// + readonly SwarmConfiguration swarmConfiguration; + /// /// Construct a /// @@ -83,6 +88,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of . /// The containing the value of . + /// The containing the value of . /// The for the . public InstanceController( IDatabaseContext databaseContext, @@ -93,6 +99,7 @@ namespace Tgstation.Server.Host.Controllers IPortAllocator portAllocator, IPlatformIdentifier platformIdentifier, IOptions generalConfigurationOptions, + IOptions swarmConfigurationOptions, ILogger logger) : base( databaseContext, @@ -106,6 +113,7 @@ namespace Tgstation.Server.Host.Controllers this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); } async Task CreateDefaultInstance(Api.Models.Instance initialSettings, CancellationToken cancellationToken) @@ -172,7 +180,8 @@ namespace Tgstation.Server.Host.Controllers InstancePermissionSets = new List // give this user full privileges on the instance { InstanceAdminPermissionSet(null) - } + }, + SwarmIdentifer = swarmConfiguration.Identifier, }; } @@ -257,6 +266,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext .Instances .AsQueryable() + .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier) .Select(x => new Models.Instance { Path = x.Path @@ -368,7 +378,7 @@ namespace Tgstation.Server.Host.Controllers var originalModel = await DatabaseContext .Instances .AsQueryable() - .Where(x => x.Id == id) + .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (originalModel == default) return Gone(); @@ -416,7 +426,7 @@ namespace Tgstation.Server.Host.Controllers IQueryable InstanceQuery() => DatabaseContext .Instances .AsQueryable() - .Where(x => x.Id == model.Id); + .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier); var moveJob = await InstanceQuery() .SelectMany(x => x.Jobs). @@ -601,7 +611,10 @@ namespace Tgstation.Server.Host.Controllers { IQueryable GetBaseQuery() { - IQueryable query = DatabaseContext.Instances; + IQueryable query = DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier); if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List)) query = query .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value)) @@ -668,7 +681,7 @@ namespace Tgstation.Server.Host.Controllers var query = DatabaseContext .Instances .AsQueryable() - .Where(x => x.Id == id); + .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier); if (cantList) query = query.Include(x => x.InstancePermissionSets); @@ -722,7 +735,7 @@ namespace Tgstation.Server.Host.Controllers var usersInstancePermissionSet = await DatabaseContext .Instances .AsQueryable() - .Where(x => x.Id == id) + .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier) .SelectMany(x => x.InstancePermissionSets) .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value) .FirstOrDefaultAsync(cancellationToken) diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs new file mode 100644 index 0000000000..cb4fa051c9 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -0,0 +1,243 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Serilog.Context; +using System; +using System.Linq; +using System.Net; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Swarm; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// For swarm server communication. + /// + [Route(SwarmConstants.ControllerRoute)] + [Produces(MediaTypeNames.Application.Json)] + [ApiController] + public sealed class SwarmController : Controller + { + /// + /// Get the current registration from the . + /// + Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First()); + + /// + /// The for the . + /// + readonly ISwarmOperations swarmOperations; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly SwarmConfiguration swarmConfiguration; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The containing the value of . + /// The value of . + public SwarmController( + ISwarmOperations swarmOperations, + IAssemblyInformationProvider assemblyInformationProvider, + IOptions swarmConfigurationOptions, + ILogger logger) + { + this.swarmOperations = swarmOperations ?? throw new ArgumentNullException(nameof(swarmOperations)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.logger = logger; + } + + /// + /// Check that the is valid. + /// + /// if the is valid, otherwise. + bool ValidateRegistration() => swarmOperations.ValidateRegistration(RequestRegistrationId); + + /// + /// Registration endpoint. + /// + /// The . + /// The of the operation. + [HttpPost(SwarmConstants.RegisterRoute)] + public IActionResult Register([FromBody]SwarmRegistrationRequest registrationRequest) + { + if (registrationRequest == null) + throw new ArgumentNullException(nameof(registrationRequest)); + + if (registrationRequest.ServerVersion != assemblyInformationProvider.Version) + return StatusCode((int)HttpStatusCode.UpgradeRequired); + + var registrationResult = swarmOperations.RegisterNode(registrationRequest, RequestRegistrationId); + if (!registrationResult) + return Conflict(); + return NoContent(); + } + + /// + /// Deregistration endpoint. + /// + /// The for the operation. + /// A resulting in the of the operation. + [HttpDelete(SwarmConstants.RegisterRoute)] + public async Task UnregisterNode(CancellationToken cancellationToken) + { + if (!ValidateRegistration()) + return Forbid(); + + await swarmOperations.UnregisterNode(RequestRegistrationId, cancellationToken).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Health check endpoint. + /// + /// The of the operation. + [HttpGet] + public IActionResult HealthCheck() + { + if (!ValidateRegistration()) + return Forbid(); + + return NoContent(); + } + + /// + /// Node list update endpoint. + /// + /// The . + /// The of the operation. + [HttpPost] + public IActionResult UpdateNodeList([FromBody]SwarmServersUpdateRequest serversUpdateRequest) + { + if (serversUpdateRequest == null) + throw new ArgumentNullException(nameof(serversUpdateRequest)); + + if (!ValidateRegistration()) + return Forbid(); + + swarmOperations.UpdateSwarmServersList(serversUpdateRequest.SwarmServers); + return NoContent(); + } + + /// + /// Update initiation endpoint. + /// + /// The . + /// The for the operation. + /// A resulting in the of the operation. + [HttpPut(SwarmConstants.UpdateRoute)] + public async Task PrepareUpdate([FromBody]SwarmUpdateRequest updateRequest, CancellationToken cancellationToken) + { + if (updateRequest == null) + throw new ArgumentNullException(nameof(updateRequest)); + + if (!ValidateRegistration()) + return Forbid(); + + var prepareResult = await swarmOperations.PrepareUpdateFromController(updateRequest.UpdateVersion, cancellationToken).ConfigureAwait(false); + if (!prepareResult) + return Conflict(); + + return NoContent(); + } + + /// + /// Update commit endpoint. + /// + /// The for the operation. + /// A resulting in the of the operation. + [HttpPost(SwarmConstants.UpdateRoute)] + public async Task CommitUpdate(CancellationToken cancellationToken) + { + if (!ValidateRegistration()) + return Forbid(); + + var result = await swarmOperations.RemoteCommitRecieved(RequestRegistrationId, cancellationToken).ConfigureAwait(false); + if (!result) + return Conflict(); + return NoContent(); + } + + /// + /// Update abort endpoint. + /// + /// The for the operation. + /// A resulting in the of the operation. + [HttpDelete(SwarmConstants.UpdateRoute)] + public async Task AbortUpdate(CancellationToken cancellationToken) + { + if (!ValidateRegistration()) + return Forbid(); + + await swarmOperations.AbortUpdate(cancellationToken).ConfigureAwait(false); + return NoContent(); + } + + /// + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (swarmConfiguration.PrivateKey == null) + { + logger.LogDebug("Attempted swarm request without private key!"); + await Forbid().ExecuteResultAsync(context).ConfigureAwait(false); + return; + } + + if (!(Request.Headers.TryGetValue(SwarmConstants.ApiKeyHeader, out var apiKeyHeaderValues) + && apiKeyHeaderValues.Count == 1 + && apiKeyHeaderValues.First() == swarmConfiguration.PrivateKey)) + { + logger.LogDebug("Unauthorized swarm request!"); + await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false); + return; + } + + if (!(Request.Headers.TryGetValue(SwarmConstants.RegistrationIdHeader, out var registrationHeaderValues) + && registrationHeaderValues.Count == 1 + && Guid.TryParse(registrationHeaderValues.First(), out var registrationId))) + { + logger.LogDebug("Swarm request without registration ID!"); + await BadRequest().ExecuteResultAsync(context).ConfigureAwait(false); + return; + } + + // we validate the registration itself on a case-by-case basis + if (ModelState?.IsValid == false) + { + var errors = ModelState + .SelectMany(x => x.Value.Errors) + .Select(x => x.Exception); + + logger.LogDebug(new AggregateException(errors), "Swarm request model validation failed!"); + await BadRequest().ExecuteResultAsync(context).ConfigureAwait(false); + return; + } + + using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}")) + { + logger.LogDebug("Starting swarm request..."); + await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 1e937fc745..10e52d48ea 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -16,6 +16,7 @@ using Serilog; using Serilog.Events; using Serilog.Formatting.Display; using System; +using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; @@ -28,7 +29,6 @@ using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Interop.Bridge; -using Tgstation.Server.Host.Components.Interop.Converters; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Components.Watchdog; @@ -36,12 +36,14 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Extensions.Converters; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Properties; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; using Tgstation.Server.Host.Setup; +using Tgstation.Server.Host.Swarm; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Transfer; @@ -105,6 +107,7 @@ namespace Tgstation.Server.Host.Core // configure configuration services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); + services.UseStandardConfig(Configuration); // enable options which give us config reloading services.AddOptions(); @@ -207,7 +210,10 @@ namespace Tgstation.Server.Host.Core options.SerializerSettings.CheckAdditionalContent = true; options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error; options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - options.SerializerSettings.Converters = new[] { new VersionConverter() }; + options.SerializerSettings.Converters = new List + { + new VersionConverter() + }; }); if (postSetupServices.GeneralConfiguration.HostApiDocumentation) @@ -325,6 +331,10 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(); // configure root services services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index 03b5b10a67..97a60b263b 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.IO; diff --git a/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs b/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs new file mode 100644 index 0000000000..9dfc307cca --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Initiates server self updates. + /// + public interface IServerUpdateInitiator + { + /// + /// Start the process of downloading and applying an update to a new server . + /// + /// The TGS to update to. + /// The for the operation. + /// A resulting in the . + Task BeginUpdate(Version version, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs b/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs new file mode 100644 index 0000000000..eda60adc04 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Octokit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class ServerUpdateInitiator : IServerUpdateInitiator + { + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The for the . + /// + readonly IServerControl serverControl; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly UpdatesConfiguration updatesConfiguration; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The containing the value of . + public ServerUpdateInitiator( + IGitHubClientFactory gitHubClientFactory, + IIOManager ioManager, + IServerControl serverControl, + ILogger logger, + IOptions updatesConfigurationOptions) + { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); + } + + /// + public async Task BeginUpdate(Version newVersion, CancellationToken cancellationToken) + { + logger.LogDebug("Looking for GitHub releases version {0}...", newVersion); + IEnumerable releases; + var gitHubClient = gitHubClientFactory.CreateClient(); + releases = await gitHubClient + .Repository + .Release + .GetAll(updatesConfiguration.GitHubRepositoryId) + .WithToken(cancellationToken) + .ConfigureAwait(false); + + releases = releases.Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)); + + logger.LogTrace("Release query complete!"); + + foreach (var release in releases) + if (Version.TryParse( + release.TagName.Replace( + updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), + out var version) + && version == newVersion) + { + var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault(); + if (asset == default) + continue; + + if (!serverControl.ApplyUpdate(version, new Uri(asset.BrowserDownloadUrl), ioManager)) + return ServerUpdateResult.UpdateInProgress; + return ServerUpdateResult.Started; + } + + return ServerUpdateResult.ReleaseMissing; + } + } +} diff --git a/src/Tgstation.Server.Host/Core/ServerUpdateResult.cs b/src/Tgstation.Server.Host/Core/ServerUpdateResult.cs new file mode 100644 index 0000000000..0fe3d71303 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/ServerUpdateResult.cs @@ -0,0 +1,23 @@ +namespace Tgstation.Server.Host.Core +{ + /// + /// The result of a call to start a server update. + /// + public enum ServerUpdateResult + { + /// + /// The update process was started successfully. + /// + Started, + + /// + /// The requested release version was not found. + /// + ReleaseMissing, + + /// + /// Another update is already in progress. + /// + UpdateInProgress, + } +} diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index 2eb316f507..f0f8c8db8c 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -385,6 +385,7 @@ namespace Tgstation.Server.Host.Core { nameof(BridgeController), nameof(ControlPanelController), + nameof(SwarmController), }; foreach (var path in swaggerDoc.Paths) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 07d234f566..024df2d86a 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -332,7 +332,7 @@ namespace Tgstation.Server.Host.Database modelBuilder.Entity().HasIndex(x => new { x.InstanceId, x.Name }).IsUnique(); var instanceModel = modelBuilder.Entity(); - instanceModel.HasIndex(x => x.Path).IsUnique(); + instanceModel.HasIndex(x => new { x.Path, x.SwarmIdentifer }).IsUnique(); instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); @@ -373,22 +373,22 @@ namespace Tgstation.Server.Host.Database /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - public static readonly Type MSLatestMigration = typeof(MSAddUserGroups); + public static readonly Type MSLatestMigration = typeof(MSAddSwarmIdentifer); /// /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - public static readonly Type MYLatestMigration = typeof(MYAddUserGroups); + public static readonly Type MYLatestMigration = typeof(MYAddSwarmIdentifer); /// /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - public static readonly Type PGLatestMigration = typeof(PGAddUserGroups); + public static readonly Type PGLatestMigration = typeof(PGAddSwarmIdentifer); /// /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - public static readonly Type SLLatestMigration = typeof(SLAddUserGroups); + public static readonly Type SLLatestMigration = typeof(SLAddSwarmIdentifer); #endif /// @@ -415,85 +415,41 @@ namespace Tgstation.Server.Host.Database // Update this with new migrations as they are made string targetMigration = null; if (targetVersion < new Version(4, 7, 0)) - switch (currentDatabaseType) + targetMigration = currentDatabaseType switch { - case DatabaseType.MariaDB: - case DatabaseType.MySql: - targetMigration = nameof(MYAddAdditionalDDParameters); - break; - case DatabaseType.PostgresSql: - targetMigration = nameof(PGAddAdditionalDDParameters); - break; - case DatabaseType.SqlServer: - targetMigration = nameof(MSAddAdditionalDDParameters); - break; - case DatabaseType.Sqlite: - targetMigration = nameof(SLAddAdditionalDDParameters); - break; - default: - throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); - } - + DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYAddAdditionalDDParameters), + DatabaseType.PostgresSql => nameof(PGAddAdditionalDDParameters), + DatabaseType.SqlServer => nameof(MSAddAdditionalDDParameters), + DatabaseType.Sqlite => nameof(SLAddAdditionalDDParameters), + _ => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)), + }; if (targetVersion < new Version(4, 6, 0)) - switch (currentDatabaseType) + targetMigration = currentDatabaseType switch { - case DatabaseType.MariaDB: - case DatabaseType.MySql: - targetMigration = nameof(MYAddDeploymentColumns); - break; - case DatabaseType.PostgresSql: - targetMigration = nameof(PGAddDeploymentColumns); - break; - case DatabaseType.SqlServer: - targetMigration = nameof(MSAddDeploymentColumns); - break; - case DatabaseType.Sqlite: - targetMigration = nameof(SLAddDeploymentColumns); - break; - default: - throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); - } - + DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYAddDeploymentColumns), + DatabaseType.PostgresSql => nameof(PGAddDeploymentColumns), + DatabaseType.SqlServer => nameof(MSAddDeploymentColumns), + DatabaseType.Sqlite => nameof(SLAddDeploymentColumns), + _ => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)), + }; if (targetVersion < new Version(4, 5, 0)) - switch (currentDatabaseType) + targetMigration = currentDatabaseType switch { - case DatabaseType.MariaDB: - case DatabaseType.MySql: - targetMigration = nameof(MYAllowNullDMApi); - break; - case DatabaseType.PostgresSql: - targetMigration = nameof(PGAllowNullDMApi); - break; - case DatabaseType.SqlServer: - targetMigration = nameof(MSAllowNullDMApi); - break; - case DatabaseType.Sqlite: - targetMigration = nameof(SLAllowNullDMApi); - break; - default: - throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); - } - + DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYAllowNullDMApi), + DatabaseType.PostgresSql => nameof(PGAllowNullDMApi), + DatabaseType.SqlServer => nameof(MSAllowNullDMApi), + DatabaseType.Sqlite => nameof(SLAllowNullDMApi), + _ => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)), + }; if (targetVersion < new Version(4, 4, 0)) - switch (currentDatabaseType) + targetMigration = currentDatabaseType switch { - case DatabaseType.MariaDB: - case DatabaseType.MySql: - targetMigration = nameof(MYFixForeignKey); - break; - case DatabaseType.PostgresSql: - targetMigration = nameof(PGCreate); - break; - case DatabaseType.SqlServer: - targetMigration = nameof(MSRemoveSoftColumns); - break; - case DatabaseType.Sqlite: - targetMigration = nameof(SLRemoveSoftColumns); - break; - default: - throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); - } - + DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYFixForeignKey), + DatabaseType.PostgresSql => nameof(PGCreate), + DatabaseType.SqlServer => nameof(MSRemoveSoftColumns), + DatabaseType.Sqlite => nameof(SLRemoveSoftColumns), + _ => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)), + }; if (targetVersion < new Version(4, 2, 0)) targetMigration = currentDatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete); @@ -503,29 +459,18 @@ namespace Tgstation.Server.Host.Database return; } - string migrationSubstitution; - switch (currentDatabaseType) + // already setup + var migrationSubstitution = currentDatabaseType switch { - case DatabaseType.SqlServer: - // already setup - migrationSubstitution = null; - break; - case DatabaseType.MySql: - case DatabaseType.MariaDB: - migrationSubstitution = "MY{0}"; - break; - case DatabaseType.Sqlite: - migrationSubstitution = "SL{0}"; - break; - case DatabaseType.PostgresSql: - migrationSubstitution = "PG{0}"; - break; - default: - throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"); - } + DatabaseType.SqlServer => null,// already setup + DatabaseType.MySql or DatabaseType.MariaDB => "MY{0}", + DatabaseType.Sqlite => "SL{0}", + DatabaseType.PostgresSql => "PG{0}", + _ => throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"), + }; if (migrationSubstitution != null) - targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration.Substring(2)); + targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration[2..]); // even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻ var dbServiceProvider = ((IInfrastructure)Database).Instance; diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.Designer.cs new file mode 100644 index 0000000000..5ceffde807 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.Designer.cs @@ -0,0 +1,899 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20201222175310_MSAddSwarmIdentifer")] + partial class MSAddSwarmIdentifer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SwarmIdentifer") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique() + .HasFilter("[SwarmIdentifer] IS NOT NULL"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("decimal(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique() + .HasFilter("[GroupId] IS NOT NULL"); + + b.HasIndex("UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasColumnType("nvarchar(max)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.cs new file mode 100644 index 0000000000..70e154ffd9 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175310_MSAddSwarmIdentifer.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the swarm identifier column for MSSQL. + /// + public partial class MSAddSwarmIdentifer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path", + table: "Instances"); + + migrationBuilder.AddColumn( + name: "SwarmIdentifer", + table: "Instances", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances", + columns: new[] { "Path", "SwarmIdentifer" }, + unique: true, + filter: "[SwarmIdentifer] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "SwarmIdentifer", + table: "Instances"); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path", + table: "Instances", + column: "Path", + unique: true); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.Designer.cs new file mode 100644 index 0000000000..61060d15ec --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.Designer.cs @@ -0,0 +1,883 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20201222175357_MYAddSwarmIdentifer")] + partial class MYAddSwarmIdentifer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RepositoryOrigin") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("SwarmIdentifer") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("bigint unsigned"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SystemIdentifier") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.cs new file mode 100644 index 0000000000..643ffd516c --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175357_MYAddSwarmIdentifer.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the swarm identifier column for MySQL. + /// + public partial class MYAddSwarmIdentifer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path", + table: "Instances"); + + migrationBuilder.AddColumn( + name: "SwarmIdentifer", + table: "Instances", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances", + columns: new[] { "Path", "SwarmIdentifer" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "SwarmIdentifer", + table: "Instances"); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path", + table: "Instances", + column: "Path", + unique: true); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.Designer.cs new file mode 100644 index 0000000000..bd76fc40c7 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.Designer.cs @@ -0,0 +1,893 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20201222175444_PGAddSwarmIdentifer")] + partial class PGAddSwarmIdentifer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RepositoryOrigin") + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SwarmIdentifer") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("numeric(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.cs new file mode 100644 index 0000000000..f052cf0609 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175444_PGAddSwarmIdentifer.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the swarm identifier column for PostgresSQL. + /// + public partial class PGAddSwarmIdentifer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path", + table: "Instances"); + + migrationBuilder.AddColumn( + name: "SwarmIdentifer", + table: "Instances", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances", + columns: new[] { "Path", "SwarmIdentifer" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "SwarmIdentifer", + table: "Instances"); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path", + table: "Instances", + column: "Path", + unique: true); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.Designer.cs new file mode 100644 index 0000000000..21719bc857 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.Designer.cs @@ -0,0 +1,882 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20201222175532_SLAddSwarmIdentifer")] + partial class SLAddSwarmIdentifer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.10"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SwarmIdentifer") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondRights") + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("INTEGER"); + + b.Property("PermissionSetId") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs new file mode 100644 index 0000000000..bd8751bf63 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Adds the swarm identifier column for SQLite. + /// + public partial class SLAddSwarmIdentifer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropIndex( + name: "IX_Instances_Path", + table: "Instances"); + + migrationBuilder.AddColumn( + name: "SwarmIdentifer", + table: "Instances", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path_SwarmIdentifer", + table: "Instances", + columns: new[] { "Path", "SwarmIdentifer" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.RenameTable( + name: "Instances", + newName: "Instances_down"); + + migrationBuilder.CreateTable( + name: "Instances", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 10000, nullable: false), + Path = table.Column(nullable: false), + Online = table.Column(nullable: false), + ConfigurationType = table.Column(nullable: false), + AutoUpdateInterval = table.Column(nullable: false), + ChatBotLimit = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Instances", x => x.Id); + }); + + migrationBuilder.Sql("INSERT INTO Instances (Id, Name, Path, Online, ConfigurationType, AutoUpdateInterval, ChatBotLimit) SELECT Id, Name, Path, Online, ConfigurationType, AutoUpdateInterval, ChatBotLimit FROM Instances_down"); + + migrationBuilder.DropTable( + name: "Instances_down"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index 9d1317fd6d..8707171228 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -272,9 +272,12 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + b.Property("SwarmIdentifer") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + b.HasKey("Id"); - b.HasIndex("Path") + b.HasIndex("Path", "SwarmIdentifer") .IsUnique(); b.ToTable("Instances"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index 2e57e221e0..a4d189000e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -271,9 +271,12 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("SwarmIdentifer") + .HasColumnType("text"); + b.HasKey("Id"); - b.HasIndex("Path") + b.HasIndex("Path", "SwarmIdentifer") .IsUnique(); b.ToTable("Instances"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index a6c42736a5..5f77127198 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -273,10 +273,14 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("nvarchar(450)"); + b.Property("SwarmIdentifer") + .HasColumnType("nvarchar(450)"); + b.HasKey("Id"); - b.HasIndex("Path") - .IsUnique(); + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique() + .HasFilter("[SwarmIdentifer] IS NOT NULL"); b.ToTable("Instances"); }); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index 59ee3f2b8f..ca51e0514a 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -271,9 +271,12 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("SwarmIdentifer") + .HasColumnType("TEXT"); + b.HasKey("Id"); - b.HasIndex("Path") + b.HasIndex("Path", "SwarmIdentifer") .IsUnique(); b.ToTable("Instances"); diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index 28f3d93034..96ed8a2952 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -17,6 +17,8 @@ namespace Tgstation.Server.Host.Extensions /// static class ApplicationBuilderExtensions { + public const string XPoweredByHeader = "X-Powered-By"; + /// /// Gets a from a given /// @@ -129,7 +131,7 @@ namespace Tgstation.Server.Host.Extensions applicationBuilder.Use(async (context, next) => { - context.Response.Headers.Add("X-Powered-By", assemblyInformationProvider.VersionPrefix); + context.Response.Headers.Add(XPoweredByHeader, assemblyInformationProvider.VersionPrefix); await next().ConfigureAwait(false); }); } diff --git a/src/Tgstation.Server.Host/Components/Interop/Converters/BoolConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs similarity index 87% rename from src/Tgstation.Server.Host/Components/Interop/Converters/BoolConverter.cs rename to src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs index 864127890a..3ce617cdb9 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Converters/BoolConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs @@ -1,7 +1,7 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; -namespace Tgstation.Server.Host.Components.Interop.Converters +namespace Tgstation.Server.Host.Extensions.Converters { /// /// for decoding s returned by BYOND. diff --git a/src/Tgstation.Server.Host/Components/Interop/Converters/VersionConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs similarity index 93% rename from src/Tgstation.Server.Host/Components/Interop/Converters/VersionConverter.cs rename to src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs index 5e4c307b86..9d5ebeec77 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Converters/VersionConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using Tgstation.Server.Api; -namespace Tgstation.Server.Host.Components.Interop.Converters +namespace Tgstation.Server.Host.Extensions.Converters { /// /// for serializing s for BYOND. diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 29867be375..3c721a12f1 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -27,6 +27,11 @@ namespace Tgstation.Server.Host.Models /// public RepositorySettings RepositorySettings { get; set; } + /// + /// The of the the server in the swarm this instance belongs to. + /// + public string SwarmIdentifer { get; set; } + /// /// The s in the /// diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index dbfaee0b7a..f8f9c91a73 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Swarm; namespace Tgstation.Server.Host { @@ -48,6 +49,11 @@ namespace Tgstation.Server.Host /// readonly object restartLock; + /// + /// The for the . + /// + ISwarmService swarmService; + /// /// The for the /// @@ -140,6 +146,7 @@ namespace Tgstation.Server.Host using var host = hostBuilder.Build(); try { + swarmService = host.Services.GetRequiredService(); logger = host.Services.GetRequiredService>(); using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!"))) { @@ -193,31 +200,63 @@ namespace Tgstation.Server.Host throw new InvalidOperationException("Tried to update a non-running Server!"); var cancellationToken = cancellationTokenSource.Token; - logger.LogTrace("Downloading zip package..."); - using var updateZipData = new MemoryStream( - await ioManager.DownloadFile( - updateZipUrl, - cancellationToken) - .ConfigureAwait(false)); + var updatePrepareResult = await swarmService.PrepareUpdate(version, cancellationToken).ConfigureAwait(false); + if (!updatePrepareResult) + return; + + MemoryStream updateZipData; try { - logger.LogTrace("Exctracting zip package to {0}...", updatePath); - await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false); + logger.LogTrace("Downloading zip package..."); + updateZipData = new MemoryStream( + await ioManager.DownloadFile( + updateZipUrl, + cancellationToken) + .ConfigureAwait(false)); } - catch (Exception e) + catch (Exception e1) { - updating = false; try { - // important to not leave this directory around if possible - await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false); + await swarmService.AbortUpdate(cancellationToken).ConfigureAwait(false); } catch (Exception e2) { - throw new AggregateException(e, e2); + throw new AggregateException(e1, e2); + } + + throw; + } + + using (updateZipData) + { + var updateCommitResult = await swarmService.CommitUpdate(cancellationToken).ConfigureAwait(false); + if (!updateCommitResult) + { + logger.LogError("Swarm distributed commit failed, not applying update!"); + return; } - throw; + try + { + logger.LogTrace("Extracting zip package to {0}...", updatePath); + await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + updating = false; + try + { + // important to not leave this directory around if possible + await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false); + } + catch (Exception e2) + { + throw new AggregateException(e, e2); + } + + throw; + } } await Restart(version, null, true).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs new file mode 100644 index 0000000000..3cdd9f1774 --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// Swarm service operations for the . + /// + public interface ISwarmOperations : ISwarmServiceBase + { + /// + /// Pass in an updated list of to the node. + /// + /// An of the updated s. + void UpdateSwarmServersList(IEnumerable swarmServers); + + /// + /// Notify the node of an update request from the controller. + /// + /// The of TGS to update to. + /// The for the operation. + /// A resulting in if the node is able to update, otherwise. + Task PrepareUpdateFromController(Version version, CancellationToken cancellationToken); + + /// + /// Validate a given . + /// + /// The registration to validate. + /// if the registration is valid, otherwise. + bool ValidateRegistration(Guid registrationId); + + /// + /// Attempt to register a given with the controller. + /// + /// The that is registering. + /// The registration . + /// if the registration was successful, otherwise. + bool RegisterNode(Api.Models.Internal.SwarmServer node, Guid registrationId); + + /// + /// Attempt to unregister a node with a given with the controller. + /// + /// The registration . + /// The for the operation. + /// A representing the running operation. + Task UnregisterNode(Guid registrationId, CancellationToken cancellationToken); + + /// + /// Notify the controller that the node with the given is ready to commit or notify the node of the controller telling it to commit. + /// + /// The registration . + /// The for the operation. + /// A representing the running operation. + Task RemoteCommitRecieved(Guid registrationId, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs new file mode 100644 index 0000000000..cb3f8252fb --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// Used for swarm operations. Functions may be no-op based on configuration. + /// + public interface ISwarmService : ISwarmServiceBase + { + /// + /// Attempt to register with the swarm controller if not one, sets up the database otherwise. + /// + /// The for the operation. + /// A resulting in the . + Task Initialize(CancellationToken cancellationToken); + + /// + /// Deregister with the swarm controller or put clients into querying state. + /// + /// The for the operation. + /// A representing the running operation. + Task Shutdown(CancellationToken cancellationToken); + + /// + /// Signal to the swarm that an update is requested. + /// + /// The to update to. + /// The for the operation. + /// A resulting in if the update should proceed, otherwise. + Task PrepareUpdate(Version version, CancellationToken cancellationToken); + + /// + /// Signal to the swarm that an update is ready to be applied. + /// + /// The for the operation. + /// A resulting in if the update should proceed, otherwise. + Task CommitUpdate(CancellationToken cancellationToken); + + /// + /// Gets the list of s in the swarm, including the current one. + /// + /// A of s in the swarm. If the server is not part of a swarm, will be returned. + ICollection GetSwarmServers(); + } +} diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs b/src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs new file mode 100644 index 0000000000..5ae46cebff --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// For aborting swarm updates. + /// + public interface ISwarmServiceBase + { + /// + /// Abort an uncommitted update. + /// + /// The for the operation. + /// A representing the running operation. + Task AbortUpdate(CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs new file mode 100644 index 0000000000..6581c0602a --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs @@ -0,0 +1,35 @@ +using Tgstation.Server.Api; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// Constants used by the swarm system. + /// + static class SwarmConstants + { + /// + /// The base route for . + /// + public const string ControllerRoute = Routes.Root + "Swarm"; + + /// + /// The header used to pass in the . + /// + public const string ApiKeyHeader = "X-API-KEY"; + + /// + /// The header used to pass in swarm registration IDs. + /// + public const string RegistrationIdHeader = "SwarmRegistration"; + + /// + /// The route used for swarm registration. + /// + public const string RegisterRoute = "/Register"; + + /// + /// The route used for swarm updates. + /// + public const string UpdateRoute = "/Update"; + } +} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs new file mode 100644 index 0000000000..3de0403284 --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// A request to register with a swarm controller. + /// + public sealed class SwarmRegistrationRequest : SwarmServer + { + /// + /// The TGS of the sending server. + /// + [Required] + public Version ServerVersion { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs new file mode 100644 index 0000000000..585c9b7e3a --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs @@ -0,0 +1,28 @@ +namespace Tgstation.Server.Host.Swarm +{ + /// + /// Result of attempting to register with a swarm controller. + /// + public enum SwarmRegistrationResult + { + /// + /// The registration succeeded. + /// + Success, + + /// + /// The swarm private keys didn't match. + /// + Unauthorized, + + /// + /// The swarm controller is running a different server version. + /// + VersionMismatch, + + /// + /// A communication error occurred. + /// + CommunicationFailure, + } +} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs new file mode 100644 index 0000000000..ed480707ca --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// A request to update a nodes list of . + /// + public sealed class SwarmServersUpdateRequest + { + /// + /// The of updated s. + /// + [Required] + public ICollection SwarmServers { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs new file mode 100644 index 0000000000..c450365f15 --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -0,0 +1,1091 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Extensions.Converters; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// Helps keep servers connected to the same database in sync by coordinating updates. + /// + sealed class SwarmService : ISwarmService, ISwarmOperations, IRestartHandler, IDisposable + { + /// + /// Interval at which the swarm controller makes health checks on nodes. + /// + const int ControllerHealthCheckIntervalMinutes = 5; + + /// + /// Interval at which the node makes health checks on the controller if it has not received one. + /// + const int NodeHealthCheckIntervalMinutes = 7; + + /// + /// See for the swarm system. + /// + static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }, + Converters = new JsonConverter[] + { + new VersionConverter(), + new BoolConverter() + }, + DefaultValueHandling = DefaultValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + + /// + /// If the swarm system is enabled. + /// + bool SwarmMode => swarmConfiguration.PrivateKey != null; + + /// + /// Lazily constructed . + /// + readonly Lazy lazyRestartRegistration; + + /// + /// The for the . + /// + readonly IDatabaseContextFactory databaseContextFactory; + + /// + /// The for the . + /// + readonly IDatabaseSeeder databaseSeeder; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly IHttpClientFactory httpClientFactory; + + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly IServerUpdateInitiator serverUpdater; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly SwarmConfiguration swarmConfiguration; + + /// + /// The for . + /// + readonly CancellationTokenSource serverHealthCheckCancellationTokenSource; + + /// + /// of connected s. + /// + readonly List swarmServers; + + /// + /// of s to registration s. + /// + readonly Dictionary registrationIds; + + /// + /// used for accessing . + /// + readonly object updateSynchronizationLock; + + /// + /// If the current server is the swarm controller. + /// + readonly bool swarmController; + + /// + /// The that is used to proceed with committing an update. + /// + TaskCompletionSource updateCommitTcs; + + /// + /// of s that need to send a ready-commit before the update can proceed. + /// + List nodesThatNeedToBeReadyToCommit; + + /// + /// The for the . + /// + Task serverHealthCheckTask; + + /// + /// The set for a two phase commit update. + /// + Version targetUpdateVersion; + + /// + /// The registration provided by the swarm controller. + /// + Guid? controllerRegistration; + + /// + /// The last when the controller checked on this node. + /// + DateTimeOffset? lastControllerHealthCheck; + + /// + /// If was called. + /// + bool restarting; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The to register ourselves as a with. + /// The value of . + /// The value of . + /// The containing the value of . + /// The value of . + public SwarmService( + IDatabaseContextFactory databaseContextFactory, + IDatabaseSeeder databaseSeeder, + IAssemblyInformationProvider assemblyInformationProvider, + IHttpClientFactory httpClientFactory, + IServerControl serverControl, + IServerUpdateInitiator serverUpdateInitiator, + IAsyncDelayer asyncDelayer, + IOptions swarmConfigurationOptions, + ILogger logger) + { + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + if (serverControl == null) + throw new ArgumentNullException(nameof(serverControl)); + + serverUpdater = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + if (SwarmMode) + { + if (swarmConfiguration.Address == null) + throw new InvalidOperationException("Swarm configuration missing Address!"); + if (String.IsNullOrWhiteSpace(swarmConfiguration.Identifier)) + throw new InvalidOperationException("Swarm configuration missing Identifier!"); + } + + swarmController = !SwarmMode || swarmConfiguration.ControllerAddress == null; + if (SwarmMode) + { + serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); + if (swarmController) + registrationIds = new Dictionary(); + + swarmServers = new List + { + new SwarmServer + { + Address = swarmConfiguration.Address, + Controller = swarmController, + Identifier = swarmConfiguration.Identifier + } + }; + + updateSynchronizationLock = new object(); + } + + lazyRestartRegistration = new Lazy(() => serverControl.RegisterForRestart(this)); + } + + /// + public void Dispose() => serverHealthCheckCancellationTokenSource?.Dispose(); + + /// + public async Task AbortUpdate(CancellationToken cancellationToken) + { + if (!SwarmMode) + return; + + if (targetUpdateVersion == null) + { + logger.LogTrace("Not aborting non-exitent update"); + return; + } + + logger.LogInformation("Aborting swarm update!"); + updateCommitTcs?.TrySetResult(false); + updateCommitTcs = null; + nodesThatNeedToBeReadyToCommit = null; + targetUpdateVersion = null; + + using var httpClient = httpClientFactory.CreateClient(); + async Task SendRemoteAbort(SwarmServer swarmServer) + { + using var request = PrepareSwarmRequest( + swarmServer, + HttpMethod.Delete, + SwarmConstants.UpdateRoute, + null); + + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + + Task task; + if (!swarmController) + task = SendRemoteAbort(new SwarmServer + { + Address = swarmConfiguration.ControllerAddress, + }); + else + { + lock (swarmServers) + task = Task.WhenAll( + swarmServers.Select( + x => SendRemoteAbort(x))); + } + + await task.ConfigureAwait(false); + } + + /// + public async Task CommitUpdate(CancellationToken cancellationToken) + { + if (!SwarmMode) + return true; + + logger.LogInformation("Waiting to commit update..."); + using var httpClient = httpClientFactory.CreateClient(); + if (!swarmController) + { + // let the controller know we're ready + logger.LogTrace("Sending ready-commit to swarm controller..."); + using var commitReadyRequest = PrepareSwarmRequest( + null, + HttpMethod.Post, + SwarmConstants.UpdateRoute, + null); + + try + { + using var commitReadyResponse = await httpClient.SendAsync(commitReadyRequest, cancellationToken).ConfigureAwait(false); + commitReadyResponse.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Unable to send ready-commit to swarm controller!"); + await AbortUpdate(cancellationToken).ConfigureAwait(false); + return false; + } + } + + // wait for the update commit TCS + var commitTcsTask = updateCommitTcs?.Task; + if (commitTcsTask == null) + { + logger.LogDebug("Update commit failed, no pending task completion source!"); + return false; + } + + var commitGoAhead = await commitTcsTask.ConfigureAwait(false) && updateCommitTcs?.Task == commitTcsTask; + if (!commitGoAhead) + { + logger.LogDebug("Update commit failed!"); + return false; + } + + logger.LogTrace("Update commit task complete"); + + // on nodes, it means we can go straight ahead + if (!swarmController) + return true; + + // on the controller, we first need to signal for nodes to go ahead + // if anything fails at this point, there's nothing we can do + logger.LogDebug("Sending remote commit message to nodes..."); + async Task SendRemoteCommitUpdate(SwarmServer swarmServer) + { + using var request = PrepareSwarmRequest( + swarmServer, + HttpMethod.Post, + SwarmConstants.UpdateRoute, + null); + + try + { + // I know using the cancellationToken after this point doesn't seem very sane + // It's the token for Ctrl+C on server's console though, so we must respect it + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Failed to send update commit request to node {0}!", swarmServer.Identifier); + } + } + + Task task; + lock (swarmServers) + task = Task.WhenAll( + swarmServers.Select( + x => SendRemoteCommitUpdate(x))); + + await task.ConfigureAwait(false); + return true; + } + + /// + public ICollection GetSwarmServers() + { + if (!SwarmMode) + return null; + + lock (swarmServers) + return swarmServers.ToList(); + } + + /// + public async Task PrepareUpdate(Version version, CancellationToken cancellationToken) + { + if (version == null) + throw new ArgumentNullException(nameof(version)); + + if (!SwarmMode) + return true; + + logger.LogTrace("Begin PrepareUpdate..."); + if (version == targetUpdateVersion) + { + logger.LogDebug("Prepare update short circuit!"); + return true; + } + + using var httpClient = httpClientFactory.CreateClient(); + async Task RemotePrepareUpdate(SwarmServer swarmServer) + { + using var request = PrepareSwarmRequest( + swarmServer, + HttpMethod.Put, + SwarmConstants.UpdateRoute, + new SwarmUpdateRequest + { + UpdateVersion = version + }); + + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + return response.IsSuccessStatusCode; + } + + if (!swarmController) + { + logger.LogDebug("Forwarding update request to swarm controller..."); + + SwarmServer controller; + lock (swarmServers) + controller = swarmServers.First(); + + return await RemotePrepareUpdate(controller).ConfigureAwait(false); + } + + var selfPrepare = await PrepareUpdateFromController(version, cancellationToken).ConfigureAwait(false); + if (!selfPrepare) + return false; + + try + { + logger.LogTrace("Sending remote prepare nodes..."); + List> tasks; + lock (swarmServers) + tasks = swarmServers.Select(x => RemotePrepareUpdate(x)).ToList(); + await Task.WhenAll(tasks); + + // if all succeeds... + if (tasks.All(x => x.Result)) + { + logger.LogDebug("Distributed prepare for update to version {0} complete.", version); + updateCommitTcs = new TaskCompletionSource(); + lock (swarmServers) + nodesThatNeedToBeReadyToCommit = new List(swarmServers.Select(x => x.Identifier)); + return true; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error remotely preparing updates!"); + } + + logger.LogDebug("Distrubuted prepare failed!"); + await AbortUpdate(cancellationToken).ConfigureAwait(false); + return false; + } + + /// + public async Task PrepareUpdateFromController(Version version, CancellationToken cancellationToken) + { + logger.LogTrace("PrepareUpdateFromController {0}...", version); + var shouldAbort = false; + try + { + lock (updateSynchronizationLock) + { + if (targetUpdateVersion == version) + { + logger.LogTrace("PrepareUpdateFromController early out, already prepared!"); + return true; + } + + if (targetUpdateVersion != null) + { + logger.LogDebug("Aborting update preparation, version {0} already prepared!", targetUpdateVersion); + shouldAbort = true; + return false; + } + + targetUpdateVersion = version; + } + + if (!swarmController) + { + updateCommitTcs = new TaskCompletionSource(); + var updateApplyResult = await serverUpdater.BeginUpdate(version, cancellationToken).ConfigureAwait(false); + if (updateApplyResult != ServerUpdateResult.Started) + { + logger.LogWarning("Failed to prepare update! Result: {0}", updateApplyResult); + shouldAbort = true; + return false; + } + } + + logger.LogDebug("Prepared for update to version {0}", version); + return true; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to prepare update!"); + shouldAbort = true; + return false; + } + finally + { + if (shouldAbort) + await AbortUpdate(cancellationToken).ConfigureAwait(false); + } + } + + /// + public async Task Initialize(CancellationToken cancellationToken) + { + if (SwarmMode) + logger.LogInformation("Swarm mode enabled"); + else + logger.LogTrace("Swarm mode disabled"); + + var _ = lazyRestartRegistration.Value; + + if (swarmController) + { + await databaseContextFactory.UseContext( + databaseContext => databaseSeeder.Initialize(databaseContext, cancellationToken)) + .ConfigureAwait(false); + if (SwarmMode) + serverHealthCheckTask = HealthCheckLoop(serverHealthCheckCancellationTokenSource.Token); + + return SwarmRegistrationResult.Success; + } + + return await RegisterWithController(cancellationToken).ConfigureAwait(false); + } + + /// + public async Task Shutdown(CancellationToken cancellationToken) + { + // downgrade the db if necessary + if (swarmController) + { + serverHealthCheckCancellationTokenSource?.Cancel(); + await serverHealthCheckTask.ConfigureAwait(false); + + if (targetUpdateVersion != null + && targetUpdateVersion < assemblyInformationProvider.Version) + await databaseContextFactory.UseContext( + db => databaseSeeder.Downgrade(db, targetUpdateVersion, cancellationToken)) + .ConfigureAwait(false); + + // we don't tell nodes about us unregistering, they'll try to reconnect eventually. + if (SwarmMode) + logger.LogTrace("Swarm controller shutdown"); + + return; + } + + // if we restart a node, we don't want to unregister it so the controller doesn't try to update without it + // if we're shutting it down, though we should unregister it + if (restarting) + { + logger.LogTrace("Not unregistering from swarm controller as we are restarting"); + return; + } + + logger.LogInformation("Unregistering from swarm controller..."); + using var httpClient = httpClientFactory.CreateClient(); + using var request = PrepareSwarmRequest( + null, + HttpMethod.Delete, + SwarmConstants.RegisterRoute, + null); + + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + + /// + /// Ping each node to see that they are still running. + /// + /// The for the operation. + /// A representing the running operation. + async Task HealthCheckNodes(CancellationToken cancellationToken) + { + using var httpClient = httpClientFactory.CreateClient(); + + List currentSwarmServers; + lock (swarmServers) + currentSwarmServers = swarmServers.ToList(); + + async Task HealthRequestForServer(SwarmServer swarmServer) + { + using var request = PrepareSwarmRequest( + swarmServer, + HttpMethod.Get, + String.Empty, + null); + + try + { + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = JsonConvert.DeserializeObject(responseString, SerializerSettings); + + if (result.Address == swarmServer.Address + && result.Identifier == swarmServer.Identifier) + return; + + logger.LogWarning("Error during swarm server health check on node '{0}'! Response: {1}. Unregistering...", swarmServer.Identifier, + responseString); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error during swarm server health check on node '{0}'! Unregistering...", swarmServer.Identifier); + } + + lock (swarmServers) + { + swarmServers.Remove(swarmServer); + registrationIds.Remove(swarmServer.Identifier); + }; + } + + await Task.WhenAll( + currentSwarmServers.Select( + x => HealthRequestForServer(x))) + .ConfigureAwait(false); + + lock (swarmServers) + if (swarmServers.Count == currentSwarmServers.Count) + return; + + await SendUpdatedServerListToNodes(cancellationToken).ConfigureAwait(false); + } + + /// + /// Ping the swarm controller to see that it is still running. If need be, reregister. + /// + /// The for the operation. + /// A representing the running operation. + async Task HealthCheckController(CancellationToken cancellationToken) + { + using var request = PrepareSwarmRequest( + null, + HttpMethod.Get, + String.Empty, + null); + using var httpClient = httpClientFactory.CreateClient(); + + try + { + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + logger.LogTrace("Health check successful"); + return; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error during swarm controller health check! Attempting to re-register..."); + controllerRegistration = null; + lastControllerHealthCheck = null; + } + + SwarmRegistrationResult registrationResult; + for (var I = 1; ; ++I) + { + logger.LogInformation("Swarm re-registration attempt {0}..."); + registrationResult = await RegisterWithController(cancellationToken).ConfigureAwait(false); + + if (registrationResult == SwarmRegistrationResult.Success) + return; + + if (registrationResult == SwarmRegistrationResult.Unauthorized) + { + logger.LogError("Swarm re-registration failed, controller's private key has changed!"); + break; + } + + if (registrationResult == SwarmRegistrationResult.VersionMismatch) + { + logger.LogError("Swarm Re-registration failed, controller's TGS version has changed!"); + break; + } + } + + // we could do something here... but what? + // best to just let the health check loop keep retrying... we won't be able to update at least + } + + /// + /// Attempt to register the node with the controller. + /// + /// The for the operation. + /// A resulting in the . + async Task RegisterWithController(CancellationToken cancellationToken) + { + logger.LogInformation("Attempting to register with swarm controller at {0}...", swarmConfiguration.ControllerAddress); + var requestedRegistrationId = Guid.NewGuid(); + + using var httpClient = httpClientFactory.CreateClient(); + using var registrationRequest = PrepareSwarmRequest( + null, + HttpMethod.Post, + SwarmConstants.RegisterRoute, + new SwarmRegistrationRequest + { + ServerVersion = assemblyInformationProvider.Version, + Identifier = swarmConfiguration.Identifier, + Address = swarmConfiguration.Address + }, + requestedRegistrationId); + + try + { + using var response = await httpClient.SendAsync(registrationRequest, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + logger.LogInformation("Sucessfully registered with ID {0}", requestedRegistrationId); + controllerRegistration = requestedRegistrationId; + lastControllerHealthCheck = DateTimeOffset.Now; + return SwarmRegistrationResult.Success; + } + + logger.LogWarning("Unable to register with swarm: HTTP {0}!", response.StatusCode); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + return SwarmRegistrationResult.Unauthorized; + + if (response.StatusCode == HttpStatusCode.UpgradeRequired) + return SwarmRegistrationResult.VersionMismatch; + + logger.LogWarning("Error registering with swarm controller: HTTP {0}", response.StatusCode); + try + { + var responseData = await response.Content.ReadAsStringAsync(); + if (!String.IsNullOrWhiteSpace(responseData)) + logger.LogDebug("Response:{0}{1}", Environment.NewLine, responseData); + } + catch (Exception ex) + { + logger.LogDebug(ex, "Error reading registration response content stream!"); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error sending registration request!"); + } + + return SwarmRegistrationResult.CommunicationFailure; + } + + /// + /// Sends the controllers list of nodes to all nodes. + /// + /// The for the operation. + /// A representing the running operation. + async Task SendUpdatedServerListToNodes(CancellationToken cancellationToken) + { + logger.LogDebug("Sending updated server list to all nodes..."); + List currentSwarmServers; + lock (swarmServers) + currentSwarmServers = swarmServers.ToList(); + + using var httpClient = httpClientFactory.CreateClient(); + async Task UpdateRequestForServer(SwarmServer swarmServer) + { + using var request = PrepareSwarmRequest( + swarmServer, + HttpMethod.Post, + String.Empty, + new SwarmServersUpdateRequest + { + SwarmServers = currentSwarmServers + }); + + try + { + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error during swarm server list update for node '{0}'! Unregistering...", swarmServer.Identifier); + + lock (swarmServers) + { + swarmServers.Remove(swarmServer); + registrationIds.Remove(swarmServer.Identifier); + } + } + } + + await Task.WhenAll(currentSwarmServers.Select(x => UpdateRequestForServer(x))).ConfigureAwait(false); + } + + /// + /// Prepares a for swarm communication. + /// + /// The the message is for, if null will be sent to swarm controller. + /// The . + /// The route on to use. + /// The body if any. + /// An optional override to the . + /// A new . + HttpRequestMessage PrepareSwarmRequest( + SwarmServer swarmServer, + HttpMethod httpMethod, + string subroute, + object body, + Guid? registrationIdOverride = null) + { + swarmServer ??= new SwarmServer + { + Address = swarmConfiguration.ControllerAddress, + }; + + var request = new HttpRequestMessage( + httpMethod, + swarmServer.Address + SwarmConstants.ControllerRoute + subroute); + + request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfiguration.PrivateKey); + request.Headers.Add(ApplicationBuilderExtensions.XPoweredByHeader, assemblyInformationProvider.VersionPrefix); + if (registrationIdOverride.HasValue) + request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdOverride.Value.ToString()); + else if (swarmController) + { + lock (swarmServers) + if (registrationIds.TryGetValue(swarmServer.Identifier, out var registrationId)) + request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationId.ToString()); + } + else if (controllerRegistration.HasValue) + request.Headers.Add(SwarmConstants.RegistrationIdHeader, controllerRegistration.Value.ToString()); + + try + { + if (body != null) + request.Content = new StringContent( + JsonConvert.SerializeObject(body, SerializerSettings)); + + return request; + } + catch + { + request.Dispose(); + throw; + } + } + + /// + public void UpdateSwarmServersList(IEnumerable swarmServers) + { + if (swarmServers == null) + throw new ArgumentNullException(nameof(swarmServers)); + + if (swarmController) + throw new InvalidOperationException("Cannot UpdateSwarmServersList on swarm controller!"); + + lock (this.swarmServers) + { + this.swarmServers.Clear(); + this.swarmServers.AddRange(swarmServers); + logger.LogDebug("Updated swarm server list with {0} total nodes", this.swarmServers.Count); + } + } + + /// + /// Timed loop for calling . + /// + /// The for the operation + /// A representing the rinning operation + async Task HealthCheckLoop(CancellationToken cancellationToken) + { + logger.LogTrace("Starting HealthCheckLoop..."); + try + { + while (!cancellationToken.IsCancellationRequested) + { + var delay = swarmController + ? TimeSpan.FromMinutes(ControllerHealthCheckIntervalMinutes) + : lastControllerHealthCheck.HasValue + ? (lastControllerHealthCheck.Value.AddMinutes(NodeHealthCheckIntervalMinutes) - DateTimeOffset.Now) + : TimeSpan.FromMinutes(NodeHealthCheckIntervalMinutes); + await asyncDelayer.Delay( + delay, + cancellationToken) + .ConfigureAwait(false); + + if (!swarmController) + { + if (!lastControllerHealthCheck.HasValue) + { + logger.LogTrace("Not registered with controller, skipping health check."); + continue; // unregistered + } + + if ((DateTimeOffset.Now - lastControllerHealthCheck.Value).TotalMinutes < NodeHealthCheckIntervalMinutes) + { + logger.LogTrace("Controller seems to be active, skipping health check."); + continue; + } + } + + logger.LogDebug("Performing swarm health check..."); + try + { + if (swarmController) + await HealthCheckNodes(cancellationToken).ConfigureAwait(false); + else + await HealthCheckController(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + logger.LogError(ex, "Health check error!"); + } + } + } + catch (OperationCanceledException ex) + { + logger.LogTrace(ex, "Health check loop cancelled!"); + } + + logger.LogTrace("Stopped HealthCheckLoop"); + } + + /// + public bool ValidateRegistration(Guid registrationId) + { + if (swarmController) + lock (swarmServers) + return registrationIds.Values.Any(x => x == registrationId); + + if (registrationId != controllerRegistration) + return false; + + lastControllerHealthCheck = DateTimeOffset.Now; + return true; + } + + /// + public bool RegisterNode(Api.Models.Internal.SwarmServer node, Guid registrationId) + { + if (node == null) + throw new ArgumentNullException(nameof(node)); + + if (node.Identifier == null) + throw new ArgumentException("Node missing Identifier!", nameof(node)); + + if (node.Address == null) + throw new ArgumentException("Node missing Address!", nameof(node)); + + if (!swarmController) + throw new InvalidOperationException("Cannot RegisterNode on swarm node!"); + + lock (updateSynchronizationLock) + { + if (targetUpdateVersion != null) + { + logger.LogInformation("Not registering node {0} as a distributed update is in progress.", node.Identifier); + return false; + } + + lock (swarmServers) + { + if (registrationIds.Any(x => x.Value == registrationId)) + { + var preExistingRegistrationKvp = registrationIds.FirstOrDefault(x => x.Value == registrationId); + if (preExistingRegistrationKvp.Key == node.Identifier) + { + logger.LogWarning("Node {0} has already registered!", node.Identifier); + return true; + } + + logger.LogWarning( + "Registration ID collision! Node {0} tried to register with {1}'s registration ID: {2}", + node.Identifier, + preExistingRegistrationKvp.Key, + registrationId); + return false; + } + + if (registrationIds.TryGetValue(node.Identifier, out var oldRegistration)) + { + logger.LogInformation("Node {0} is re-registering without first unregistering. Indicative of restart.", node.Identifier); + swarmServers.RemoveAll(x => x.Identifier == node.Identifier); + registrationIds.Remove(node.Identifier); + } + + swarmServers.Add(new SwarmServer + { + Address = node.Address, + Identifier = node.Identifier, + Controller = false, + }); + registrationIds.Add(node.Identifier, registrationId); + } + } + + logger.LogInformation("Registered node {0} with ID {1}", node.Identifier, registrationId); + return true; + } + + /// + public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) + { + restarting = true; + return Task.CompletedTask; + } + + /// + public async Task RemoteCommitRecieved(Guid registrationId, CancellationToken cancellationToken) + { + if (!swarmController) + { + logger.LogDebug("Received remote commit go ahead"); + var commitTcs = updateCommitTcs; + commitTcs?.TrySetResult(true); + return commitTcs != null; + } + + var nodeIdentifier = NodeIdentifierFromRegistration(registrationId); + if (nodeIdentifier == null) + { + // Something fucky is happening, take no chances. + logger.LogDebug("Aborting update due to unforseen circumstances!"); + await AbortUpdate(cancellationToken).ConfigureAwait(false); + return false; + } + + var nodeList = nodesThatNeedToBeReadyToCommit; + if (nodeList == null) + { + logger.LogTrace("Ignoring ready-commit from node {0} as the update appears to have been aborted.", nodeIdentifier); + return false; + } + + logger.LogDebug("Node {0} is ready to commit.", nodeIdentifier); + lock (nodeList) + { + nodeList.Remove(nodeIdentifier); + if (nodeList.Count == 0) + { + logger.LogTrace("All nodes ready, update commit is a go once controller is ready"); + var commitTcs = updateCommitTcs; + commitTcs?.TrySetResult(true); + return commitTcs != null; + } + } + + return true; + } + + /// + /// Gets the from a given . + /// + /// The registration . + /// The registered or if it does not exist. + string NodeIdentifierFromRegistration(Guid registrationId) + { + if (!swarmController) + throw new InvalidOperationException("NodeIdentifierFromRegistration on node!"); + + lock (swarmServers) + { + var exists = registrationIds.Any(x => x.Value == registrationId); + if (!exists) + { + logger.LogDebug("A node that was to be looked up ({0}) disappeared from our records!", registrationId); + return null; + } + + return registrationIds.First(x => x.Value == registrationId).Key; + } + } + + /// + public async Task UnregisterNode(Guid registrationId, CancellationToken cancellationToken) + { + if (!swarmController) + throw new InvalidOperationException("Cannot UnregisterNode on swarm node!"); + + logger.LogTrace("UnregisterNode {0}", registrationId); + var nodeIdentifier = NodeIdentifierFromRegistration(registrationId); + if (nodeIdentifier == null) + return; + + logger.LogInformation("Unregistering node {0}...", nodeIdentifier); + await AbortUpdate(cancellationToken).ConfigureAwait(false); + lock (swarmServers) + { + swarmServers.RemoveAll(x => x.Identifier == nodeIdentifier); + registrationIds.Remove(nodeIdentifier); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs new file mode 100644 index 0000000000..410794de68 --- /dev/null +++ b/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Host.Swarm +{ + /// + /// A request to update the swarm's TGS version. + /// + public sealed class SwarmUpdateRequest + { + /// + /// The TGS to update to. + /// + [Required] + public Version UpdateVersion { get; set; } + } +} From 51b6f798bb3880485ad61fb50fd4ee45ca1a6faf Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 26 Dec 2020 11:44:05 -0500 Subject: [PATCH 098/154] Increase BYOND download timeout --- tests/Tgstation.Server.Tests/Instance/ByondTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs index 4c50aab14f..ca48a4edf5 100644 --- a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs @@ -59,7 +59,7 @@ namespace Tgstation.Server.Tests.Instance var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken).ConfigureAwait(false); Assert.IsNotNull(test.InstallJob); Assert.IsNull(test.Version); - await WaitForJob(test.InstallJob, 60, false, null, cancellationToken).ConfigureAwait(false); + await WaitForJob(test.InstallJob, 120, false, null, cancellationToken).ConfigureAwait(false); var currentShit = await byondClient.ActiveVersion(cancellationToken).ConfigureAwait(false); Assert.AreEqual(newModel.Version.Semver(), currentShit.Version); From 2deb9e9521450641d4b314563ec68defd355c023 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 26 Dec 2020 20:58:13 -0500 Subject: [PATCH 099/154] Remove C#9 feature --- .../Database/DatabaseContext.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 024df2d86a..7d2b7a2d51 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -409,6 +409,9 @@ namespace Tgstation.Server.Host.Database if (currentDatabaseType == DatabaseType.PostgresSql && targetVersion < new Version(4, 3, 0)) throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!"); + if (currentDatabaseType == DatabaseType.MariaDB) + currentDatabaseType = DatabaseType.MySql; // Keeping switch expressions while avoiding `or` syntax from C#9 + if (targetVersion < new Version(4, 1, 0)) throw new NotSupportedException("Cannot migrate below version 4.1.0!"); @@ -417,7 +420,7 @@ namespace Tgstation.Server.Host.Database if (targetVersion < new Version(4, 7, 0)) targetMigration = currentDatabaseType switch { - DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYAddAdditionalDDParameters), + DatabaseType.MySql => nameof(MYAddAdditionalDDParameters), DatabaseType.PostgresSql => nameof(PGAddAdditionalDDParameters), DatabaseType.SqlServer => nameof(MSAddAdditionalDDParameters), DatabaseType.Sqlite => nameof(SLAddAdditionalDDParameters), @@ -426,7 +429,7 @@ namespace Tgstation.Server.Host.Database if (targetVersion < new Version(4, 6, 0)) targetMigration = currentDatabaseType switch { - DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYAddDeploymentColumns), + DatabaseType.MySql => nameof(MYAddDeploymentColumns), DatabaseType.PostgresSql => nameof(PGAddDeploymentColumns), DatabaseType.SqlServer => nameof(MSAddDeploymentColumns), DatabaseType.Sqlite => nameof(SLAddDeploymentColumns), @@ -435,7 +438,7 @@ namespace Tgstation.Server.Host.Database if (targetVersion < new Version(4, 5, 0)) targetMigration = currentDatabaseType switch { - DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYAllowNullDMApi), + DatabaseType.MySql => nameof(MYAllowNullDMApi), DatabaseType.PostgresSql => nameof(PGAllowNullDMApi), DatabaseType.SqlServer => nameof(MSAllowNullDMApi), DatabaseType.Sqlite => nameof(SLAllowNullDMApi), @@ -444,7 +447,7 @@ namespace Tgstation.Server.Host.Database if (targetVersion < new Version(4, 4, 0)) targetMigration = currentDatabaseType switch { - DatabaseType.MariaDB or DatabaseType.MySql => nameof(MYFixForeignKey), + DatabaseType.MySql => nameof(MYFixForeignKey), DatabaseType.PostgresSql => nameof(PGCreate), DatabaseType.SqlServer => nameof(MSRemoveSoftColumns), DatabaseType.Sqlite => nameof(SLRemoveSoftColumns), @@ -463,7 +466,7 @@ namespace Tgstation.Server.Host.Database var migrationSubstitution = currentDatabaseType switch { DatabaseType.SqlServer => null,// already setup - DatabaseType.MySql or DatabaseType.MariaDB => "MY{0}", + DatabaseType.MySql => "MY{0}", DatabaseType.Sqlite => "SL{0}", DatabaseType.PostgresSql => "PG{0}", _ => throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"), From 5acc7c6c54ce026edd513810771b847a7da8262f Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 26 Dec 2020 21:23:10 -0500 Subject: [PATCH 100/154] README updates for swarm --- README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 07fa42e3fb..36be4c01b0 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ The latter two are not recommended as they cannot be dynamically changed at runt #### Manual Configuration -Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon's running). Note these are all case-sensitive: +Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon instances running). Note these are all case-sensitive: - `General:ConfigVersion`: Suppresses warnings about out of date config versions. You should change this after updating TGS to one with a new config version. The current version can be found on the releases page for your server version (This field did not exist before v4.4.0). @@ -130,13 +130,29 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `ControlPanel:AllowedOrigins`: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by `ControlPanel:AllowAnyOrigin` -- `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: +- `Swarm`: This section should be left `null` unless using the server swarm system. If this is to happen, ensure all swarm servers are set to connect to the same database. + +- `Swarm:PrivateKey`: Should be a secure string set identically on all swarmed servers. + +- `Swarm:ControllerAddress`: Should be set on all swarmed servers that are **not** the controller server and should be an address the controller server may be reached at. + +- `Swarm:Address`: Should be set on all swarmed servers. Should be an address the server can be reached at by other servers in the swarm. + +- `Swarm:Identifier` should be set uniquely on all swarmed servers. Used to identify the current server. This is also used to select which instances exist on the current machine and should not be changed post-setup. + +- `Security:OAuth:`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry: ```json -"GitHubOAuth":{ - "ClientId": "...", - "ClientSecret": "...", - "RedirectUrl": "...", (Used with certain providers) - "ServerUrl": "...", (Used with certain providers) +{ + "Security": { + "OAuth": { + "Keycloak": { + "ClientId": "...", + "ClientSecret": "...", + "RedirectUrl": "..." + "ServerUrl": "..." + } + } + } } ``` The following providers use the `RedirectUrl` setting: @@ -310,6 +326,12 @@ Example VirtualHost Entry ``` +## Swarmed Servers + +Multiple tgstation-servers can be linked together in a swarm. The main benefit of this is allowing for users, groups, and permissions to be shared across the servers. Servers in a swarm must connect to the same database, use the same tgstation-server version, and have their own unique names. + +In a swarm, one server is designated the 'controller'. This is the server other 'node's in the swarm communicate with and coordinates group updates. Issuing an update command to one server in a swarm will update them all to the specified version. + ## Usage tgstation-server v4 is controlled via a RESTful HTTP json API. Documentation on this API can be found [here](https://tgstation.github.io/tgstation-server/api.html). This section serves to document the concepts of the server. The API is versioned separately from the release version. A specification for it can be found in the api-vX.X.X git releases/tags. From dd18c5253fbe17e4b17b0aedaaf0f97195712487 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 26 Dec 2020 21:44:35 -0500 Subject: [PATCH 101/154] Rename Version to ServerInformation in client --- src/Tgstation.Server.Client/IServerClient.cs | 6 +++--- src/Tgstation.Server.Client/ServerClient.cs | 2 +- tests/Tgstation.Server.Tests/RootTest.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs index 9732e57352..048d7eb53e 100644 --- a/src/Tgstation.Server.Client/IServerClient.cs +++ b/src/Tgstation.Server.Client/IServerClient.cs @@ -46,11 +46,11 @@ namespace Tgstation.Server.Client IUserGroupsClient Groups { get; } /// - /// The of the + /// The of the /// /// The for the operation - /// A resulting in the of the target server - Task Version(CancellationToken cancellationToken); + /// A resulting in the of the target server + Task ServerInformation(CancellationToken cancellationToken); /// /// Adds a to the request pipeline diff --git a/src/Tgstation.Server.Client/ServerClient.cs b/src/Tgstation.Server.Client/ServerClient.cs index 0704caafb7..e1b8756558 100644 --- a/src/Tgstation.Server.Client/ServerClient.cs +++ b/src/Tgstation.Server.Client/ServerClient.cs @@ -75,7 +75,7 @@ namespace Tgstation.Server.Client public void Dispose() => apiClient.Dispose(); /// - public Task Version(CancellationToken cancellationToken) => apiClient.Read(Routes.Root, cancellationToken); + public Task ServerInformation(CancellationToken cancellationToken) => apiClient.Read(Routes.Root, cancellationToken); /// public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger); diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs index 4d2c023c62..f3fd6feb68 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RootTest.cs @@ -154,7 +154,7 @@ namespace Tgstation.Server.Tests async Task TestServerInformation(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken) { - var serverInfo = await serverClient.Version(default).ConfigureAwait(false); + var serverInfo = await serverClient.ServerInformation(default).ConfigureAwait(false); Assert.AreEqual(ApiHeaders.Version, serverInfo.ApiVersion); var assemblyVersion = typeof(IServer).Assembly.GetName().Version.Semver(); From 4bf6095211f0ba464d2df5a5aa9b605e975ac2a9 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 11:21:38 -0500 Subject: [PATCH 102/154] Removed non-standard slash from BridgeController route --- src/Tgstation.Server.Host/Controllers/BridgeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index fd8cc46620..8bb613ff4d 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Controllers /// /// for recieving DMAPI requests from DreamDaemon. /// - [Route("Bridge")] + [Route("/Bridge")] [Produces(MediaTypeNames.Application.Json)] [ApiController] public class BridgeController : Controller From 9e8321689475a63887c98ef88297ca9d0e6db3fe Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 11:22:21 -0500 Subject: [PATCH 103/154] Ensure updated server list is sent out when a node (de)registers --- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index c450365f15..0fd6d761ed 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -162,6 +162,11 @@ namespace Tgstation.Server.Host.Swarm /// bool restarting; + /// + /// If the list has been updated and needs to be resent to clients. + /// + bool serversDirty; + /// /// Initializes a new instance of the . /// @@ -623,7 +628,7 @@ namespace Tgstation.Server.Host.Swarm .ConfigureAwait(false); lock (swarmServers) - if (swarmServers.Count == currentSwarmServers.Count) + if (!serversDirty && swarmServers.Count == currentSwarmServers.Count) return; await SendUpdatedServerListToNodes(cancellationToken).ConfigureAwait(false); @@ -787,6 +792,7 @@ namespace Tgstation.Server.Host.Swarm } await Task.WhenAll(currentSwarmServers.Select(x => UpdateRequestForServer(x))).ConfigureAwait(false); + serversDirty = false; } /// @@ -812,7 +818,7 @@ namespace Tgstation.Server.Host.Swarm var request = new HttpRequestMessage( httpMethod, - swarmServer.Address + SwarmConstants.ControllerRoute + subroute); + swarmServer.Address + SwarmConstants.ControllerRoute.Substring(1) + subroute); request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfiguration.PrivateKey); request.Headers.Add(ApplicationBuilderExtensions.XPoweredByHeader, assemblyInformationProvider.VersionPrefix); @@ -1086,6 +1092,8 @@ namespace Tgstation.Server.Host.Swarm swarmServers.RemoveAll(x => x.Identifier == nodeIdentifier); registrationIds.Remove(nodeIdentifier); } + + serversDirty = true; } } } From 08cf743d5ad7f6f5ed6c7583a9cf6dceec979fb6 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 11:23:41 -0500 Subject: [PATCH 104/154] Log HttpApiPort on startup --- src/Tgstation.Server.Host/Core/Application.cs | 4 +++- .../Extensions/WebHostBuilderExtensions.cs | 3 ++- .../Core/TestApplication.cs | 20 +++++++++++-------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 10e52d48ea..8d0a203961 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -371,6 +371,7 @@ namespace Tgstation.Server.Host.Core /// The for the /// The value of /// The . + /// The . /// The containing the to use /// The containing the to use /// The for the @@ -379,6 +380,7 @@ namespace Tgstation.Server.Host.Core IServerControl serverControl, ITokenFactory tokenFactory, IInstanceManager instanceManager, + IServerPortProvider serverPortProvider, IOptions controlPanelConfigurationOptions, IOptions generalConfigurationOptions, ILogger logger) @@ -493,7 +495,7 @@ namespace Tgstation.Server.Host.Core logger.LogTrace("DMAPI version: {0}", masterVersionsAttribute.RawDMApiVersion); logger.LogTrace("Web control panel version: {0}", masterVersionsAttribute.RawControlPanelVersion); - logger.LogDebug("Starting hosting..."); + logger.LogDebug("Starting hosting on port {0}...", serverPortProvider.HttpApiPort); } } } diff --git a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs index 09f3b4bb51..e1fafbddc5 100644 --- a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -43,6 +43,7 @@ namespace Tgstation.Server.Host.Extensions applicationBuilder.ApplicationServices.GetRequiredService(), applicationBuilder.ApplicationServices.GetRequiredService(), applicationBuilder.ApplicationServices.GetRequiredService(), + applicationBuilder.ApplicationServices.GetRequiredService(), applicationBuilder.ApplicationServices.GetRequiredService>(), applicationBuilder.ApplicationServices.GetRequiredService>(), applicationBuilder.ApplicationServices.GetRequiredService>())); diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index b94d88c5ad..672fadd9c9 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Configuration; @@ -31,28 +31,32 @@ namespace Tgstation.Server.Host.Core.Tests var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object); Assert.ThrowsException(() => app.ConfigureServices(null)); - Assert.ThrowsException(() => app.Configure(null, null, null, null, null, null, null)); + Assert.ThrowsException(() => app.Configure(null, null, null, null, null, null, null, null)); var mockAppBuilder = new Mock(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null)); var mockServerControl = new Mock(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null)); var mockTokenFactory = new Mock(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null)); var mockInstanceManager = new Mock(); mockInstanceManager.SetupGet(x => x.Ready).Returns(Extensions.TaskExtensions.InfiniteTask()); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, null, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, null, null, null, null)); + + var mockServerPortProvider = new Mock(); + mockServerPortProvider.SetupGet(x => x.HttpApiPort).Returns(5345); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, null, null, null)); var mockControlPanelOptions = new Mock>(); mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockControlPanelOptions.Object, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, mockControlPanelOptions.Object, null, null)); var mockGeneralOptions = new Mock>(); mockGeneralOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockControlPanelOptions.Object, mockGeneralOptions.Object, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, mockControlPanelOptions.Object, mockGeneralOptions.Object, null)); mockControlPanelOptions.VerifyAll(); mockGeneralOptions.VerifyAll(); } From 8c8ac15f26174c5cea1a7f89017ef87a245d0040 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 11:24:22 -0500 Subject: [PATCH 105/154] Add Swarm configuration to setup wizard --- .../Setup/SetupWizard.cs | 86 ++++++++++++++++++- .../Setup/TestSetupWizard.cs | 17 ++++ 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index efecf6cd04..6a7c99e03b 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -747,6 +747,65 @@ namespace Tgstation.Server.Host.Setup return config; } + /// + /// Prompts the user to create a . + /// + /// The for the operation. + /// A resulting in the new . + async Task ConfigureSwarm(CancellationToken cancellationToken) + { + var enable = await PromptYesNo("Enable swarm mode? (y/n): ", cancellationToken).ConfigureAwait(false); + if (!enable) + return null; + + string identifer; + do + { + await console.WriteAsync("Enter this server's identifer: ", false, cancellationToken).ConfigureAwait(false); + identifer = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); + } + while (String.IsNullOrWhiteSpace(identifer)); + + async Task ParseAddress(string question) + { + Uri address; + do + { + await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false); + var addressString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); + if (Uri.TryCreate(addressString, UriKind.Absolute, out address) + && address.Scheme != Uri.UriSchemeHttp + && address.Scheme != Uri.UriSchemeHttps) + address = null; + } + while (address == null); + + return address; + } + + var address = await ParseAddress("Enter this server's HTTP(S) address: ").ConfigureAwait(false); + string privateKey; + do + { + await console.WriteAsync("Enter the swarm private key: ", false, cancellationToken).ConfigureAwait(false); + privateKey = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); + } + while (String.IsNullOrWhiteSpace(privateKey)); + + var controller = await PromptYesNo("Is this server the swarm's controller? (y/n): ", cancellationToken).ConfigureAwait(false); + Uri controllerAddress = null; + if (!controller) + controllerAddress = await ParseAddress("Enter the swarm controller's HTTP(S) address: ").ConfigureAwait(false); + + return new SwarmConfiguration + { + Address = address, + ControllerAddress = controllerAddress, + Identifier = identifer, + PrivateKey = privateKey, + }; + } + /// /// Saves a given set to /// @@ -756,9 +815,18 @@ namespace Tgstation.Server.Host.Setup /// The to save /// The to save /// The to save + /// The to save. /// The for the operation /// A representing the running operation - async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken) + async Task SaveConfiguration( + string userConfigFileName, + ushort? hostingPort, + DatabaseConfiguration databaseConfiguration, + GeneralConfiguration newGeneralConfiguration, + FileLoggingConfiguration fileLoggingConfiguration, + ControlPanelConfiguration controlPanelConfiguration, + SwarmConfiguration swarmConfiguration, + CancellationToken cancellationToken) { await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false); @@ -769,7 +837,8 @@ namespace Tgstation.Server.Host.Setup { DatabaseConfiguration.Section, databaseConfiguration }, { GeneralConfiguration.Section, newGeneralConfiguration }, { FileLoggingConfiguration.Section, fileLoggingConfiguration }, - { ControlPanelConfiguration.Section, controlPanelConfiguration } + { ControlPanelConfiguration.Section, controlPanelConfiguration }, + { SwarmConfiguration.Section, swarmConfiguration }, }; var json = JsonConvert.SerializeObject(map, Formatting.Indented); @@ -826,9 +895,20 @@ namespace Tgstation.Server.Host.Setup var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false); + var swarmConfiguration = await ConfigureSwarm(cancellationToken).ConfigureAwait(false); + await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); - await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false); + await SaveConfiguration( + userConfigFileName, + hostingPort, + databaseConfiguration, + newGeneralConfiguration, + fileLoggingConfiguration, + controlPanelConfiguration, + swarmConfiguration, + cancellationToken) + .ConfigureAwait(false); } /// diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs index f45af4db4b..2667e33a3f 100644 --- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs +++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs @@ -177,6 +177,8 @@ namespace Tgstation.Server.Host.Setup.Tests //cp config "y", "y", + // swarm config + "n", //saved, now for second run //this time use defaults amap String.Empty, @@ -204,6 +206,15 @@ namespace Tgstation.Server.Host.Setup.Tests "y", "n", String.Empty, + //swarm config + "y", + "node1", + "not a url", + "net.tcp://notandhttpAddress.com", + "http://node1:3400", + "privatekey", + "n", + "http://controller.com", //third run, we already hit all the code coverage so just get through it String.Empty, nameof(DatabaseType.MariaDB), @@ -229,6 +240,12 @@ namespace Tgstation.Server.Host.Setup.Tests "y", "n", "http://fake.com, https://example.org", + //swarm config + "y", + "controller", + "https://controller.com", + "privatekey", + "y" }; var inputPos = 0; From 975ed3dee4c89f40f759a46933cdd85a0c292ce2 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 12:29:48 -0500 Subject: [PATCH 106/154] Swarm system fixes round #1 - Fixed controller sub-routing. - Improved logging. - Reduced health check intervals. - Fixed including controller in node-only messages. - Fixed a NullReferenceException when not in swarm mode. - Always reference controller using configured address --- .../Controllers/SwarmController.cs | 11 ++- .../Swarm/SwarmConstants.cs | 4 +- .../Swarm/SwarmService.cs | 72 +++++++++++++------ 3 files changed, 57 insertions(+), 30 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index cb4fa051c9..ef91dd1457 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -196,9 +196,11 @@ namespace Tgstation.Server.Host.Controllers /// public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { + using var _ = LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}"); + logger.LogTrace("Swarm request from {0}...", Request.HttpContext.Connection.RemoteIpAddress); if (swarmConfiguration.PrivateKey == null) { - logger.LogDebug("Attempted swarm request without private key!"); + logger.LogDebug("Attempted swarm request without private key configured!"); await Forbid().ExecuteResultAsync(context).ConfigureAwait(false); return; } @@ -233,11 +235,8 @@ namespace Tgstation.Server.Host.Controllers return; } - using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}")) - { - logger.LogDebug("Starting swarm request..."); - await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); - } + logger.LogDebug("Starting swarm request processing..."); + await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs index 6581c0602a..c5a5d4fbbf 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs @@ -25,11 +25,11 @@ namespace Tgstation.Server.Host.Swarm /// /// The route used for swarm registration. /// - public const string RegisterRoute = "/Register"; + public const string RegisterRoute = "Register"; /// /// The route used for swarm updates. /// - public const string UpdateRoute = "/Update"; + public const string UpdateRoute = "Update"; } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 0fd6d761ed..9d54cc62da 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -27,12 +27,12 @@ namespace Tgstation.Server.Host.Swarm /// /// Interval at which the swarm controller makes health checks on nodes. /// - const int ControllerHealthCheckIntervalMinutes = 5; + const int ControllerHealthCheckIntervalMinutes = 3; /// /// Interval at which the node makes health checks on the controller if it has not received one. /// - const int NodeHealthCheckIntervalMinutes = 7; + const int NodeHealthCheckIntervalMinutes = 5; /// /// See for the swarm system. @@ -277,8 +277,10 @@ namespace Tgstation.Server.Host.Swarm { lock (swarmServers) task = Task.WhenAll( - swarmServers.Select( - x => SendRemoteAbort(x))); + swarmServers + .Where(x => !x.Controller) + .Select( + x => SendRemoteAbort(x))); } await task.ConfigureAwait(false); @@ -363,8 +365,10 @@ namespace Tgstation.Server.Host.Swarm Task task; lock (swarmServers) task = Task.WhenAll( - swarmServers.Select( - x => SendRemoteCommitUpdate(x))); + swarmServers + .Where(x => !x.Controller) + .Select( + x => SendRemoteCommitUpdate(x))); await task.ConfigureAwait(false); return true; @@ -416,11 +420,7 @@ namespace Tgstation.Server.Host.Swarm { logger.LogDebug("Forwarding update request to swarm controller..."); - SwarmServer controller; - lock (swarmServers) - controller = swarmServers.First(); - - return await RemotePrepareUpdate(controller).ConfigureAwait(false); + return await RemotePrepareUpdate(null).ConfigureAwait(false); } var selfPrepare = await PrepareUpdateFromController(version, cancellationToken).ConfigureAwait(false); @@ -432,7 +432,10 @@ namespace Tgstation.Server.Host.Swarm logger.LogTrace("Sending remote prepare nodes..."); List> tasks; lock (swarmServers) - tasks = swarmServers.Select(x => RemotePrepareUpdate(x)).ToList(); + tasks = swarmServers + .Where(x => !x.Controller) + .Select(x => RemotePrepareUpdate(x)) + .ToList(); await Task.WhenAll(tasks); // if all succeeds... @@ -441,7 +444,10 @@ namespace Tgstation.Server.Host.Swarm logger.LogDebug("Distributed prepare for update to version {0} complete.", version); updateCommitTcs = new TaskCompletionSource(); lock (swarmServers) - nodesThatNeedToBeReadyToCommit = new List(swarmServers.Select(x => x.Identifier)); + nodesThatNeedToBeReadyToCommit = new List( + swarmServers + .Where(x => !x.Controller) + .Select(x => x.Identifier)); return true; } } @@ -512,7 +518,11 @@ namespace Tgstation.Server.Host.Swarm public async Task Initialize(CancellationToken cancellationToken) { if (SwarmMode) - logger.LogInformation("Swarm mode enabled"); + logger.LogInformation( + "Swarm mode enabled ({0})", + swarmController + ? "controller" + : "node"); else logger.LogTrace("Swarm mode disabled"); @@ -539,7 +549,8 @@ namespace Tgstation.Server.Host.Swarm if (swarmController) { serverHealthCheckCancellationTokenSource?.Cancel(); - await serverHealthCheckTask.ConfigureAwait(false); + if (serverHealthCheckTask != null) + await serverHealthCheckTask.ConfigureAwait(false); if (targetUpdateVersion != null && targetUpdateVersion < assemblyInformationProvider.Version) @@ -607,12 +618,17 @@ namespace Tgstation.Server.Host.Swarm && result.Identifier == swarmServer.Identifier) return; - logger.LogWarning("Error during swarm server health check on node '{0}'! Response: {1}. Unregistering...", swarmServer.Identifier, + logger.LogWarning( + "Error during swarm server health check on node '{0}'! Response: {1}. Unregistering...", + swarmServer.Identifier, responseString); } catch (Exception ex) { - logger.LogWarning(ex, "Error during swarm server health check on node '{0}'! Unregistering...", swarmServer.Identifier); + logger.LogWarning( + ex, + "Error during swarm server health check on node '{0}'! Unregistering...", + swarmServer.Identifier); } lock (swarmServers) @@ -623,9 +639,11 @@ namespace Tgstation.Server.Host.Swarm } await Task.WhenAll( - currentSwarmServers.Select( - x => HealthRequestForServer(x))) - .ConfigureAwait(false); + currentSwarmServers + .Where(x => !x.Controller) + .Select( + x => HealthRequestForServer(x))) + .ConfigureAwait(false); lock (swarmServers) if (!serversDirty && swarmServers.Count == currentSwarmServers.Count) @@ -791,7 +809,10 @@ namespace Tgstation.Server.Host.Swarm } } - await Task.WhenAll(currentSwarmServers.Select(x => UpdateRequestForServer(x))).ConfigureAwait(false); + await Task.WhenAll( + currentSwarmServers + .Where(x => !x.Controller) + .Select(x => UpdateRequestForServer(x))).ConfigureAwait(false); serversDirty = false; } @@ -816,9 +837,16 @@ namespace Tgstation.Server.Host.Swarm Address = swarmConfiguration.ControllerAddress, }; + subroute = $"{SwarmConstants.ControllerRoute}/{subroute}"; + logger.LogTrace( + "{0} {1} to swarm server {2}", + httpMethod, + subroute, + swarmServer.Identifier ?? swarmServer.Address.ToString()); + var request = new HttpRequestMessage( httpMethod, - swarmServer.Address + SwarmConstants.ControllerRoute.Substring(1) + subroute); + swarmServer.Address + subroute); request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfiguration.PrivateKey); request.Headers.Add(ApplicationBuilderExtensions.XPoweredByHeader, assemblyInformationProvider.VersionPrefix); From ff20c9bbc95804fc099366926ae438a1ce380ec8 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 17:43:59 -0500 Subject: [PATCH 107/154] More swarm fixes. - Fix client routing again. - Swap X-Powered-By for User-Agent in clients - Set Accept header to application/json. --- .../Extensions/ApplicationBuilderExtensions.cs | 4 +--- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index 96ed8a2952..28f3d93034 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -17,8 +17,6 @@ namespace Tgstation.Server.Host.Extensions /// static class ApplicationBuilderExtensions { - public const string XPoweredByHeader = "X-Powered-By"; - /// /// Gets a from a given /// @@ -131,7 +129,7 @@ namespace Tgstation.Server.Host.Extensions applicationBuilder.Use(async (context, next) => { - context.Response.Headers.Add(XPoweredByHeader, assemblyInformationProvider.VersionPrefix); + context.Response.Headers.Add("X-Powered-By", assemblyInformationProvider.VersionPrefix); await next().ConfigureAwait(false); }); } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 9d54cc62da..880277c87e 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -7,13 +7,14 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Extensions.Converters; using Tgstation.Server.Host.System; @@ -846,10 +847,13 @@ namespace Tgstation.Server.Host.Swarm var request = new HttpRequestMessage( httpMethod, - swarmServer.Address + subroute); + swarmServer.Address + subroute.Substring(1)); request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfiguration.PrivateKey); - request.Headers.Add(ApplicationBuilderExtensions.XPoweredByHeader, assemblyInformationProvider.VersionPrefix); + request.Headers.UserAgent.Clear(); + request.Headers.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue); + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); if (registrationIdOverride.HasValue) request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdOverride.Value.ToString()); else if (swarmController) From e37908aaa9f05c983d218bb20dee85c043d5afed Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 17:51:23 -0500 Subject: [PATCH 108/154] Properly set Content-Type in swarm clients --- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 880277c87e..f6550411a8 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -9,6 +9,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; +using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -869,7 +870,9 @@ namespace Tgstation.Server.Host.Swarm { if (body != null) request.Content = new StringContent( - JsonConvert.SerializeObject(body, SerializerSettings)); + JsonConvert.SerializeObject(body, SerializerSettings), + Encoding.UTF8, + MediaTypeNames.Application.Json); return request; } From 09b049a11964f9c71c2331b0c55be45707d61b5f Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 20:10:20 -0500 Subject: [PATCH 109/154] Fix updating swarm node server list --- .../Swarm/SwarmService.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index f6550411a8..2da4d811a2 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -612,18 +612,7 @@ namespace Tgstation.Server.Host.Swarm { using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var result = JsonConvert.DeserializeObject(responseString, SerializerSettings); - - if (result.Address == swarmServer.Address - && result.Identifier == swarmServer.Identifier) - return; - - logger.LogWarning( - "Error during swarm server health check on node '{0}'! Response: {1}. Unregistering...", - swarmServer.Identifier, - responseString); + return; } catch (Exception ex) { @@ -648,10 +637,11 @@ namespace Tgstation.Server.Host.Swarm .ConfigureAwait(false); lock (swarmServers) - if (!serversDirty && swarmServers.Count == currentSwarmServers.Count) - return; + if (swarmServers.Count != currentSwarmServers.Count) + serversDirty = true; - await SendUpdatedServerListToNodes(cancellationToken).ConfigureAwait(false); + if (serversDirty) + await SendUpdatedServerListToNodes(cancellationToken).ConfigureAwait(false); } /// @@ -814,7 +804,8 @@ namespace Tgstation.Server.Host.Swarm await Task.WhenAll( currentSwarmServers .Where(x => !x.Controller) - .Select(x => UpdateRequestForServer(x))).ConfigureAwait(false); + .Select(x => UpdateRequestForServer(x))) + .ConfigureAwait(false); serversDirty = false; } @@ -1033,6 +1024,7 @@ namespace Tgstation.Server.Host.Swarm } logger.LogInformation("Registered node {0} with ID {1}", node.Identifier, registrationId); + serversDirty = true; return true; } From f5fcf331d3f30f8b632cfb413b62f8d57ddfd0c2 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sun, 27 Dec 2020 20:21:30 -0500 Subject: [PATCH 110/154] Switch all DateTimeOffset.Now references to UtcNow --- .../RateLimitException.cs | 2 +- .../Chat/Providers/DiscordProvider.cs | 2 +- .../Components/Chat/Providers/IrcProvider.cs | 2 +- .../Components/Deployment/DmbFactory.cs | 2 +- .../Components/Deployment/DreamMaker.cs | 2 +- .../Components/Repository/Repository.cs | 6 +++--- .../Components/Session/SessionController.cs | 4 ++-- .../Components/Watchdog/WatchdogBase.cs | 2 +- .../Controllers/ApiController.cs | 2 +- .../Controllers/RepositoryController.cs | 2 +- .../Controllers/UserController.cs | 4 ++-- .../Database/DatabaseSeeder.cs | 4 ++-- src/Tgstation.Server.Host/Jobs/JobManager.cs | 6 +++--- .../Security/CryptographySuite.cs | 2 +- .../Security/IdentityCacheObject.cs | 4 ++-- .../Security/OAuth/TGForumsOAuthValidator.cs | 4 ++-- .../Security/TokenFactory.cs | 2 +- .../Swarm/SwarmService.cs | 8 ++++---- .../Transfer/FileTransferService.cs | 4 ++-- .../Components/TestDreamDaemonClient.cs | 4 ++-- .../Tgstation.Server.Tests/IntegrationTest.cs | 18 +++++++++--------- 21 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Tgstation.Server.Client/RateLimitException.cs b/src/Tgstation.Server.Client/RateLimitException.cs index c42a9b69af..acef0d769e 100644 --- a/src/Tgstation.Server.Client/RateLimitException.cs +++ b/src/Tgstation.Server.Client/RateLimitException.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Client var secondsString = values.FirstOrDefault(); if (UInt32.TryParse(secondsString, out var seconds)) - RetryAfter = DateTimeOffset.Now.AddSeconds(seconds); + RetryAfter = DateTimeOffset.UtcNow.AddSeconds(seconds); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 629b39382c..4b60ad50fb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -461,7 +461,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var completionString = errorMessage == null ? "Succeeded" : "Failed"; builder.Footer.Text = completionString; builder.Color = errorMessage == null ? Color.Green : Color.Red; - builder.Timestamp = DateTimeOffset.Now; + builder.Timestamp = DateTimeOffset.UtcNow; builder.Description = errorMessage == null ? "The deployment completed successfully and will be available at the next server reboot." : "The deployment failed."; diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 2767163710..a554d12637 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -619,7 +619,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers ? byondVersion.ToString() : $"{byondVersion.Major}.{byondVersion.Minor}", estimatedCompletionTime.HasValue - ? $" ETA: {estimatedCompletionTime - DateTimeOffset.Now}" + ? $" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}" : String.Empty), cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index ac78dd6fe4..874de6b9a6 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -270,7 +270,7 @@ namespace Tgstation.Server.Host.Components.Deployment // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{0}...", compileJob.Job.Id); - compileJob.Job.StoppedAt = DateTimeOffset.Now; + compileJob.Job.StoppedAt = DateTimeOffset.UtcNow; } var providerSubmitted = false; diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 4d9f1aa9b4..7b2d5f1b07 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -807,7 +807,7 @@ namespace Tgstation.Server.Host.Components.Deployment currentChatCallback = chatManager.QueueDeploymentMessage( revisionInformation, byondLock.Version, - DateTimeOffset.Now + estimatedDuration, + DateTimeOffset.UtcNow + estimatedDuration, repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, localCommitExistsOnRemote); diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index e04c113935..3ab698c381 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -293,7 +293,7 @@ namespace Tgstation.Server.Host.Components.Repository MergeResult result = null; - var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now); + var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow); await Task.Factory.StartNew(() => { try @@ -576,7 +576,7 @@ namespace Tgstation.Server.Host.Components.Repository trackedBranch.FriendlyName, committerName, committerEmail); - result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.Now), new MergeOptions + result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.UtcNow), new MergeOptions { CommitOnSuccess = true, FailOnConflict = true, @@ -748,7 +748,7 @@ namespace Tgstation.Server.Host.Components.Repository new Signature( DefaultCommitterName, DefaultCommitterEmail, - DateTimeOffset.Now), + DateTimeOffset.UtcNow), new MergeOptions { FastForwardStrategy = FastForwardStrategy.FastForwardOnly, diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index b064a63c73..1462b6cd50 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -300,7 +300,7 @@ namespace Tgstation.Server.Host.Components.Session bool reattached, bool apiValidate) { - var startTime = DateTimeOffset.Now; + var startTime = DateTimeOffset.UtcNow; var useBridgeRequestForLaunchResult = !reattached && (apiValidate || DMApiAvailable); var startupTask = useBridgeRequestForLaunchResult ? initialBridgeRequestTcs.Task @@ -320,7 +320,7 @@ namespace Tgstation.Server.Host.Components.Session var result = new LaunchResult { ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null, - StartupTime = startupTask.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null + StartupTime = startupTask.IsCompleted ? (TimeSpan?)(DateTimeOffset.UtcNow - startTime) : null }; logger.LogTrace("Launch result: {0}", result); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index cf873601f4..5a24b01076 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -980,7 +980,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var dumpFileName = diagnosticsIOManager.ResolvePath( diagnosticsIOManager.ConcatPath( DumpDirectory, - $"DreamDaemon-{DateTimeOffset.Now.ToFileStamp()}.dmp")); + $"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp")); var session = GetActiveController(); if (session?.Lifetime.IsCompleted != false) diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 2f2aee2b8a..2d7fd10387 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -143,7 +143,7 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(rateLimitException)); Logger.LogWarning(rateLimitException, "Exceeded GitHub rate limit!"); - var secondsString = Math.Ceiling((rateLimitException.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture); + var secondsString = Math.Ceiling((rateLimitException.Reset - DateTimeOffset.UtcNow).TotalSeconds).ToString(CultureInfo.InvariantCulture); Response.Headers.Add(HeaderNames.RetryAfter, secondsString); return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit)); } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 1a900e812f..c76b40682b 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -815,7 +815,7 @@ namespace Tgstation.Server.Host.Controllers { Author = ex.Message, BodyAtMerge = ex.Message, - MergedAt = DateTimeOffset.Now, + MergedAt = DateTimeOffset.UtcNow, TitleAtMerge = ex.Message, Comment = I.Comment, Number = I.Number, diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index c1d14a5d49..f0f3c80888 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -250,7 +250,7 @@ namespace Tgstation.Server.Host.Controllers if (model.Enabled.HasValue) { if (originalUser.Enabled.Value && !model.Enabled.Value) - originalUser.LastPasswordUpdate = DateTimeOffset.Now; + originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow; originalUser.Enabled = model.Enabled.Value; } @@ -431,7 +431,7 @@ namespace Tgstation.Server.Host.Controllers return new Models.User { - CreatedAt = DateTimeOffset.Now, + CreatedAt = DateTimeOffset.UtcNow, CreatedBy = AuthenticationContext.User, Enabled = model.Enabled ?? false, PermissionSet = permissionSet, diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 9b5816ab16..9e1c6afd79 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -83,7 +83,7 @@ namespace Tgstation.Server.Host.Database bool alreadyExists = tgsUser != null; tgsUser ??= new User() { - CreatedAt = DateTimeOffset.Now, + CreatedAt = DateTimeOffset.UtcNow, CanonicalName = User.CanonicalizeName(User.TgsSystemUserName), }; @@ -111,7 +111,7 @@ namespace Tgstation.Server.Host.Database AdministrationRights = RightsHelper.AllRights(), InstanceManagerRights = RightsHelper.AllRights(), }, - CreatedAt = DateTimeOffset.Now, + CreatedAt = DateTimeOffset.UtcNow, Name = Api.Models.User.AdminName, CanonicalName = User.CanonicalizeName(Api.Models.User.AdminName), Enabled = true, diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 891d4bc409..afd47a3b6b 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -147,7 +147,7 @@ namespace Tgstation.Server.Host.Jobs }; databaseContext.Jobs.Attach(attachedJob); - attachedJob.StoppedAt = DateTimeOffset.Now; + attachedJob.StoppedAt = DateTimeOffset.UtcNow; attachedJob.ExceptionDetails = job.ExceptionDetails; attachedJob.ErrorCode = job.ErrorCode; attachedJob.Cancelled = job.Cancelled; @@ -177,7 +177,7 @@ namespace Tgstation.Server.Host.Jobs if (operation == null) throw new ArgumentNullException(nameof(operation)); - job.StartedAt = DateTimeOffset.Now; + job.StartedAt = DateTimeOffset.UtcNow; job.Cancelled = false; job.Instance = new Models.Instance @@ -238,7 +238,7 @@ namespace Tgstation.Server.Host.Jobs var job = new Job { Id = I }; databaseContext.Jobs.Attach(job); job.Cancelled = true; - job.StoppedAt = DateTimeOffset.Now; + job.StoppedAt = DateTimeOffset.UtcNow; } await databaseContext.Save(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Security/CryptographySuite.cs b/src/Tgstation.Server.Host/Security/CryptographySuite.cs index 979ca8b5df..491d245ecd 100644 --- a/src/Tgstation.Server.Host/Security/CryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/CryptographySuite.cs @@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Security throw new ArgumentNullException(nameof(newPassword)); user.PasswordHash = passwordHasher.HashPassword(user, newPassword); if (!newUser) - user.LastPasswordUpdate = DateTimeOffset.Now; + user.LastPasswordUpdate = DateTimeOffset.UtcNow; } /// diff --git a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs index 9b54996f93..1684935a6c 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs @@ -41,9 +41,9 @@ namespace Tgstation.Server.Host.Security if (onExpiry == null) throw new ArgumentNullException(nameof(onExpiry)); - var now = DateTimeOffset.Now; + var now = DateTimeOffset.UtcNow; if (expiry < now) - throw new ArgumentOutOfRangeException(nameof(expiry), expiry, "expiry must be greater than DateTimeOffset.Now!"); + throw new ArgumentOutOfRangeException(nameof(expiry), expiry, "expiry must be greater than DateTimeOffset.UtcNow!"); cancellationTokenSource = new CancellationTokenSource(); diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index 4e6028987e..3024e61001 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -56,7 +56,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// public override async Task GetProviderInfo(CancellationToken cancellationToken) { - var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.Now); + var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.UtcNow); if (expiredSessions > 0) Logger.LogTrace("Expired {0} sessions", expiredSessions); @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Security.OAuth sessions.Add( Tuple.Create( newSession, - DateTimeOffset.Now)); + DateTimeOffset.UtcNow)); return new OAuthProviderInfo { ClientId = newSession.SessionPublicToken, diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 2c9a86df45..d4deedbb62 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Security if (user == null) throw new ArgumentNullException(nameof(user)); - var now = DateTimeOffset.Now; + var now = DateTimeOffset.UtcNow; var nowUnix = now.ToUnixTimeSeconds(); // this prevents validation conflicts down the line diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 2da4d811a2..29f9d2da45 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -728,7 +728,7 @@ namespace Tgstation.Server.Host.Swarm { logger.LogInformation("Sucessfully registered with ID {0}", requestedRegistrationId); controllerRegistration = requestedRegistrationId; - lastControllerHealthCheck = DateTimeOffset.Now; + lastControllerHealthCheck = DateTimeOffset.UtcNow; return SwarmRegistrationResult.Success; } @@ -906,7 +906,7 @@ namespace Tgstation.Server.Host.Swarm var delay = swarmController ? TimeSpan.FromMinutes(ControllerHealthCheckIntervalMinutes) : lastControllerHealthCheck.HasValue - ? (lastControllerHealthCheck.Value.AddMinutes(NodeHealthCheckIntervalMinutes) - DateTimeOffset.Now) + ? (lastControllerHealthCheck.Value.AddMinutes(NodeHealthCheckIntervalMinutes) - DateTimeOffset.UtcNow) : TimeSpan.FromMinutes(NodeHealthCheckIntervalMinutes); await asyncDelayer.Delay( delay, @@ -921,7 +921,7 @@ namespace Tgstation.Server.Host.Swarm continue; // unregistered } - if ((DateTimeOffset.Now - lastControllerHealthCheck.Value).TotalMinutes < NodeHealthCheckIntervalMinutes) + if ((DateTimeOffset.UtcNow - lastControllerHealthCheck.Value).TotalMinutes < NodeHealthCheckIntervalMinutes) { logger.LogTrace("Controller seems to be active, skipping health check."); continue; @@ -960,7 +960,7 @@ namespace Tgstation.Server.Host.Swarm if (registrationId != controllerRegistration) return false; - lastControllerHealthCheck = DateTimeOffset.Now; + lastControllerHealthCheck = DateTimeOffset.UtcNow; return true; } diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index 812e2c11f7..08c2649b86 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -127,12 +127,12 @@ namespace Tgstation.Server.Host.Transfer async Task ExpireAsync() { - var expireAt = DateTimeOffset.Now + TimeSpan.FromMinutes(TicketValidityMinutes); + var expireAt = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(TicketValidityMinutes); try { await oldExpireTask.WithToken(disposeCts.Token).ConfigureAwait(false); - var now = DateTimeOffset.Now; + var now = DateTimeOffset.UtcNow; if (now < expireAt) await asyncDelayer.Delay(expireAt - now, disposeCts.Token).ConfigureAwait(false); } diff --git a/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs b/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs index a70423e97c..60d40191ad 100644 --- a/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs +++ b/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; using System.Collections.Generic; @@ -19,7 +19,7 @@ namespace Tgstation.Server.Client.Components.Tests var example = new Job { Id = 347, - StartedAt = DateTimeOffset.Now + StartedAt = DateTimeOffset.UtcNow }; var inst = new Instance diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index bb105f31fb..77fd0425a3 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -122,7 +122,7 @@ namespace Tgstation.Server.Tests async Task CreateAdminClient(Uri url, CancellationToken cancellationToken) { - var giveUpAt = DateTimeOffset.Now.AddSeconds(60); + var giveUpAt = DateTimeOffset.UtcNow.AddSeconds(60); for(var I = 1; ; ++I) { try @@ -139,14 +139,14 @@ namespace Tgstation.Server.Tests catch (HttpRequestException) { //migrating, to be expected - if (DateTimeOffset.Now > giveUpAt) + if (DateTimeOffset.UtcNow > giveUpAt) throw; await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } catch (ServiceUnavailableException) { // migrating, to be expected - if (DateTimeOffset.Now > giveUpAt) + if (DateTimeOffset.UtcNow > giveUpAt) throw; await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } @@ -227,7 +227,7 @@ namespace Tgstation.Server.Tests var user = new Host.Models.User { Name = TestUserName, - CreatedAt = DateTimeOffset.Now, + CreatedAt = DateTimeOffset.UtcNow, OAuthConnections = new List(), CanonicalName = Host.Models.User.CanonicalizeName(TestUserName), Enabled = false, @@ -350,7 +350,7 @@ namespace Tgstation.Server.Tests } catch (Exception ex) { - Console.WriteLine($"[{DateTimeOffset.Now}] TEST ERROR: {ex}"); + Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}"); serverCts.Cancel(); throw; } @@ -396,7 +396,7 @@ namespace Tgstation.Server.Tests await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); - var preStartupTime = DateTimeOffset.Now; + var preStartupTime = DateTimeOffset.UtcNow; serverTask = server.Run(cancellationToken); using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) @@ -440,7 +440,7 @@ namespace Tgstation.Server.Tests await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); - preStartupTime = DateTimeOffset.Now; + preStartupTime = DateTimeOffset.UtcNow; serverTask = server.Run(cancellationToken); using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) { @@ -481,12 +481,12 @@ namespace Tgstation.Server.Tests } catch(ApiException ex) { - Console.WriteLine($"[{DateTimeOffset.Now}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}"); + Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}"); throw; } catch (Exception ex) { - Console.WriteLine($"[{DateTimeOffset.Now}] TEST ERROR: {ex}"); + Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}"); throw; } finally From 83c7828e38123056eb70a7f18c767acc2a83f557 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 10:59:11 -0500 Subject: [PATCH 111/154] Don't set InstancePermissionSet for instances not on the swarm node --- .../Security/AuthenticationContextFactory.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index bef18b9b7c..be393e8db9 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -1,9 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; @@ -30,19 +32,27 @@ namespace Tgstation.Server.Host.Security /// readonly ILogger logger; + /// + /// The for the . + /// + readonly SwarmConfiguration swarmConfiguration; + /// /// Construct an /// /// The value of /// The value of + /// The containing the value of . /// The value of . public AuthenticationContextFactory( IDatabaseContext databaseContext, IIdentityCache identityCache, + IOptions swarmConfigurationOptions, ILogger logger) { this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); + swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -96,7 +106,7 @@ namespace Tgstation.Server.Host.Security { instancePermissionSet = await databaseContext.InstancePermissionSets .AsQueryable() - .Where(x => x.PermissionSetId == userPermissionSet.Id && x.InstanceId == instanceId) + .Where(x => x.PermissionSetId == userPermissionSet.Id && x.InstanceId == instanceId && x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -105,7 +115,10 @@ namespace Tgstation.Server.Host.Security logger.LogDebug("User {0} does not have permissions on instance {1}!", userId, instanceId.Value); } - CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instancePermissionSet); + CurrentAuthenticationContext = new AuthenticationContext( + systemIdentity, + user, + instancePermissionSet); } catch { From e22830007a72821301744b970ac687c6002ccac7 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 10:59:47 -0500 Subject: [PATCH 112/154] Some fixes related to update commit and abort --- .../Controllers/SwarmController.cs | 2 +- .../Swarm/ISwarmOperations.cs | 9 ++- .../Swarm/ISwarmService.cs | 9 ++- .../Swarm/ISwarmServiceBase.cs | 18 ------ .../Swarm/SwarmService.cs | 62 +++++++++++++------ 5 files changed, 59 insertions(+), 41 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index ef91dd1457..1c2409dd8a 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -189,7 +189,7 @@ namespace Tgstation.Server.Host.Controllers if (!ValidateRegistration()) return Forbid(); - await swarmOperations.AbortUpdate(cancellationToken).ConfigureAwait(false); + await swarmOperations.RemoteAbortUpdate(cancellationToken).ConfigureAwait(false); return NoContent(); } diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs index 3cdd9f1774..2b7ccf26d4 100644 --- a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs +++ b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Swarm /// /// Swarm service operations for the . /// - public interface ISwarmOperations : ISwarmServiceBase + public interface ISwarmOperations { /// /// Pass in an updated list of to the node. @@ -55,5 +55,12 @@ namespace Tgstation.Server.Host.Swarm /// The for the operation. /// A representing the running operation. Task RemoteCommitRecieved(Guid registrationId, CancellationToken cancellationToken); + + /// + /// Remotely abort an uncommitted update. + /// + /// The for the operation. + /// A representing the running operation. + Task RemoteAbortUpdate(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs index cb3f8252fb..1c80b46928 100644 --- a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Swarm /// /// Used for swarm operations. Functions may be no-op based on configuration. /// - public interface ISwarmService : ISwarmServiceBase + public interface ISwarmService { /// /// Attempt to register with the swarm controller if not one, sets up the database otherwise. @@ -45,5 +45,12 @@ namespace Tgstation.Server.Host.Swarm /// /// A of s in the swarm. If the server is not part of a swarm, will be returned. ICollection GetSwarmServers(); + + /// + /// Abort an uncommitted update. + /// + /// The for the operation. + /// A representing the running operation. + Task AbortUpdate(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs b/src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs deleted file mode 100644 index 5ae46cebff..0000000000 --- a/src/Tgstation.Server.Host/Swarm/ISwarmServiceBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Swarm -{ - /// - /// For aborting swarm updates. - /// - public interface ISwarmServiceBase - { - /// - /// Abort an uncommitted update. - /// - /// The for the operation. - /// A representing the running operation. - Task AbortUpdate(CancellationToken cancellationToken); - } -} diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 29f9d2da45..20a178c852 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -238,18 +238,24 @@ namespace Tgstation.Server.Host.Swarm /// public void Dispose() => serverHealthCheckCancellationTokenSource?.Dispose(); + /// + public async Task RemoteAbortUpdate(CancellationToken cancellationToken) + { + if (targetUpdateVersion == null) + { + logger.LogTrace("Not remote aborting non-existent update"); + return; + } + + await AbortUpdate(cancellationToken).ConfigureAwait(false); + } + /// public async Task AbortUpdate(CancellationToken cancellationToken) { if (!SwarmMode) return; - if (targetUpdateVersion == null) - { - logger.LogTrace("Not aborting non-exitent update"); - return; - } - logger.LogInformation("Aborting swarm update!"); updateCommitTcs?.TrySetResult(false); updateCommitTcs = null; @@ -324,6 +330,7 @@ namespace Tgstation.Server.Host.Swarm if (commitTcsTask == null) { logger.LogDebug("Update commit failed, no pending task completion source!"); + await AbortUpdate(cancellationToken).ConfigureAwait(false); return false; } @@ -331,6 +338,7 @@ namespace Tgstation.Server.Host.Swarm if (!commitGoAhead) { logger.LogDebug("Update commit failed!"); + await AbortUpdate(cancellationToken).ConfigureAwait(false); return false; } @@ -425,7 +433,7 @@ namespace Tgstation.Server.Host.Swarm return await RemotePrepareUpdate(null).ConfigureAwait(false); } - var selfPrepare = await PrepareUpdateFromController(version, cancellationToken).ConfigureAwait(false); + var selfPrepare = await PrepareUpdateImpl(version, true, cancellationToken).ConfigureAwait(false); if (!selfPrepare) return false; @@ -438,18 +446,13 @@ namespace Tgstation.Server.Host.Swarm .Where(x => !x.Controller) .Select(x => RemotePrepareUpdate(x)) .ToList(); + await Task.WhenAll(tasks); // if all succeeds... if (tasks.All(x => x.Result)) { logger.LogDebug("Distributed prepare for update to version {0} complete.", version); - updateCommitTcs = new TaskCompletionSource(); - lock (swarmServers) - nodesThatNeedToBeReadyToCommit = new List( - swarmServers - .Where(x => !x.Controller) - .Select(x => x.Identifier)); return true; } } @@ -464,9 +467,19 @@ namespace Tgstation.Server.Host.Swarm } /// - public async Task PrepareUpdateFromController(Version version, CancellationToken cancellationToken) + public Task PrepareUpdateFromController(Version version, CancellationToken cancellationToken) + => PrepareUpdateImpl(version, false, cancellationToken); + + /// + /// Implementation of , + /// + /// The being updated to. + /// Whether or not the update request originated on this server. + /// The for the operation. + /// A resulting in whether or not the update should proceed. + async Task PrepareUpdateImpl(Version version, bool initiator, CancellationToken cancellationToken) { - logger.LogTrace("PrepareUpdateFromController {0}...", version); + logger.LogTrace("PrepareUpdateImpl {0}...", version); var shouldAbort = false; try { @@ -488,16 +501,25 @@ namespace Tgstation.Server.Host.Swarm targetUpdateVersion = version; } - if (!swarmController) - { - updateCommitTcs = new TaskCompletionSource(); - var updateApplyResult = await serverUpdater.BeginUpdate(version, cancellationToken).ConfigureAwait(false); + updateCommitTcs = new TaskCompletionSource(); + if (!initiator) + { + var updateApplyResult = await serverUpdater.BeginUpdate( + version, + cancellationToken) + .ConfigureAwait(false); if (updateApplyResult != ServerUpdateResult.Started) { logger.LogWarning("Failed to prepare update! Result: {0}", updateApplyResult); shouldAbort = true; return false; } + + lock (swarmServers) + nodesThatNeedToBeReadyToCommit = new List( + swarmServers + .Where(x => !x.Controller) + .Select(x => x.Identifier)); } logger.LogDebug("Prepared for update to version {0}", version); @@ -1058,7 +1080,7 @@ namespace Tgstation.Server.Host.Swarm var nodeList = nodesThatNeedToBeReadyToCommit; if (nodeList == null) { - logger.LogTrace("Ignoring ready-commit from node {0} as the update appears to have been aborted.", nodeIdentifier); + logger.LogDebug("Ignoring ready-commit from node {0} as the update appears to have been aborted.", nodeIdentifier); return false; } From bc90c13322be5ef3bd30a2617f55f87bb3830dd9 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 11:00:10 -0500 Subject: [PATCH 113/154] Adds swarm integration test --- .../Tgstation.Server.Tests/IntegrationTest.cs | 180 +++++++++++++++++- tests/Tgstation.Server.Tests/TestingServer.cs | 19 +- 2 files changed, 190 insertions(+), 9 deletions(-) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 77fd0425a3..f2c960e0f5 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -43,7 +43,7 @@ namespace Tgstation.Server.Tests [TestMethod] public async Task TestUpdateProtocolAndDisabledOAuth() { - using var server = new TestingServer(false); + using var server = new TestingServer(null, false); using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; var serverTask = server.Run(cancellationToken); @@ -113,6 +113,178 @@ namespace Tgstation.Server.Tests Assert.IsTrue(server.RestartRequested, "Server not requesting restart!"); } + [TestMethod] + public async Task TestSwarm() + { + const string PrivateKey = "adlfj73ywifhks7iwrgfegjs"; + + var controllerAddress = new Uri("http://localhost:5011"); + using var controller = new TestingServer(new SwarmConfiguration + { + Address = controllerAddress, + Identifier = "controller", + PrivateKey = PrivateKey + }, false, 5011); + using var node1 = new TestingServer(new SwarmConfiguration + { + Address = new Uri("http://localhost:5012"), + ControllerAddress = controllerAddress, + Identifier = "node1", + PrivateKey = PrivateKey + }, false, 5012); + using var node2 = new TestingServer(new SwarmConfiguration + { + Address = new Uri("http://localhost:5013"), + ControllerAddress = controllerAddress, + Identifier = "node2", + PrivateKey = PrivateKey + }, false, 5013); + using var serverCts = new CancellationTokenSource(); + var cancellationToken = serverCts.Token; + var serverTask = Task.WhenAll( + node1.Run(cancellationToken), + node2.Run(cancellationToken), + controller.Run(cancellationToken)); + + try + { + using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); + using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); + using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + + var controllerInfo = await controllerClient.ServerInformation(cancellationToken); + + async Task WaitForSwarmServerUpdate() + { + ServerInformation serverInformation; + do + { + await Task.Delay(TimeSpan.FromSeconds(10)); + serverInformation = await node1Client.ServerInformation(cancellationToken); + } + while (serverInformation.SwarmServers.Count == 1); + } + + static void CheckInfo(ServerInformation serverInformation) + { + Assert.IsNotNull(serverInformation.SwarmServers); + Assert.AreEqual(3, serverInformation.SwarmServers.Count); + + var node1 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node1"); + Assert.IsNotNull(node1); + Assert.AreEqual(node1.Address, "http://localhost:5012"); + Assert.IsFalse(node1.Controller); + + var node2 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"); + Assert.IsNotNull(node2); + Assert.AreEqual(node2.Address, "http://localhost:5013"); + Assert.IsFalse(node2.Controller); + + var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"); + Assert.IsNotNull(controller); + Assert.AreEqual(controller.Address, "http://localhost:5011"); + Assert.IsTrue(controller.Controller); + } + + CheckInfo(controllerInfo); + + // wait a few minutes for the updated server list to dispatch + await Task.WhenAny( + WaitForSwarmServerUpdate(), + Task.Delay(TimeSpan.FromMinutes(4))); + + var node2Info = await node2Client.ServerInformation(cancellationToken); + var node1Info = await node1Client.ServerInformation(cancellationToken); + CheckInfo(node1Info); + CheckInfo(node2Info); + + // check user info is shared + var newUser = await node2Client.Users.Create(new UserUpdate + { + Name = "asdf", + Password = "asdfasdfasdfasdf", + Enabled = true, + PermissionSet = new PermissionSet + { + AdministrationRights = AdministrationRights.ChangeVersion + } + }, cancellationToken); + + var node1User = await node1Client.Users.GetId(newUser, cancellationToken); + Assert.AreEqual(newUser.Name, node1User.Name); + Assert.AreEqual(newUser.Enabled, node1User.Enabled); + + using var controllerUserClient = await clientFactory.CreateFromLogin( + controllerAddress, + newUser.Name, + "asdfasdfasdfasdf"); + + using var node1BadClient = clientFactory.CreateFromToken(node1.Url, controllerUserClient.Token); + await Assert.ThrowsExceptionAsync(() => node1BadClient.Administration.Read(cancellationToken)); + + // check instance info is not shared + var controllerInstance = await controllerClient.Instances.CreateOrAttach( + new Api.Models.Instance + { + Name = "ControllerInstance", + Path = Path.Combine(controller.Directory, "ControllerInstance") + }, + cancellationToken); + + var node2Instance = await node2Client.Instances.CreateOrAttach( + new Api.Models.Instance + { + Name = "Node2Instance", + Path = Path.Combine(node2.Directory, "Node2Instance") + }, + cancellationToken); + var node2InstanceList = await node2Client.Instances.List(null, cancellationToken); + Assert.AreEqual(1, node2InstanceList.Count); + Assert.AreEqual(node2Instance.Id, node2InstanceList.First().Id); + Assert.IsNotNull(await node2Client.Instances.GetId(node2Instance, cancellationToken)); + var controllerInstanceList = await controllerClient.Instances.List(null, cancellationToken); + Assert.AreEqual(1, controllerInstanceList.Count); + Assert.AreEqual(controllerInstance.Id, controllerInstanceList.First().Id); + Assert.IsNotNull(await controllerClient.Instances.GetId(controllerInstance, cancellationToken)); + + await Assert.ThrowsExceptionAsync(() => controllerClient.Instances.GetId(node2Instance, cancellationToken)); + await Assert.ThrowsExceptionAsync(() => node1Client.Instances.GetId(controllerInstance, cancellationToken)); + + // test update + var testUpdateVersion = new Version(4, 6, 2); + await node1Client.Administration.Update( + new Administration + { + NewVersion = testUpdateVersion + }, + cancellationToken); + await Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(2)), serverTask); + Assert.IsTrue(serverTask.IsCompleted); + + void CheckServerUpdated(TestingServer server) + { + Assert.IsTrue(Directory.Exists(server.UpdatePath), "Update directory not present!"); + + var updatedAssemblyPath = Path.Combine(server.UpdatePath, "Tgstation.Server.Host.dll"); + Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!"); + + var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath); + Assert.AreEqual(testUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver()); + } + + CheckServerUpdated(controller); + CheckServerUpdated(node1); + CheckServerUpdated(node2); + } + finally + { + serverCts.Cancel(); + await serverTask; + } + + Directory.Delete(Path.GetDirectoryName(controller.Directory), true); + } + static void TerminateAllDDs() { foreach (var proc in System.Diagnostics.Process.GetProcessesByName("DreamDaemon")) @@ -122,7 +294,7 @@ namespace Tgstation.Server.Tests async Task CreateAdminClient(Uri url, CancellationToken cancellationToken) { - var giveUpAt = DateTimeOffset.UtcNow.AddSeconds(60); + var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2); for(var I = 1; ; ++I) { try @@ -311,7 +483,7 @@ namespace Tgstation.Server.Tests Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!"); } - using var server = new TestingServer(true); + using var server = new TestingServer(null, true); const int MaximumTestMinutes = 20; using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaximumTestMinutes)); @@ -554,7 +726,7 @@ namespace Tgstation.Server.Tests [TestMethod] public async Task TestRepoParentLookup() { - using var testingServer = new TestingServer(false); + using var testingServer = new TestingServer(null, false); LibGit2Sharp.Repository.Clone("https://github.com/Cyberboss/test", testingServer.Directory); var libGit2Repo = new LibGit2Sharp.Repository(testingServer.Directory); using var repo = new Host.Components.Repository.Repository( diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 6a0788af86..f9c888e341 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -32,13 +32,13 @@ namespace Tgstation.Server.Tests IServer realServer; - public TestingServer(bool enableOAuth) + public TestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth, ushort port = 5010) { Directory = Environment.GetEnvironmentVariable("TGS4_TEST_TEMP_DIRECTORY"); if (String.IsNullOrWhiteSpace(Directory)) { Directory = Path.Combine(Path.GetTempPath(), "TGS4_INTEGRATION_TEST"); - if (System.IO.Directory.Exists(Directory)) + if (System.IO.Directory.Exists(Directory) && swarmConfiguration == null) try { System.IO.Directory.Delete(Directory, true); @@ -49,8 +49,8 @@ namespace Tgstation.Server.Tests Directory = Path.Combine(Directory, Guid.NewGuid().ToString()); System.IO.Directory.CreateDirectory(Directory); - const string UrlString = "http://localhost:5010"; - Url = new Uri(UrlString); + string urlString = $"http://localhost:{port}"; + Url = new Uri(urlString); //so we need a db //we have to rely on env vars @@ -73,7 +73,7 @@ namespace Tgstation.Server.Tests var args = new List() { String.Format(CultureInfo.InvariantCulture, "Database:DropDatabase={0}", true), - String.Format(CultureInfo.InvariantCulture, "General:ApiPort={0}", 5010), + String.Format(CultureInfo.InvariantCulture, "General:ApiPort={0}", port), String.Format(CultureInfo.InvariantCulture, "Database:DatabaseType={0}", DatabaseType), String.Format(CultureInfo.InvariantCulture, "Database:ConnectionString={0}", connectionString), String.Format(CultureInfo.InvariantCulture, "General:SetupWizardMode={0}", SetupWizardMode.Never), @@ -87,6 +87,15 @@ namespace Tgstation.Server.Tests "General:ByondTopicTimeout=3000" }; + if (swarmConfiguration != null) + { + args.Add($"Swarm:PrivateKey={swarmConfiguration.PrivateKey}"); + args.Add($"Swarm:Identifier={swarmConfiguration.Identifier}"); + args.Add($"Swarm:Address={swarmConfiguration.Address}"); + if (swarmConfiguration.ControllerAddress != null) + args.Add($"Swarm:ControllerAddress={swarmConfiguration.ControllerAddress}"); + } + // enable all oauth providers if (enableOAuth) foreach (var I in Enum.GetValues(typeof(OAuthProvider))) From 44834b1be76f1fc72defa7aa933e0c390c472134 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 13:12:58 -0500 Subject: [PATCH 114/154] Improve details of a log message --- src/Tgstation.Server.Host/Components/InstanceManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index f50154764e..4e272e0012 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -203,7 +203,7 @@ namespace Tgstation.Server.Host.Components { if (!instances.TryGetValue(metadata.Id, out var instance)) { - logger.LogTrace("Cannot reference instance {0} as it is not online!", metadata.Id); + logger.LogTrace("Cannot reference instance {0} as it is not online or on this node!", metadata.Id); return null; } From b3d7454a5f7b9f0cdcae7ef668391580f2153169 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 13:14:44 -0500 Subject: [PATCH 115/154] Add UpdateInProgress to ServerInformation --- .../Models/ServerInformation.cs | 5 +++++ .../Controllers/HomeController.cs | 11 ++++++++++- .../Core/IServerControl.cs | 5 +++++ src/Tgstation.Server.Host/Server.cs | 18 ++++++++---------- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs index 23f7f4cfa6..16b7ea0732 100644 --- a/src/Tgstation.Server.Api/Models/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs @@ -28,6 +28,11 @@ namespace Tgstation.Server.Api.Models /// public bool WindowsHost { get; set; } + /// + /// If there is a server update in progress. + /// + public bool UpdateInProgress { get; set; } + /// /// A of connected s. /// diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index ec9bb367eb..23a539405e 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -73,6 +73,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly ISwarmService swarmService; + /// + /// The for the . + /// + readonly IServerControl serverControl; + /// /// The for the /// @@ -102,6 +107,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of /// The value of . + /// The value of . /// The containing the value of . /// The containing the value of /// The for the @@ -117,6 +123,7 @@ namespace Tgstation.Server.Host.Controllers IPlatformIdentifier platformIdentifier, IBrowserResolver browserResolver, ISwarmService swarmService, + IServerControl serverControl, IOptions generalConfigurationOptions, IOptions controlPanelConfigurationOptions, ILogger logger) @@ -135,6 +142,7 @@ namespace Tgstation.Server.Host.Controllers this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders)); this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver)); this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); } @@ -186,7 +194,8 @@ namespace Tgstation.Server.Host.Controllers ValidInstancePaths = generalConfiguration.ValidInstancePaths, WindowsHost = platformIdentifier.IsWindows, SwarmServers = swarmService.GetSwarmServers(), - OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken).ConfigureAwait(false) + OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken).ConfigureAwait(false), + UpdateInProgress = serverControl.UpdateInProgress, }); } #pragma warning restore CA1506 diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index 97a60b263b..a7ca77c1b5 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -15,6 +15,11 @@ namespace Tgstation.Server.Host.Core /// bool WatchdogPresent { get; } + /// + /// Whether or not the server is currently updating + /// + bool UpdateInProgress { get; } + /// /// Run a new assembly and stop the current one. This will likely trigger all active s /// diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index f8f9c91a73..3f98fbe38a 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -21,6 +21,9 @@ namespace Tgstation.Server.Host /// public bool RestartRequested { get; private set; } + /// + public bool UpdateInProgress { get; private set; } + /// public bool WatchdogPresent => #if WATCHDOG_FREE_RESTART @@ -74,11 +77,6 @@ namespace Tgstation.Server.Host /// Exception propagatedException; - /// - /// If a server update has been or is being applied - /// - bool updating; - /// /// Construct a /// @@ -181,13 +179,13 @@ namespace Tgstation.Server.Host lock (restartLock) { - if (updating || RestartRequested) + if (UpdateInProgress || RestartRequested) { logger.LogTrace("Aborted due to concurrency conflict!"); return false; } - updating = true; + UpdateInProgress = true; } async void RunUpdate() @@ -244,7 +242,7 @@ namespace Tgstation.Server.Host } catch (Exception e) { - updating = false; + UpdateInProgress = false; try { // important to not leave this directory around if possible @@ -271,7 +269,7 @@ namespace Tgstation.Server.Host } finally { - updating = false; + UpdateInProgress = false; } } @@ -321,7 +319,7 @@ namespace Tgstation.Server.Host lock (restartLock) { - if ((updating && newVersion == null) || RestartRequested) + if ((UpdateInProgress && newVersion == null) || RestartRequested) { logger.LogTrace("Aborted due to concurrency conflict!"); return; From 337e653ebda03d88d27d3f810f9e4c4c10931565 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 13:31:29 -0500 Subject: [PATCH 116/154] Further swarm update commit fixes Also adds a 10 minute timeout on the controller. --- .../Swarm/SwarmService.cs | 170 ++++++++++-------- 1 file changed, 100 insertions(+), 70 deletions(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 20a178c852..9843b8657a 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -36,6 +36,11 @@ namespace Tgstation.Server.Host.Swarm /// const int NodeHealthCheckIntervalMinutes = 5; + /// + /// Number of minutes the controller waits to receive a ready-commit from all nodes before aborting an update. + /// + const int UpdateCommitTimeoutMinutes = 10; + /// /// See for the swarm system. /// @@ -334,10 +339,26 @@ namespace Tgstation.Server.Host.Swarm return false; } - var commitGoAhead = await commitTcsTask.ConfigureAwait(false) && updateCommitTcs?.Task == commitTcsTask; + var timeoutTask = swarmController + ? asyncDelayer.Delay( + TimeSpan.FromMinutes(UpdateCommitTimeoutMinutes), + cancellationToken) + : Extensions.TaskExtensions.InfiniteTask(); + + var commitTask = Task.WhenAny(commitTcsTask, timeoutTask); + + await commitTask.ConfigureAwait(false); + + var commitGoAhead = commitTcsTask.IsCompleted + && commitTcsTask.Result + && updateCommitTcs?.Task == commitTcsTask; if (!commitGoAhead) { - logger.LogDebug("Update commit failed!"); + logger.LogDebug( + "Update commit failed!{0}", + timeoutTask.IsCompleted + ? " Timed out!" + : String.Empty); await AbortUpdate(cancellationToken).ConfigureAwait(false); return false; } @@ -395,15 +416,36 @@ namespace Tgstation.Server.Host.Swarm } /// - public async Task PrepareUpdate(Version version, CancellationToken cancellationToken) + public Task PrepareUpdate(Version version, CancellationToken cancellationToken) + { + logger.LogTrace("Begin PrepareUpdate..."); + return PrepareUpdateImpl(version, true, cancellationToken); + } + + /// + public Task PrepareUpdateFromController(Version version, CancellationToken cancellationToken) + { + logger.LogTrace("Received remote update request from {0}", !swarmController ? "controller" : "node"); + return PrepareUpdateImpl(version, false, cancellationToken); + } + + /// + /// Implementation of , + /// + /// The being updated to. + /// Whether or not the update request originated on this server. + /// The for the operation. + /// A resulting in whether or not the update should proceed. + async Task PrepareUpdateImpl(Version version, bool initiator, CancellationToken cancellationToken) { if (version == null) throw new ArgumentNullException(nameof(version)); + logger.LogTrace("PrepareUpdateImpl {0}...", version); + if (!SwarmMode) return true; - logger.LogTrace("Begin PrepareUpdate..."); if (version == targetUpdateVersion) { logger.LogDebug("Prepare update short circuit!"); @@ -426,60 +468,6 @@ namespace Tgstation.Server.Host.Swarm return response.IsSuccessStatusCode; } - if (!swarmController) - { - logger.LogDebug("Forwarding update request to swarm controller..."); - - return await RemotePrepareUpdate(null).ConfigureAwait(false); - } - - var selfPrepare = await PrepareUpdateImpl(version, true, cancellationToken).ConfigureAwait(false); - if (!selfPrepare) - return false; - - try - { - logger.LogTrace("Sending remote prepare nodes..."); - List> tasks; - lock (swarmServers) - tasks = swarmServers - .Where(x => !x.Controller) - .Select(x => RemotePrepareUpdate(x)) - .ToList(); - - await Task.WhenAll(tasks); - - // if all succeeds... - if (tasks.All(x => x.Result)) - { - logger.LogDebug("Distributed prepare for update to version {0} complete.", version); - return true; - } - } - catch (Exception ex) - { - logger.LogWarning(ex, "Error remotely preparing updates!"); - } - - logger.LogDebug("Distrubuted prepare failed!"); - await AbortUpdate(cancellationToken).ConfigureAwait(false); - return false; - } - - /// - public Task PrepareUpdateFromController(Version version, CancellationToken cancellationToken) - => PrepareUpdateImpl(version, false, cancellationToken); - - /// - /// Implementation of , - /// - /// The being updated to. - /// Whether or not the update request originated on this server. - /// The for the operation. - /// A resulting in whether or not the update should proceed. - async Task PrepareUpdateImpl(Version version, bool initiator, CancellationToken cancellationToken) - { - logger.LogTrace("PrepareUpdateImpl {0}...", version); var shouldAbort = false; try { @@ -501,9 +489,20 @@ namespace Tgstation.Server.Host.Swarm targetUpdateVersion = version; } - updateCommitTcs = new TaskCompletionSource(); + if (!swarmController && initiator) + { + logger.LogDebug("Forwarding update request to swarm controller..."); + var result = await RemotePrepareUpdate(null).ConfigureAwait(false); + if (result) + updateCommitTcs = new TaskCompletionSource(); + + return result; + } + if (!initiator) - { + { + logger.LogTrace("Beginning local update process..."); + updateCommitTcs = new TaskCompletionSource(); var updateApplyResult = await serverUpdater.BeginUpdate( version, cancellationToken) @@ -514,16 +513,9 @@ namespace Tgstation.Server.Host.Swarm shouldAbort = true; return false; } - - lock (swarmServers) - nodesThatNeedToBeReadyToCommit = new List( - swarmServers - .Where(x => !x.Controller) - .Select(x => x.Identifier)); } logger.LogDebug("Prepared for update to version {0}", version); - return true; } catch (Exception ex) { @@ -536,6 +528,43 @@ namespace Tgstation.Server.Host.Swarm if (shouldAbort) await AbortUpdate(cancellationToken).ConfigureAwait(false); } + + if (!swarmController) + return true; + + try + { + logger.LogTrace("Sending remote prepare to nodes..."); + List> tasks; + lock (swarmServers) + { + nodesThatNeedToBeReadyToCommit = new List( + swarmServers + .Where(x => !x.Controller) + .Select(x => x.Identifier)); + tasks = swarmServers + .Where(x => !x.Controller) + .Select(x => RemotePrepareUpdate(x)) + .ToList(); + } + + await Task.WhenAll(tasks); + + // if all succeeds... + if (tasks.All(x => x.Result)) + { + logger.LogDebug("Distributed prepare for update to version {0} complete.", version); + return true; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error remotely preparing updates!"); + } + + logger.LogDebug("Distrubuted prepare failed!"); + await AbortUpdate(cancellationToken).ConfigureAwait(false); + return false; } /// @@ -543,10 +572,11 @@ namespace Tgstation.Server.Host.Swarm { if (SwarmMode) logger.LogInformation( - "Swarm mode enabled ({0})", + "Swarm mode enabled: {0} {1}", swarmController - ? "controller" - : "node"); + ? "Controller" + : "Node", + swarmConfiguration.Identifier); else logger.LogTrace("Swarm mode disabled"); From f25fda747d6bef4d57e2b41625b4ee30de35292d Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 13:31:49 -0500 Subject: [PATCH 117/154] Improved cleanup in swarm test --- .../Tgstation.Server.Tests/IntegrationTest.cs | 285 +++++++++--------- 1 file changed, 145 insertions(+), 140 deletions(-) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index f2c960e0f5..a4fa6f3b86 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -116,173 +116,178 @@ namespace Tgstation.Server.Tests [TestMethod] public async Task TestSwarm() { + // cleanup existing directories + new TestingServer(null, false).Dispose(); + const string PrivateKey = "adlfj73ywifhks7iwrgfegjs"; var controllerAddress = new Uri("http://localhost:5011"); - using var controller = new TestingServer(new SwarmConfiguration + using (var controller = new TestingServer(new SwarmConfiguration { Address = controllerAddress, Identifier = "controller", PrivateKey = PrivateKey - }, false, 5011); - using var node1 = new TestingServer(new SwarmConfiguration + }, false, 5011)) { - Address = new Uri("http://localhost:5012"), - ControllerAddress = controllerAddress, - Identifier = "node1", - PrivateKey = PrivateKey - }, false, 5012); - using var node2 = new TestingServer(new SwarmConfiguration - { - Address = new Uri("http://localhost:5013"), - ControllerAddress = controllerAddress, - Identifier = "node2", - PrivateKey = PrivateKey - }, false, 5013); - using var serverCts = new CancellationTokenSource(); - var cancellationToken = serverCts.Token; - var serverTask = Task.WhenAll( - node1.Run(cancellationToken), - node2.Run(cancellationToken), - controller.Run(cancellationToken)); - - try - { - using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); - using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); - using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); - - var controllerInfo = await controllerClient.ServerInformation(cancellationToken); - - async Task WaitForSwarmServerUpdate() + using var node1 = new TestingServer(new SwarmConfiguration { - ServerInformation serverInformation; - do + Address = new Uri("http://localhost:5012"), + ControllerAddress = controllerAddress, + Identifier = "node1", + PrivateKey = PrivateKey + }, false, 5012); + using var node2 = new TestingServer(new SwarmConfiguration + { + Address = new Uri("http://localhost:5013"), + ControllerAddress = controllerAddress, + Identifier = "node2", + PrivateKey = PrivateKey + }, false, 5013); + using var serverCts = new CancellationTokenSource(); + var cancellationToken = serverCts.Token; + var serverTask = Task.WhenAll( + node1.Run(cancellationToken), + node2.Run(cancellationToken), + controller.Run(cancellationToken)); + + try + { + using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); + using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); + using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + + var controllerInfo = await controllerClient.ServerInformation(cancellationToken); + + async Task WaitForSwarmServerUpdate() { - await Task.Delay(TimeSpan.FromSeconds(10)); - serverInformation = await node1Client.ServerInformation(cancellationToken); + ServerInformation serverInformation; + do + { + await Task.Delay(TimeSpan.FromSeconds(10)); + serverInformation = await node1Client.ServerInformation(cancellationToken); + } + while (serverInformation.SwarmServers.Count == 1); } - while (serverInformation.SwarmServers.Count == 1); - } - static void CheckInfo(ServerInformation serverInformation) - { - Assert.IsNotNull(serverInformation.SwarmServers); - Assert.AreEqual(3, serverInformation.SwarmServers.Count); - - var node1 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node1"); - Assert.IsNotNull(node1); - Assert.AreEqual(node1.Address, "http://localhost:5012"); - Assert.IsFalse(node1.Controller); - - var node2 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"); - Assert.IsNotNull(node2); - Assert.AreEqual(node2.Address, "http://localhost:5013"); - Assert.IsFalse(node2.Controller); - - var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"); - Assert.IsNotNull(controller); - Assert.AreEqual(controller.Address, "http://localhost:5011"); - Assert.IsTrue(controller.Controller); - } - - CheckInfo(controllerInfo); - - // wait a few minutes for the updated server list to dispatch - await Task.WhenAny( - WaitForSwarmServerUpdate(), - Task.Delay(TimeSpan.FromMinutes(4))); - - var node2Info = await node2Client.ServerInformation(cancellationToken); - var node1Info = await node1Client.ServerInformation(cancellationToken); - CheckInfo(node1Info); - CheckInfo(node2Info); - - // check user info is shared - var newUser = await node2Client.Users.Create(new UserUpdate - { - Name = "asdf", - Password = "asdfasdfasdfasdf", - Enabled = true, - PermissionSet = new PermissionSet + static void CheckInfo(ServerInformation serverInformation) { - AdministrationRights = AdministrationRights.ChangeVersion + Assert.IsNotNull(serverInformation.SwarmServers); + Assert.AreEqual(3, serverInformation.SwarmServers.Count); + + var node1 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node1"); + Assert.IsNotNull(node1); + Assert.AreEqual(node1.Address, "http://localhost:5012"); + Assert.IsFalse(node1.Controller); + + var node2 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"); + Assert.IsNotNull(node2); + Assert.AreEqual(node2.Address, "http://localhost:5013"); + Assert.IsFalse(node2.Controller); + + var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"); + Assert.IsNotNull(controller); + Assert.AreEqual(controller.Address, "http://localhost:5011"); + Assert.IsTrue(controller.Controller); } - }, cancellationToken); - var node1User = await node1Client.Users.GetId(newUser, cancellationToken); - Assert.AreEqual(newUser.Name, node1User.Name); - Assert.AreEqual(newUser.Enabled, node1User.Enabled); + CheckInfo(controllerInfo); - using var controllerUserClient = await clientFactory.CreateFromLogin( - controllerAddress, - newUser.Name, - "asdfasdfasdfasdf"); + // wait a few minutes for the updated server list to dispatch + await Task.WhenAny( + WaitForSwarmServerUpdate(), + Task.Delay(TimeSpan.FromMinutes(4))); - using var node1BadClient = clientFactory.CreateFromToken(node1.Url, controllerUserClient.Token); - await Assert.ThrowsExceptionAsync(() => node1BadClient.Administration.Read(cancellationToken)); + var node2Info = await node2Client.ServerInformation(cancellationToken); + var node1Info = await node1Client.ServerInformation(cancellationToken); + CheckInfo(node1Info); + CheckInfo(node2Info); - // check instance info is not shared - var controllerInstance = await controllerClient.Instances.CreateOrAttach( - new Api.Models.Instance + // check user info is shared + var newUser = await node2Client.Users.Create(new UserUpdate { - Name = "ControllerInstance", - Path = Path.Combine(controller.Directory, "ControllerInstance") - }, - cancellationToken); + Name = "asdf", + Password = "asdfasdfasdfasdf", + Enabled = true, + PermissionSet = new PermissionSet + { + AdministrationRights = AdministrationRights.ChangeVersion + } + }, cancellationToken); - var node2Instance = await node2Client.Instances.CreateOrAttach( - new Api.Models.Instance + var node1User = await node1Client.Users.GetId(newUser, cancellationToken); + Assert.AreEqual(newUser.Name, node1User.Name); + Assert.AreEqual(newUser.Enabled, node1User.Enabled); + + using var controllerUserClient = await clientFactory.CreateFromLogin( + controllerAddress, + newUser.Name, + "asdfasdfasdfasdf"); + + using var node1BadClient = clientFactory.CreateFromToken(node1.Url, controllerUserClient.Token); + await Assert.ThrowsExceptionAsync(() => node1BadClient.Administration.Read(cancellationToken)); + + // check instance info is not shared + var controllerInstance = await controllerClient.Instances.CreateOrAttach( + new Api.Models.Instance + { + Name = "ControllerInstance", + Path = Path.Combine(controller.Directory, "ControllerInstance") + }, + cancellationToken); + + var node2Instance = await node2Client.Instances.CreateOrAttach( + new Api.Models.Instance + { + Name = "Node2Instance", + Path = Path.Combine(node2.Directory, "Node2Instance") + }, + cancellationToken); + var node2InstanceList = await node2Client.Instances.List(null, cancellationToken); + Assert.AreEqual(1, node2InstanceList.Count); + Assert.AreEqual(node2Instance.Id, node2InstanceList.First().Id); + Assert.IsNotNull(await node2Client.Instances.GetId(node2Instance, cancellationToken)); + var controllerInstanceList = await controllerClient.Instances.List(null, cancellationToken); + Assert.AreEqual(1, controllerInstanceList.Count); + Assert.AreEqual(controllerInstance.Id, controllerInstanceList.First().Id); + Assert.IsNotNull(await controllerClient.Instances.GetId(controllerInstance, cancellationToken)); + + await Assert.ThrowsExceptionAsync(() => controllerClient.Instances.GetId(node2Instance, cancellationToken)); + await Assert.ThrowsExceptionAsync(() => node1Client.Instances.GetId(controllerInstance, cancellationToken)); + + // test update + var testUpdateVersion = new Version(4, 6, 2); + await node1Client.Administration.Update( + new Administration + { + NewVersion = testUpdateVersion + }, + cancellationToken); + await Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(2)), serverTask); + Assert.IsTrue(serverTask.IsCompleted); + + void CheckServerUpdated(TestingServer server) { - Name = "Node2Instance", - Path = Path.Combine(node2.Directory, "Node2Instance") - }, - cancellationToken); - var node2InstanceList = await node2Client.Instances.List(null, cancellationToken); - Assert.AreEqual(1, node2InstanceList.Count); - Assert.AreEqual(node2Instance.Id, node2InstanceList.First().Id); - Assert.IsNotNull(await node2Client.Instances.GetId(node2Instance, cancellationToken)); - var controllerInstanceList = await controllerClient.Instances.List(null, cancellationToken); - Assert.AreEqual(1, controllerInstanceList.Count); - Assert.AreEqual(controllerInstance.Id, controllerInstanceList.First().Id); - Assert.IsNotNull(await controllerClient.Instances.GetId(controllerInstance, cancellationToken)); + Assert.IsTrue(Directory.Exists(server.UpdatePath), "Update directory not present!"); - await Assert.ThrowsExceptionAsync(() => controllerClient.Instances.GetId(node2Instance, cancellationToken)); - await Assert.ThrowsExceptionAsync(() => node1Client.Instances.GetId(controllerInstance, cancellationToken)); + var updatedAssemblyPath = Path.Combine(server.UpdatePath, "Tgstation.Server.Host.dll"); + Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!"); - // test update - var testUpdateVersion = new Version(4, 6, 2); - await node1Client.Administration.Update( - new Administration - { - NewVersion = testUpdateVersion - }, - cancellationToken); - await Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(2)), serverTask); - Assert.IsTrue(serverTask.IsCompleted); + var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath); + Assert.AreEqual(testUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver()); + } - void CheckServerUpdated(TestingServer server) - { - Assert.IsTrue(Directory.Exists(server.UpdatePath), "Update directory not present!"); - - var updatedAssemblyPath = Path.Combine(server.UpdatePath, "Tgstation.Server.Host.dll"); - Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!"); - - var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath); - Assert.AreEqual(testUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver()); + CheckServerUpdated(controller); + CheckServerUpdated(node1); + CheckServerUpdated(node2); + } + finally + { + serverCts.Cancel(); + await serverTask; } - - CheckServerUpdated(controller); - CheckServerUpdated(node1); - CheckServerUpdated(node2); - } - finally - { - serverCts.Cancel(); - await serverTask; } - Directory.Delete(Path.GetDirectoryName(controller.Directory), true); + new TestingServer(null, false).Dispose(); } static void TerminateAllDDs() From 30d25c8c5ec0ce36a0e248bda42205968302e59e Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 14:25:15 -0500 Subject: [PATCH 118/154] Reduce linux integration tests to relieve DB pressure --- .github/workflows/ci-suite.yml | 50 +--------------------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 8e119c41bb..9cab7602b9 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -305,7 +305,7 @@ jobs: matrix: database-type: [ 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ] watchdog-type: [ 'Basic', 'System' ] - configuration: [ 'Debug', 'Release' ] + configuration: [ 'Release' ] runs-on: ubuntu-latest steps: - name: Disable ptrace_scope @@ -459,96 +459,48 @@ jobs: name: linux-unit-test-coverage-Release path: ./code_coverage/unit_tests/linux_unit_tests_release - - name: Retrieve Linux Integration Test Coverage (Debug, Basic, Sqlite) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-Basic-Sqlite - path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_sqlite - - name: Retrieve Linux Integration Test Coverage (Release, Basic, Sqlite) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-Basic-Sqlite path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_sqlite - - name: Retrieve Linux Integration Test Coverage (Debug, System, Sqlite) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-System-Sqlite - path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_sqlite - - name: Retrieve Linux Integration Test Coverage (Release, System, Sqlite) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-System-Sqlite path: ./code_coverage/integration_tests/linux_integration_tests_release_system_sqlite - - name: Retrieve Linux Integration Test Coverage (Debug, Basic, PostgresSql) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-Basic-PostgresSql - path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_postgressql - - name: Retrieve Linux Integration Test Coverage (Release, Basic, PostgresSql) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-Basic-PostgresSql path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_postgressql - - name: Retrieve Linux Integration Test Coverage (Debug, System, PostgresSql) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-System-PostgresSql - path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_postgressql - - name: Retrieve Linux Integration Test Coverage (Release, System, PostgresSql) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-System-PostgresSql path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mariadb - - name: Retrieve Linux Integration Test Coverage (Debug, Basic, MariaDB) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-Basic-MariaDB - path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mariadb - - name: Retrieve Linux Integration Test Coverage (Release, Basic, MariaDB) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-Basic-MariaDB path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mariadb - - name: Retrieve Linux Integration Test Coverage (Debug, System, MariaDB) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-System-MariaDB - path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mariadb - - name: Retrieve Linux Integration Test Coverage (Release, System, MariaDB) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-System-MariaDB path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql - - name: Retrieve Linux Integration Test Coverage (Debug, Basic, MySql) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-Basic-MySql - path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql - - name: Retrieve Linux Integration Test Coverage (Release, Basic, MySql) uses: actions/download-artifact@v2 with: name: linux-integration-test-coverage-Release-Basic-MySql path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql - - name: Retrieve Linux Integration Test Coverage (Debug, System, MySql) - uses: actions/download-artifact@v2 - with: - name: linux-integration-test-coverage-Debug-System-MySql - path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql - - name: Retrieve Linux Integration Test Coverage (Release, System, MySql) uses: actions/download-artifact@v2 with: From 007c40fe0ff165b3047dbb7fb79c11dba6b18e25 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 28 Dec 2020 18:23:28 -0500 Subject: [PATCH 119/154] Fix dotnet version used for [NugetDeploy] --- .github/workflows/ci-suite.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 9cab7602b9..1e33654d09 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -649,6 +649,11 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && contains(github.event.head_commit.message, '[NugetDeploy]') steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.TGS4_DOTNET_VERSION }} + - name: Checkout uses: actions/checkout@v1 From c48f277279e666b968f6de2fcb81e948fc1061ef Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 29 Dec 2020 20:11:07 -0500 Subject: [PATCH 120/154] Document 410 on POST /User --- src/Tgstation.Server.Host/Controllers/UserController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index f0f3c80888..6acadfae0a 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -182,10 +182,12 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation. /// updated successfully. /// Requested does not exist. + /// Requested does not exist. [HttpPost] [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)] [ProducesResponseType(typeof(Api.Models.User), 200)] [ProducesResponseType(typeof(ErrorMessage), 404)] + [ProducesResponseType(typeof(ErrorMessage), 410)] #pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1506 public async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken) From 989b39d7232e5756dfbc135ee902260c4007eb4a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 29 Dec 2020 20:14:18 -0500 Subject: [PATCH 121/154] Add user group limit to config --- .../Models/Internal/ServerInformation.cs | 7 ++++++- .../Configuration/GeneralConfiguration.cs | 6 ++++++ src/Tgstation.Server.Host/Controllers/HomeController.cs | 1 + src/Tgstation.Server.Host/appsettings.json | 1 + tests/Tgstation.Server.Tests/RootTest.cs | 1 + tests/Tgstation.Server.Tests/TestingServer.cs | 1 + 6 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Api/Models/Internal/ServerInformation.cs b/src/Tgstation.Server.Api/Models/Internal/ServerInformation.cs index 0c6768e53a..21f4ed26eb 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ServerInformation.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Tgstation.Server.Api.Models.Internal { @@ -22,6 +22,11 @@ namespace Tgstation.Server.Api.Models.Internal /// public uint UserLimit { get; set; } + /// + /// The maximum number of s allowed. + /// + public uint UserGroupLimit { get; set; } + /// /// Limits the locations instances may be created or attached from. /// diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index e58c8d036e..766e4d4cd4 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -43,6 +43,11 @@ namespace Tgstation.Server.Host.Configuration /// const uint DefaultUserLimit = 100; + /// + /// The default value for . + /// + const uint DefaultUserGroupLimit = 25; + /// /// The default value for /// @@ -102,6 +107,7 @@ namespace Tgstation.Server.Host.Configuration MinimumPasswordLength = DefaultMinimumPasswordLength; InstanceLimit = DefaultInstanceLimit; UserLimit = DefaultUserLimit; + UserGroupLimit = DefaultUserGroupLimit; } /// diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 23a539405e..e08e0cde15 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -191,6 +191,7 @@ namespace Tgstation.Server.Host.Controllers MinimumPasswordLength = generalConfiguration.MinimumPasswordLength, InstanceLimit = generalConfiguration.InstanceLimit, UserLimit = generalConfiguration.UserLimit, + UserGroupLimit = generalConfiguration.UserGroupLimit, ValidInstancePaths = generalConfiguration.ValidInstancePaths, WindowsHost = platformIdentifier.IsWindows, SwarmServers = swarmService.GetSwarmServers(), diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index b0df570838..efc04c7461 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -8,6 +8,7 @@ "ApiPort": 5000, "UseBasicWatchdog": false, "UserLimit": 100, + "UserGroupLimit": 25, "InstanceLimit": 10, "ValidInstancePaths": null, "HostApiDocumentation": false diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs index f3fd6feb68..705269c9cf 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RootTest.cs @@ -162,6 +162,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(10U, serverInfo.MinimumPasswordLength); Assert.AreEqual(11U, serverInfo.InstanceLimit); Assert.AreEqual(150U, serverInfo.UserLimit); + Assert.AreEqual(47U, serverInfo.UserGroupLimit); Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), serverInfo.WindowsHost); //check that modifying the token even slightly fucks up the auth diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index f9c888e341..12f04a0b1b 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -80,6 +80,7 @@ namespace Tgstation.Server.Tests String.Format(CultureInfo.InvariantCulture, "General:MinimumPasswordLength={0}", 10), String.Format(CultureInfo.InvariantCulture, "General:InstanceLimit={0}", 11), String.Format(CultureInfo.InvariantCulture, "General:UserLimit={0}", 150), + String.Format(CultureInfo.InvariantCulture, "General:UserGroupLimit={0}", 47), String.Format(CultureInfo.InvariantCulture, "General:HostApiDocumentation={0}", DumpOpenApiSpecpath), String.Format(CultureInfo.InvariantCulture, "FileLogging:Directory={0}", Path.Combine(Directory, "Logs")), String.Format(CultureInfo.InvariantCulture, "FileLogging:LogLevel={0}", "Trace"), From 38a8fde66cc5eb653d547889d1b77e1e3635ed54 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 29 Dec 2020 20:14:40 -0500 Subject: [PATCH 122/154] API/Client version bumps --- build/Version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/Version.props b/build/Version.props index 5bdc17ce8b..563a1e6bb5 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,8 +5,8 @@ 4.7.0 2.2.0 - 8.0.0 - 9.0.0 + 8.1.0 + 9.1.0 5.2.10 1.1.0 1.2.0 From 29485dec97756e7e838c2b31f5528afb4f87499b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 29 Dec 2020 20:39:23 -0500 Subject: [PATCH 123/154] Actually enforce user and group limits --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 12 ++++++++++++ .../Controllers/UserController.cs | 8 ++++++++ .../Controllers/UserGroupController.cs | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 2f6821868d..36f14a5ee0 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -608,5 +608,17 @@ namespace Tgstation.Server.Api.Models /// [Description("Cannot delete the user group as it is not empty!")] UserGroupNotEmpty, + + /// + /// Attempted to create an but the configured limit has been reached. + /// + [Description("The user cannot be created because the configured limit has been reached!")] + UserLimitReached, + + /// + /// Attempted to create an but the configured limit has been reached. + /// + [Description("The user group cannot be created because the configured limit has been reached!")] + UserGroupLimitReached, } } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 6acadfae0a..ffd098b961 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -139,6 +139,14 @@ namespace Tgstation.Server.Host.Controllers if (fail != null) return fail; + var totalUsers = await DatabaseContext + .Users + .AsQueryable() + .CountAsync(cancellationToken) + .ConfigureAwait(false); + if (totalUsers >= generalConfiguration.UserLimit) + return Conflict(new ErrorMessage(ErrorCode.UserLimitReached)); + var dbUser = await CreateNewUserFromModel(model, cancellationToken).ConfigureAwait(false); if (dbUser == null) return Gone(); diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index c1be2818ad..d666e0b748 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Linq; using System.Threading; @@ -8,6 +9,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Security; using Z.EntityFramework.Plus; @@ -20,15 +22,22 @@ namespace Tgstation.Server.Host.Controllers [Route(Routes.UserGroup)] public class UserGroupController : ApiController { + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Initializes a new instance of the . /// /// The for the /// The for the + /// The containing the value of . /// The for the . public UserGroupController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, + IOptions generalConfigurationOptions, ILogger logger) : base( databaseContext, @@ -36,6 +45,7 @@ namespace Tgstation.Server.Host.Controllers logger, true) { + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -56,6 +66,14 @@ namespace Tgstation.Server.Host.Controllers if (model.Name == null) return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); + var totalGroups = await DatabaseContext + .Groups + .AsQueryable() + .CountAsync(cancellationToken) + .ConfigureAwait(false); + if (totalGroups >= generalConfiguration.UserGroupLimit) + return Conflict(new ErrorMessage(ErrorCode.UserGroupLimitReached)); + var permissionSet = new Models.PermissionSet { AdministrationRights = model.PermissionSet?.AdministrationRights ?? AdministrationRights.None, From 256e8f242fb7ba6c28d8c340561eacc2a5c958b3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 29 Dec 2020 21:03:16 -0500 Subject: [PATCH 124/154] Fix the build --- src/Tgstation.Server.Host/Controllers/UserController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index ffd098b961..08f83e35cf 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -113,6 +113,7 @@ namespace Tgstation.Server.Host.Controllers [HttpPut] [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(typeof(Api.Models.User), 201)] +#pragma warning disable CA1502, CA1506 public async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) @@ -181,6 +182,7 @@ namespace Tgstation.Server.Host.Controllers return Created(dbUser.ToApi(true)); } +#pragma warning restore CA1502, CA1506 /// /// Update a . From 9cdaef04108480244839c90f2c16fefe11eb3dae Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 11:43:41 -0500 Subject: [PATCH 125/154] Remove all debug conditionals - Add a down migrations comment --- .../Database/DatabaseContext.cs | 15 +++++++++------ .../Tgstation.Server.Host.csproj | 5 ----- tests/Tgstation.Server.Tests/IntegrationTest.cs | 2 -- tests/Tgstation.Server.Tests/VersionsTest.cs | 2 -- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 7d2b7a2d51..9de0ffa3f2 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -369,27 +369,30 @@ namespace Tgstation.Server.Host.Database return wasEmpty; } -#if DEBUG + // HEY YOU + // IF YOU HAVE A TEST THAT'S CREATING ERRORS BECAUSE THESE VALUES AREN'T SET CORRECTLY THERE'S MORE TO FIXING IT THAN JUST UPDATING THEM + // IN THE FUNCTION BELOW YOU ALSO NEED TO CORRECTLY SET THE RIGHT MIGRATION TO DOWNLOAD TO FOR THE LAST TGS VERSION + // IF THIS BREAKS AGAIN I WILL PERSONALLY HAUNT YOUR ASS WHEN I DIE + /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - public static readonly Type MSLatestMigration = typeof(MSAddSwarmIdentifer); + internal static readonly Type MSLatestMigration = typeof(MSAddSwarmIdentifer); /// /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - public static readonly Type MYLatestMigration = typeof(MYAddSwarmIdentifer); + internal static readonly Type MYLatestMigration = typeof(MYAddSwarmIdentifer); /// /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - public static readonly Type PGLatestMigration = typeof(PGAddSwarmIdentifer); + internal static readonly Type PGLatestMigration = typeof(PGAddSwarmIdentifer); /// /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - public static readonly Type SLLatestMigration = typeof(SLAddSwarmIdentifer); -#endif + internal static readonly Type SLLatestMigration = typeof(SLAddSwarmIdentifer); /// #pragma warning disable CA1502 // Cyclomatic complexity diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index e29b19b44e..3951c10411 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -105,11 +105,6 @@ - - - - - diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index a4fa6f3b86..3d003e67ad 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -330,7 +330,6 @@ namespace Tgstation.Server.Tests } } -#if DEBUG [TestMethod] public async Task TestDownMigrations() { @@ -475,7 +474,6 @@ namespace Tgstation.Server.Tests await migrator.MigrateAsync(migrationName, default); await context.Database.EnsureDeletedAsync(); } -#endif [TestMethod] public async Task TestServer() diff --git a/tests/Tgstation.Server.Tests/VersionsTest.cs b/tests/Tgstation.Server.Tests/VersionsTest.cs index e96c445081..1a1cf908da 100644 --- a/tests/Tgstation.Server.Tests/VersionsTest.cs +++ b/tests/Tgstation.Server.Tests/VersionsTest.cs @@ -146,7 +146,6 @@ namespace Tgstation.Server.Tests Assert.IsNotNull(line); } -#if DEBUG [TestMethod] public void TestDowngradeMigrations() { @@ -201,6 +200,5 @@ namespace Tgstation.Server.Tests Assert.AreEqual(latestMigrationPG, DatabaseContext.PGLatestMigration); Assert.AreEqual(latestMigrationSL, DatabaseContext.SLLatestMigration); } -#endif } } From 9c1fe159fc5c5c5f8a421b70dbe5aca869298207 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 12:08:21 -0500 Subject: [PATCH 126/154] Use an unsigned long so we can outlast the heat death of the universe --- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 9843b8657a..7219487b5f 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -725,7 +725,7 @@ namespace Tgstation.Server.Host.Swarm } SwarmRegistrationResult registrationResult; - for (var I = 1; ; ++I) + for (var I = 1UL; ; ++I) { logger.LogInformation("Swarm re-registration attempt {0}..."); registrationResult = await RegisterWithController(cancellationToken).ConfigureAwait(false); From 57b28e77b544712b815f34f9918c98368514d062 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 12:14:30 -0500 Subject: [PATCH 127/154] Immediately trigger health checks when the swarm server list becomes dirty --- .../Swarm/SwarmService.cs | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 7219487b5f..dc80714175 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -134,6 +134,11 @@ namespace Tgstation.Server.Host.Swarm /// readonly bool swarmController; + /// + /// A that completes when is set. + /// + TaskCompletionSource serversUpdatedTcs; + /// /// The that is used to proceed with committing an update. /// @@ -690,12 +695,24 @@ namespace Tgstation.Server.Host.Swarm lock (swarmServers) if (swarmServers.Count != currentSwarmServers.Count) - serversDirty = true; + MarkServersDirty(); if (serversDirty) await SendUpdatedServerListToNodes(cancellationToken).ConfigureAwait(false); } + /// + /// Set and complete the current . + /// + void MarkServersDirty() + { + var currentTcs = serversUpdatedTcs; + serversDirty = true; + if (currentTcs.TrySetResult(null)) + logger.LogTrace("Server list is dirty!"); + serversUpdatedTcs = new TaskCompletionSource(); + } + /// /// Ping the swarm controller to see that it is still running. If need be, reregister. /// @@ -960,10 +977,15 @@ namespace Tgstation.Server.Host.Swarm : lastControllerHealthCheck.HasValue ? (lastControllerHealthCheck.Value.AddMinutes(NodeHealthCheckIntervalMinutes) - DateTimeOffset.UtcNow) : TimeSpan.FromMinutes(NodeHealthCheckIntervalMinutes); - await asyncDelayer.Delay( + var delayTask = asyncDelayer.Delay( delay, - cancellationToken) - .ConfigureAwait(false); + cancellationToken); + + var awakeningTask = Task.WhenAny( + delayTask, + serversUpdatedTcs.Task); + + await awakeningTask.ConfigureAwait(false); if (!swarmController) { @@ -1076,7 +1098,7 @@ namespace Tgstation.Server.Host.Swarm } logger.LogInformation("Registered node {0} with ID {1}", node.Identifier, registrationId); - serversDirty = true; + MarkServersDirty(); return true; } @@ -1172,7 +1194,7 @@ namespace Tgstation.Server.Host.Swarm registrationIds.Remove(nodeIdentifier); } - serversDirty = true; + MarkServersDirty(); } } } From 6c253b17e2e7684964d4ddd7fa59f76c9a928ee9 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 13:06:19 -0500 Subject: [PATCH 128/154] Fix NullReferenceException --- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index dc80714175..b69a1a1229 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -226,6 +226,7 @@ namespace Tgstation.Server.Host.Swarm if (SwarmMode) { serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); + serversUpdatedTcs = new TaskCompletionSource(); if (swarmController) registrationIds = new Dictionary(); From 39ad7ca17747da79222e94c1c202dee28f94717b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 14:11:30 -0500 Subject: [PATCH 129/154] Unfuck User/Usergroup shenannigans - Make Internal.User abstract - Move User Id and Name to new UserBase class - ShallowUsers are now only UserBase - Moved PermissionSet to Internal.UserGroup - Old Internal.UserGroup properties moved to new Internal.UserGroupBase - Fixed WriteUsers being able to read users with POST /User - Fixed full users returned from POST /User not including group permission sets --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 8 +++---- .../Models/Internal/User.cs | 15 +----------- .../Models/Internal/UserBase.cs | 23 +++++++++++++++++++ .../Models/Internal/UserGroup.cs | 10 +++----- .../Models/Internal/UserGroupBase.cs | 17 ++++++++++++++ src/Tgstation.Server.Api/Models/User.cs | 4 ++-- src/Tgstation.Server.Api/Models/UserGroup.cs | 7 +----- src/Tgstation.Server.Client/IUsersClient.cs | 2 +- src/Tgstation.Server.Client/UsersClient.cs | 2 +- .../Controllers/UserController.cs | 10 ++++---- .../Core/SwaggerConfiguration.cs | 2 +- .../Models/PermissionSet.cs | 2 +- src/Tgstation.Server.Host/Models/User.cs | 12 +++++----- src/Tgstation.Server.Host/Models/UserGroup.cs | 10 +++++--- .../Security/IAuthenticationContextFactory.cs | 4 ++-- .../Security/ITokenFactory.cs | 2 +- .../Security/IdentityCache.cs | 4 ++-- tests/Tgstation.Server.Tests/UsersTest.cs | 3 +-- 18 files changed, 80 insertions(+), 57 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/Internal/UserBase.cs create mode 100644 src/Tgstation.Server.Api/Models/Internal/UserGroupBase.cs diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 36f14a5ee0..60e03b909d 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -76,7 +76,7 @@ namespace Tgstation.Server.Api.Models ServerUpdateInProgress, /// - /// Attempted to change something other than the capitalization of a . + /// Attempted to change something other than the capitalization of a . /// [Description("Can only change the capitalization of a user's name!")] UserNameChange, @@ -88,7 +88,7 @@ namespace Tgstation.Server.Api.Models UserSidChange, /// - /// Attempted to create a with a and . + /// Attempted to create a with a and . /// [Description("A user cannot have both a name and systemIdentifier!")] UserMismatchNameSid, @@ -106,13 +106,13 @@ namespace Tgstation.Server.Api.Models UserPasswordLength, /// - /// Attempted to create a with a ':' in the . + /// Attempted to create a with a ':' in the . /// [Description("User names cannot contain the ':' character!")] UserColonInName, /// - /// Attempted to create a with a or whitespace . + /// Attempted to create a with a or whitespace . /// [Description("User's name is missing or invalid whitespace!")] UserMissingName, diff --git a/src/Tgstation.Server.Api/Models/Internal/User.cs b/src/Tgstation.Server.Api/Models/Internal/User.cs index c15015b4ca..ac37d2e28f 100644 --- a/src/Tgstation.Server.Api/Models/Internal/User.cs +++ b/src/Tgstation.Server.Api/Models/Internal/User.cs @@ -6,14 +6,8 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Represents a server /// - public class User + public abstract class User : UserBase { - /// - /// The ID of the - /// - [Required] - public long? Id { get; set; } - /// /// If the is enabled since users cannot be deleted. System users cannot be disabled /// @@ -31,12 +25,5 @@ namespace Tgstation.Server.Api.Models.Internal /// [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? SystemIdentifier { get; set; } - - /// - /// The name of the - /// - [Required] - [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string? Name { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/UserBase.cs b/src/Tgstation.Server.Api/Models/Internal/UserBase.cs new file mode 100644 index 0000000000..a2301ddd08 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/UserBase.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Base class for . + /// + public class UserBase + { + /// + /// The ID of the + /// + [Required] + public long? Id { get; set; } + + /// + /// The name of the + /// + [Required] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] + public string? Name { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs b/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs index 8c981782dc..25c9520ba1 100644 --- a/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs +++ b/src/Tgstation.Server.Api/Models/Internal/UserGroup.cs @@ -1,17 +1,13 @@ -using System.ComponentModel.DataAnnotations; - namespace Tgstation.Server.Api.Models.Internal { /// /// Represents a group of s. /// - public class UserGroup : EntityId + public class UserGroup : UserGroupBase { /// - /// The name of the . + /// The of the . /// - [Required] - [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string? Name { get; set; } + public PermissionSet? PermissionSet { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/UserGroupBase.cs b/src/Tgstation.Server.Api/Models/Internal/UserGroupBase.cs new file mode 100644 index 0000000000..0f144dc95d --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/UserGroupBase.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Base class for . + /// + public abstract class UserGroupBase : EntityId + { + /// + /// The name of the . + /// + [Required] + [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] + public string? Name { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs index 089d502846..729e81383e 100644 --- a/src/Tgstation.Server.Api/Models/User.cs +++ b/src/Tgstation.Server.Api/Models/User.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Api.Models /// The who created this /// [Required] - public Internal.User? CreatedBy { get; set; } + public Internal.UserBase? CreatedBy { get; set; } /// /// List of s associated with the . @@ -33,7 +33,7 @@ namespace Tgstation.Server.Api.Models public PermissionSet? PermissionSet { get; set; } /// - /// The asociated with the , if any. + /// The asociated with the , if any. /// public Internal.UserGroup? Group { get; set; } } diff --git a/src/Tgstation.Server.Api/Models/UserGroup.cs b/src/Tgstation.Server.Api/Models/UserGroup.cs index 81cfca98ae..cc9394395a 100644 --- a/src/Tgstation.Server.Api/Models/UserGroup.cs +++ b/src/Tgstation.Server.Api/Models/UserGroup.cs @@ -5,14 +5,9 @@ namespace Tgstation.Server.Api.Models /// public sealed class UserGroup : Internal.UserGroup { - /// - /// The of the . - /// - public PermissionSet? PermissionSet { get; set; } - /// /// The s the has. /// - public ICollection? Users { get; set; } + public ICollection? Users { get; set; } } } diff --git a/src/Tgstation.Server.Client/IUsersClient.cs b/src/Tgstation.Server.Client/IUsersClient.cs index f024eb38ef..0328188210 100644 --- a/src/Tgstation.Server.Client/IUsersClient.cs +++ b/src/Tgstation.Server.Client/IUsersClient.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Client /// The to get. /// The for the operation /// A resulting in the requested - Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken); + Task GetId(Api.Models.Internal.UserBase user, CancellationToken cancellationToken); /// /// List all s diff --git a/src/Tgstation.Server.Client/UsersClient.cs b/src/Tgstation.Server.Client/UsersClient.cs index 8d53a487dc..a8b9168cf9 100644 --- a/src/Tgstation.Server.Client/UsersClient.cs +++ b/src/Tgstation.Server.Client/UsersClient.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Client public Task Create(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Create(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken); /// - public Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken); + public Task GetId(Api.Models.Internal.UserBase user, CancellationToken cancellationToken) => ApiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken); /// public Task> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 08f83e35cf..662c12a163 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Check if a given has a valid specified. + /// Check if a given has a valid specified. /// /// The to check. /// If this is a new . @@ -191,7 +191,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the of the operation. /// updated successfully. - /// Requested does not exist. + /// Requested does not exist. /// Requested does not exist. [HttpPost] [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)] @@ -292,6 +292,7 @@ namespace Tgstation.Server.Host.Controllers .Groups .AsQueryable() .Where(x => x.Id == model.Group.Id) + .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -333,7 +334,7 @@ namespace Tgstation.Server.Host.Controllers // return id only if not a self update and cannot read users return Json( - model.Id == originalUser.Id + AuthenticationContext.User.Id == originalUser.Id || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers) ? originalUser.ToApi(true) : new Api.Models.User @@ -384,7 +385,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Get a specific . /// - /// The to retrieve. + /// The to retrieve. /// The for the operation. /// A resulting in the of the operation. /// The was retrieved successfully. @@ -432,6 +433,7 @@ namespace Tgstation.Server.Host.Controllers .Groups .AsQueryable() .Where(x => x.Id == model.Group.Id) + .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); else diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index f0f8c8db8c..eec48ee126 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -161,7 +161,7 @@ namespace Tgstation.Server.Host.Core swaggerGenOptions.CustomSchemaIds(type => { - if (type == typeof(Api.Models.Internal.User)) + if (type == typeof(Api.Models.Internal.UserBase)) return "ShallowUser"; if (type == typeof(Api.Models.Internal.UserGroup)) return "ShallowUserGroup"; diff --git a/src/Tgstation.Server.Host/Models/PermissionSet.cs b/src/Tgstation.Server.Host/Models/PermissionSet.cs index ccc3e51b7f..ad5c18ffc6 100644 --- a/src/Tgstation.Server.Host/Models/PermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/PermissionSet.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Models public sealed class PermissionSet : Api.Models.PermissionSet { /// - /// The of . + /// The of . /// public long? UserId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 5090e6b99e..20b3dd6d2a 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -40,7 +40,7 @@ namespace Tgstation.Server.Host.Models public PermissionSet PermissionSet { get; set; } /// - /// The uppercase invariant of + /// The uppercase invariant of /// [Required] [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] @@ -67,9 +67,9 @@ namespace Tgstation.Server.Host.Models public ICollection OAuthConnections { get; set; } /// - /// Change a into a . + /// Change a into a . /// - /// The . + /// The . /// The . public static string CanonicalizeName(string name) => name?.ToUpperInvariant() ?? throw new ArgumentNullException(nameof(name)); @@ -81,9 +81,9 @@ namespace Tgstation.Server.Host.Models /// A new Api.Models.User ToApi(bool recursive, bool showDetails) => new Api.Models.User { - CreatedAt = CreatedAt, - CreatedBy = recursive ? CreatedBy?.ToApi(false, false) : null, - Enabled = Enabled, + CreatedAt = showDetails ? CreatedAt : null, + CreatedBy = showDetails && recursive ? CreatedBy?.ToApi(false, false) : null, + Enabled = showDetails ? Enabled : null, Id = Id, Name = Name, SystemIdentifier = showDetails ? SystemIdentifier : null, diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index 52b8e8a4ff..2253b8de53 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Tgstation.Server.Host.Models { /// - public sealed class UserGroup : Api.Models.Internal.UserGroup, IApiTransformable + public sealed class UserGroup : Api.Models.Internal.UserGroupBase, IApiTransformable { /// /// The the has. @@ -27,9 +27,13 @@ namespace Tgstation.Server.Host.Models { Id = Id, Name = Name, - PermissionSet = PermissionSet?.ToApi(), + PermissionSet = PermissionSet.ToApi(), Users = showUsers - ? Users?.Select(x => x.ToApi(false)).OfType().ToList() ?? new List() + ? Users + ?.Select(x => x.ToApi(false)) + .OfType() + .ToList() + ?? new List() : null, }; diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs index da9ea886de..196ae9fecf 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.Security /// /// Create an to populate /// - /// The of the + /// The of the /// The of the operation /// The the resulting 's password must be valid after /// The for the operation diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs index 90177bb524..f955f6fcd8 100644 --- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Security /// /// Create a for a given /// - /// The to create the token for. Must have the field available + /// The to create the token for. Must have the field available /// Whether or not this is an OAuth login. /// The for the operation /// A resulting in a new diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs index ef7cd76d93..6d20f6fb6d 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Security readonly ILogger logger; /// - /// The map of s to s + /// The map of s to s /// readonly Dictionary cachedIdentities; diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index df9ee17c58..8a3f86ae48 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -46,7 +46,6 @@ namespace Tgstation.Server.Tests var systemUser = user.CreatedBy; Assert.IsNotNull(systemUser); Assert.AreEqual("TGS", systemUser.Name); - Assert.AreEqual(false, systemUser.Enabled); var users = await serverClient.Users.List(null, cancellationToken); Assert.IsTrue(users.Count > 0); @@ -165,7 +164,7 @@ namespace Tgstation.Server.Tests { Id = testUser2.Id, PermissionSet = testUser2.PermissionSet, - Group = new Api.Models.Internal.UserGroup + Group = new UserGroup { Id = group.Id }, From 6c8dc87c4af36620885b262175d7974aca2b3c2b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 14:20:14 -0500 Subject: [PATCH 130/154] Fix pagination returning untransformed results --- src/Tgstation.Server.Host/Controllers/ApiController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 2d7fd10387..b48ac68881 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -409,9 +409,9 @@ namespace Tgstation.Server.Host.Controllers .ToList(); return Json( - new Paginated + new Paginated { - Content = pagedResults, + Content = finalResults, PageSize = pageSize, TotalPages = (ushort)((totalResults % pageSize) + 1) }); From 1ec16a22bdb56e0f151c7c8463283e933df719b3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 15:11:14 -0500 Subject: [PATCH 131/154] Minor swarm fixes - Fix double checking serversUpdatedTcs - Fix unregistering causing 500 errors from remote abort failures. --- .../Swarm/SwarmService.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index b69a1a1229..306f558a66 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -282,8 +282,20 @@ namespace Tgstation.Server.Host.Swarm SwarmConstants.UpdateRoute, null); - using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + try + { + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Unable to set remote abort to {0}!", + swarmController + ? $"node {swarmServer.Identifier}" + : "controller"); + } } Task task; @@ -709,9 +721,9 @@ namespace Tgstation.Server.Host.Swarm { var currentTcs = serversUpdatedTcs; serversDirty = true; + serversUpdatedTcs = new TaskCompletionSource(); if (currentTcs.TrySetResult(null)) logger.LogTrace("Server list is dirty!"); - serversUpdatedTcs = new TaskCompletionSource(); } /// From 063ed7f8d9bffce1e284eca60a30b8bf4c978539 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 19:23:18 -0500 Subject: [PATCH 132/154] Fix user controller includes --- src/Tgstation.Server.Host/Controllers/UserController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 662c12a163..f4c46de26a 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -225,6 +225,7 @@ namespace Tgstation.Server.Host.Controllers .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) .Include(x => x.Group) + .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -375,8 +376,10 @@ namespace Tgstation.Server.Host.Controllers .AsQueryable() .Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) .Include(x => x.CreatedBy) + .Include(x => x.PermissionSet) .Include(x => x.OAuthConnections) - .Include(x => x.Group))), + .Include(x => x.Group) + .ThenInclude(x => x.PermissionSet))), null, page, pageSize, @@ -408,6 +411,8 @@ namespace Tgstation.Server.Host.Controllers .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) .Include(x => x.Group) + .ThenInclude(x => x.PermissionSet) + .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) return NotFound(); From bdce05159a0a4970b7da4bc86c15af43c9dea2a2 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 22:25:09 -0500 Subject: [PATCH 133/154] Fix /UserGroup documentation and perms --- .../Controllers/UserGroupController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index d666e0b748..72475374bc 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -148,7 +148,7 @@ namespace Tgstation.Server.Host.Controllers /// Retrieve successfully. /// The requested does not currently exist. [HttpGet("{id}")] - [TgsAuthorize(InstancePermissionSetRights.Read)] + [TgsAuthorize(AdministrationRights.ReadUsers)] [ProducesResponseType(typeof(UserGroup), 200)] [ProducesResponseType(typeof(ErrorMessage), 410)] public async Task GetId(long id, CancellationToken cancellationToken) @@ -168,7 +168,7 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Lists s for the instance. + /// Lists all s. /// /// The current page. /// The page size. @@ -176,7 +176,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the request. /// Retrieved s successfully. [HttpGet(Routes.List)] - [TgsAuthorize(InstancePermissionSetRights.Read)] + [TgsAuthorize(AdministrationRights.ReadUsers)] [ProducesResponseType(typeof(Paginated), 200)] public Task List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( @@ -202,7 +202,7 @@ namespace Tgstation.Server.Host.Controllers /// The is not empty. /// The didn't exist. [HttpDelete("{id}")] - [TgsAuthorize(InstancePermissionSetRights.Write)] + [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(204)] [ProducesResponseType(typeof(ErrorMessage), 409)] [ProducesResponseType(typeof(ErrorMessage), 410)] From f01ace47e4784c2c9667d4eb8332071d0d265fa1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 22:41:29 -0500 Subject: [PATCH 134/154] Controller shutdown now immediately tells nodes - This allows them to start polling for a re-register immediately - Also fixed trying to health check with no active controllerRegistration - Bunch of other SwarmService fixes --- .../Swarm/SwarmService.cs | 232 +++++++++++------- 1 file changed, 149 insertions(+), 83 deletions(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 306f558a66..643e85af87 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -135,9 +135,9 @@ namespace Tgstation.Server.Host.Swarm readonly bool swarmController; /// - /// A that completes when is set. + /// A that is used to force a health check. /// - TaskCompletionSource serversUpdatedTcs; + TaskCompletionSource forceHealthCheckTcs; /// /// The that is used to proceed with committing an update. @@ -226,7 +226,7 @@ namespace Tgstation.Server.Host.Swarm if (SwarmMode) { serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); - serversUpdatedTcs = new TaskCompletionSource(); + forceHealthCheckTcs = new TaskCompletionSource(); if (swarmController) registrationIds = new Dictionary(); @@ -310,8 +310,7 @@ namespace Tgstation.Server.Host.Swarm task = Task.WhenAll( swarmServers .Where(x => !x.Controller) - .Select( - x => SendRemoteAbort(x))); + .Select(SendRemoteAbort)); } await task.ConfigureAwait(false); @@ -416,8 +415,7 @@ namespace Tgstation.Server.Host.Swarm task = Task.WhenAll( swarmServers .Where(x => !x.Controller) - .Select( - x => SendRemoteCommitUpdate(x))); + .Select(SendRemoteCommitUpdate)); await task.ConfigureAwait(false); return true; @@ -562,7 +560,7 @@ namespace Tgstation.Server.Host.Swarm .Select(x => x.Identifier)); tasks = swarmServers .Where(x => !x.Controller) - .Select(x => RemotePrepareUpdate(x)) + .Select(RemotePrepareUpdate) .ToList(); } @@ -600,61 +598,102 @@ namespace Tgstation.Server.Host.Swarm var _ = lazyRestartRegistration.Value; + SwarmRegistrationResult result; if (swarmController) { await databaseContextFactory.UseContext( databaseContext => databaseSeeder.Initialize(databaseContext, cancellationToken)) .ConfigureAwait(false); - if (SwarmMode) - serverHealthCheckTask = HealthCheckLoop(serverHealthCheckCancellationTokenSource.Token); - return SwarmRegistrationResult.Success; + result = SwarmRegistrationResult.Success; } + else + result = await RegisterWithController(cancellationToken).ConfigureAwait(false); - return await RegisterWithController(cancellationToken).ConfigureAwait(false); + if (SwarmMode && result == SwarmRegistrationResult.Success) + serverHealthCheckTask = HealthCheckLoop(serverHealthCheckCancellationTokenSource.Token); + + return result; } /// public async Task Shutdown(CancellationToken cancellationToken) { + async Task SendUnregistrationRequest(SwarmServer swarmServer) + { + using var httpClient = httpClientFactory.CreateClient(); + using var request = PrepareSwarmRequest( + swarmServer, + HttpMethod.Delete, + SwarmConstants.RegisterRoute, + null); + + try + { + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Error unregistering {0}!", + swarmController + ? $"node {swarmServer.Identifier}" + : "from controller"); + } + } + + if (serverHealthCheckTask != null) + { + serverHealthCheckCancellationTokenSource.Cancel(); + await serverHealthCheckTask.ConfigureAwait(false); + } + + if (!swarmController) + { + // if we restart a node, we don't want to unregister it so the controller doesn't try to update without it + // if we're shutting it down, though we should unregister it + if (!restarting) + { + logger.LogInformation("Unregistering from swarm controller..."); + await SendUnregistrationRequest(null); + } + else + logger.LogTrace("Not unregistering from swarm controller as we are restarting"); + + return; + } + // downgrade the db if necessary - if (swarmController) + if (targetUpdateVersion != null + && targetUpdateVersion < assemblyInformationProvider.Version) + await databaseContextFactory.UseContext( + db => databaseSeeder.Downgrade(db, targetUpdateVersion, cancellationToken)) + .ConfigureAwait(false); + + if (SwarmMode) { - serverHealthCheckCancellationTokenSource?.Cancel(); - if (serverHealthCheckTask != null) - await serverHealthCheckTask.ConfigureAwait(false); + // Put the nodes into a reconnecting state + if (targetUpdateVersion == null) + { + logger.LogInformation("Unregistering nodes..."); + Task task; + lock (swarmServers) + { + task = Task.WhenAll( + swarmServers + .Where(x => !x.Controller) + .Select(SendUnregistrationRequest)); + swarmServers.RemoveRange(1, swarmServers.Count - 1); + registrationIds.Clear(); + } - if (targetUpdateVersion != null - && targetUpdateVersion < assemblyInformationProvider.Version) - await databaseContextFactory.UseContext( - db => databaseSeeder.Downgrade(db, targetUpdateVersion, cancellationToken)) - .ConfigureAwait(false); + await task.ConfigureAwait(false); + } - // we don't tell nodes about us unregistering, they'll try to reconnect eventually. - if (SwarmMode) - logger.LogTrace("Swarm controller shutdown"); - - return; + logger.LogTrace("Swarm controller shutdown"); } - - // if we restart a node, we don't want to unregister it so the controller doesn't try to update without it - // if we're shutting it down, though we should unregister it - if (restarting) - { - logger.LogTrace("Not unregistering from swarm controller as we are restarting"); - return; - } - - logger.LogInformation("Unregistering from swarm controller..."); - using var httpClient = httpClientFactory.CreateClient(); - using var request = PrepareSwarmRequest( - null, - HttpMethod.Delete, - SwarmConstants.RegisterRoute, - null); - - using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); } /// @@ -684,7 +723,7 @@ namespace Tgstation.Server.Host.Swarm response.EnsureSuccessStatusCode(); return; } - catch (Exception ex) + catch (Exception ex) when (!(ex is OperationCanceledException)) { logger.LogWarning( ex, @@ -702,8 +741,7 @@ namespace Tgstation.Server.Host.Swarm await Task.WhenAll( currentSwarmServers .Where(x => !x.Controller) - .Select( - x => HealthRequestForServer(x))) + .Select(HealthRequestForServer)) .ConfigureAwait(false); lock (swarmServers) @@ -715,17 +753,26 @@ namespace Tgstation.Server.Host.Swarm } /// - /// Set and complete the current . + /// Set and complete the current . /// void MarkServersDirty() { - var currentTcs = serversUpdatedTcs; serversDirty = true; - serversUpdatedTcs = new TaskCompletionSource(); - if (currentTcs.TrySetResult(null)) + if(TriggerHealthCheck()) logger.LogTrace("Server list is dirty!"); } + /// + /// Complete the current . + /// + /// the result of the call to . + bool TriggerHealthCheck() + { + var currentTcs = forceHealthCheckTcs; + forceHealthCheckTcs = new TaskCompletionSource(); + return currentTcs.TrySetResult(null); + } + /// /// Ping the swarm controller to see that it is still running. If need be, reregister. /// @@ -733,31 +780,31 @@ namespace Tgstation.Server.Host.Swarm /// A representing the running operation. async Task HealthCheckController(CancellationToken cancellationToken) { - using var request = PrepareSwarmRequest( - null, - HttpMethod.Get, - String.Empty, - null); using var httpClient = httpClientFactory.CreateClient(); - try - { - using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - logger.LogTrace("Health check successful"); - return; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Error during swarm controller health check! Attempting to re-register..."); - controllerRegistration = null; - lastControllerHealthCheck = null; - } + if (controllerRegistration.HasValue) + try + { + using var request = PrepareSwarmRequest( + null, + HttpMethod.Get, + String.Empty, + null); + using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + logger.LogTrace("Controller health check successful"); + return; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error during swarm controller health check! Attempting to re-register..."); + controllerRegistration = null; + } SwarmRegistrationResult registrationResult; for (var I = 1UL; ; ++I) { - logger.LogInformation("Swarm re-registration attempt {0}..."); + logger.LogInformation("Swarm re-registration attempt {0}...", I); registrationResult = await RegisterWithController(cancellationToken).ConfigureAwait(false); if (registrationResult == SwarmRegistrationResult.Success) @@ -771,7 +818,7 @@ namespace Tgstation.Server.Host.Swarm if (registrationResult == SwarmRegistrationResult.VersionMismatch) { - logger.LogError("Swarm Re-registration failed, controller's TGS version has changed!"); + logger.LogError("Swarm re-registration failed, controller's TGS version has changed!"); break; } } @@ -871,7 +918,7 @@ namespace Tgstation.Server.Host.Swarm using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } - catch (Exception ex) + catch (Exception ex) when (!(ex is OperationCanceledException)) { logger.LogWarning(ex, "Error during swarm server list update for node '{0}'! Unregistering...", swarmServer.Identifier); @@ -886,7 +933,7 @@ namespace Tgstation.Server.Host.Swarm await Task.WhenAll( currentSwarmServers .Where(x => !x.Controller) - .Select(x => UpdateRequestForServer(x))) + .Select(UpdateRequestForServer)) .ConfigureAwait(false); serversDirty = false; } @@ -983,28 +1030,39 @@ namespace Tgstation.Server.Host.Swarm logger.LogTrace("Starting HealthCheckLoop..."); try { + var nextForceHealthCheckTask = forceHealthCheckTcs.Task; while (!cancellationToken.IsCancellationRequested) { - var delay = swarmController - ? TimeSpan.FromMinutes(ControllerHealthCheckIntervalMinutes) - : lastControllerHealthCheck.HasValue - ? (lastControllerHealthCheck.Value.AddMinutes(NodeHealthCheckIntervalMinutes) - DateTimeOffset.UtcNow) - : TimeSpan.FromMinutes(NodeHealthCheckIntervalMinutes); + TimeSpan delay; + if (swarmController) + delay = TimeSpan.FromMinutes(ControllerHealthCheckIntervalMinutes); + else + { + delay = TimeSpan.FromMinutes(NodeHealthCheckIntervalMinutes); + if (lastControllerHealthCheck.HasValue) + { + var recommendedTimeOfNextCheck = lastControllerHealthCheck.Value + delay; + + if (recommendedTimeOfNextCheck > DateTimeOffset.UtcNow) + delay = recommendedTimeOfNextCheck - DateTimeOffset.UtcNow; + } + } + var delayTask = asyncDelayer.Delay( delay, cancellationToken); var awakeningTask = Task.WhenAny( delayTask, - serversUpdatedTcs.Task); + nextForceHealthCheckTask); await awakeningTask.ConfigureAwait(false); - if (!swarmController) + if (!swarmController && !nextForceHealthCheckTask.IsCompleted) { if (!lastControllerHealthCheck.HasValue) { - logger.LogTrace("Not registered with controller, skipping health check."); + logger.LogTrace("Not initially registered with controller, skipping health check."); continue; // unregistered } @@ -1015,6 +1073,8 @@ namespace Tgstation.Server.Host.Swarm } } + nextForceHealthCheckTask = forceHealthCheckTcs.Task; + logger.LogDebug("Performing swarm health check..."); try { @@ -1192,7 +1252,13 @@ namespace Tgstation.Server.Host.Swarm public async Task UnregisterNode(Guid registrationId, CancellationToken cancellationToken) { if (!swarmController) - throw new InvalidOperationException("Cannot UnregisterNode on swarm node!"); + { + // immediately trigger a health check + logger.LogInformation("Controller unregistering, will attempt re-registration..."); + controllerRegistration = null; + TriggerHealthCheck(); + return; + } logger.LogTrace("UnregisterNode {0}", registrationId); var nodeIdentifier = NodeIdentifierFromRegistration(registrationId); From 2e8f1e701a3ff1d52d54331372742f1c268e4369 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 22:41:41 -0500 Subject: [PATCH 135/154] Adds more integration tests --- .../Tgstation.Server.Tests/IntegrationTest.cs | 198 +++++++++++++++++- .../{RootTest.cs => RawRequestTests.cs} | 41 +++- 2 files changed, 234 insertions(+), 5 deletions(-) rename tests/Tgstation.Server.Tests/{RootTest.cs => RawRequestTests.cs} (87%) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 3d003e67ad..83b09adc0f 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -74,6 +74,8 @@ namespace Tgstation.Server.Tests { NewVersion = testUpdateVersion }, cancellationToken).ConfigureAwait(false); + var serverInfo = await adminClient.ServerInformation(cancellationToken); + Assert.IsTrue(serverInfo.UpdateInProgress); } //wait up to 3 minutes for the dl and install @@ -114,7 +116,7 @@ namespace Tgstation.Server.Tests } [TestMethod] - public async Task TestSwarm() + public async Task TestSwarmSynchronizationAndUpdates() { // cleanup existing directories new TestingServer(null, false).Dispose(); @@ -195,7 +197,7 @@ namespace Tgstation.Server.Tests // wait a few minutes for the updated server list to dispatch await Task.WhenAny( WaitForSwarmServerUpdate(), - Task.Delay(TimeSpan.FromMinutes(4))); + Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); var node2Info = await node2Client.ServerInformation(cancellationToken); var node1Info = await node1Client.ServerInformation(cancellationToken); @@ -290,6 +292,196 @@ namespace Tgstation.Server.Tests new TestingServer(null, false).Dispose(); } + [TestMethod] + public async Task TestSwarmReconnection() + { + // cleanup existing directories + new TestingServer(null, false).Dispose(); + + const string PrivateKey = "adlfj73ywifhks7iwrgfegjs"; + + var controllerAddress = new Uri("http://localhost:5011"); + using (var controller = new TestingServer(new SwarmConfiguration + { + Address = controllerAddress, + Identifier = "controller", + PrivateKey = PrivateKey + }, false, 5011)) + { + using var node1 = new TestingServer(new SwarmConfiguration + { + Address = new Uri("http://localhost:5012"), + ControllerAddress = controllerAddress, + Identifier = "node1", + PrivateKey = PrivateKey + }, false, 5012); + using var node2 = new TestingServer(new SwarmConfiguration + { + Address = new Uri("http://localhost:5013"), + ControllerAddress = controllerAddress, + Identifier = "node2", + PrivateKey = PrivateKey + }, false, 5013); + using var serverCts = new CancellationTokenSource(); + + var cancellationToken = serverCts.Token; + using var node1Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + Task node1Task, node2Task, controllerTask; + var serverTask = Task.WhenAll( + node1Task = node1.Run(node1Cts.Token), + node2Task = node2.Run(cancellationToken), + controllerTask = controller.Run(cancellationToken)); + + try + { + using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); + using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); + using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + + var controllerInfo = await controllerClient.ServerInformation(cancellationToken); + + async Task WaitForSwarmServerUpdate(IServerClient client, int currentServerCount) + { + ServerInformation serverInformation; + do + { + await Task.Delay(TimeSpan.FromSeconds(10)); + serverInformation = await client.ServerInformation(cancellationToken); + } + while (serverInformation.SwarmServers.Count == currentServerCount); + } + + static void CheckInfo(ServerInformation serverInformation) + { + Assert.IsNotNull(serverInformation.SwarmServers); + Assert.AreEqual(3, serverInformation.SwarmServers.Count); + + var node1 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node1"); + Assert.IsNotNull(node1); + Assert.AreEqual(node1.Address, "http://localhost:5012"); + Assert.IsFalse(node1.Controller); + + var node2 = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "node2"); + Assert.IsNotNull(node2); + Assert.AreEqual(node2.Address, "http://localhost:5013"); + Assert.IsFalse(node2.Controller); + + var controller = serverInformation.SwarmServers.SingleOrDefault(x => x.Identifier == "controller"); + Assert.IsNotNull(controller); + Assert.AreEqual(controller.Address, "http://localhost:5011"); + Assert.IsTrue(controller.Controller); + } + + CheckInfo(controllerInfo); + + // wait a few minutes for the updated server list to dispatch + await Task.WhenAny( + WaitForSwarmServerUpdate(node1Client, 1), + Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); + + var node2Info = await node2Client.ServerInformation(cancellationToken); + var node1Info = await node1Client.ServerInformation(cancellationToken); + CheckInfo(node1Info); + CheckInfo(node2Info); + + // kill node1 + node1Cts.Cancel(); + await Task.WhenAny( + node1Task, + Task.Delay(TimeSpan.FromMinutes(1))); + Assert.IsTrue(node1Task.IsCompleted); + + // it should unregister + controllerInfo = await controllerClient.ServerInformation(cancellationToken); + Assert.AreEqual(2, controllerInfo.SwarmServers.Count); + Assert.IsFalse(controllerInfo.SwarmServers.Any(x => x.Identifier == "node1")); + + // wait a few minutes for the updated server list to dispatch + await Task.WhenAny( + WaitForSwarmServerUpdate(node2Client, 3), + Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); + + node2Info = await node2Client.ServerInformation(cancellationToken); + Assert.AreEqual(2, node2Info.SwarmServers.Count); + Assert.IsFalse(node2Info.SwarmServers.Any(x => x.Identifier == "node1")); + + // restart the controller + await controllerClient.Administration.Restart(cancellationToken); + await Task.WhenAny( + controllerTask, + Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); + Assert.IsTrue(controllerTask.IsCompleted); + + controllerTask = controller.Run(cancellationToken); + using var controllerClient2 = await CreateAdminClient(controller.Url, cancellationToken); + + // node 2 should reconnect once it's health check triggers + await Task.WhenAny( + WaitForSwarmServerUpdate(controllerClient2, 1), + Task.Delay(TimeSpan.FromMinutes(5), cancellationToken)); + + controllerInfo = await controllerClient2.ServerInformation(cancellationToken); + Assert.AreEqual(2, controllerInfo.SwarmServers.Count); + Assert.IsNotNull(controllerInfo.SwarmServers.SingleOrDefault(x => x.Identifier == "node2")); + + // wait a few seconds to dispatch the updated list to node2 + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + + // restart node2 + await node2Client.Administration.Restart(cancellationToken); + await Task.WhenAny( + node2Task, + Task.Delay(TimeSpan.FromMinutes(1))); + Assert.IsTrue(node1Task.IsCompleted); + + // should remain registered + controllerInfo = await controllerClient2.ServerInformation(cancellationToken); + Assert.AreEqual(2, controllerInfo.SwarmServers.Count); + Assert.IsNotNull(controllerInfo.SwarmServers.SingleOrDefault(x => x.Identifier == "node2")); + + // update should fail + await controllerClient2.Administration.Update(new Administration + { + NewVersion = new Version(4, 6, 2) + }, cancellationToken); + + async Task WaitForUpdateFailure() + { + ServerInformation serverInformation; + serverInformation = await controllerClient2.ServerInformation(cancellationToken); + while (serverInformation.UpdateInProgress) + { + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + serverInformation = await controllerClient2.ServerInformation(cancellationToken); + } + } + + var updateFailureTask = WaitForUpdateFailure(); + await Task.WhenAny(updateFailureTask, Task.Delay(TimeSpan.FromMinutes(5), cancellationToken)); + + node2Task = node2.Run(cancellationToken); + using var node2Client2 = await CreateAdminClient(node2.Url, cancellationToken); + + // should re-register + await Task.WhenAny( + WaitForSwarmServerUpdate(node2Client2, 1), + Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); + + node2Info = await node2Client2.ServerInformation(cancellationToken); + Assert.AreEqual(2, node2Info.SwarmServers.Count); + Assert.IsNotNull(node2Info.SwarmServers.SingleOrDefault(x => x.Identifier == "controller")); + } + finally + { + serverCts.Cancel(); + await serverTask; + } + } + + new TestingServer(null, false).Dispose(); + } + static void TerminateAllDDs() { foreach (var proc in System.Diagnostics.Process.GetProcessesByName("DreamDaemon")) @@ -531,7 +723,7 @@ namespace Tgstation.Server.Tests } } - var rootTest = FailFast(new RootTest().Run(clientFactory, adminClient, cancellationToken)); + var rootTest = FailFast(new RawRequestTests().Run(clientFactory, adminClient, cancellationToken)); var adminTest = FailFast(new AdministrationTest(adminClient.Administration).Run(cancellationToken)); var usersTest = FailFast(new UsersTest(adminClient).Run(cancellationToken)); instance = await new InstanceManagerTest(adminClient.Instances, adminClient.Users, server.Directory).RunPreInstanceTest(cancellationToken); diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RawRequestTests.cs similarity index 87% rename from tests/Tgstation.Server.Tests/RootTest.cs rename to tests/Tgstation.Server.Tests/RawRequestTests.cs index 705269c9cf..df9bad0018 100644 --- a/tests/Tgstation.Server.Tests/RootTest.cs +++ b/tests/Tgstation.Server.Tests/RawRequestTests.cs @@ -16,7 +16,7 @@ using Tgstation.Server.Host; namespace Tgstation.Server.Tests { - class RootTest + sealed class RawRequestTests { async Task TestRequestValidation(IServerClient serverClient, CancellationToken cancellationToken) { @@ -272,11 +272,48 @@ namespace Tgstation.Server.Tests } } + async Task RegressionTestForLeakedPasswordHashesBug(IServerClient serverClient, CancellationToken cancellationToken) + { + // See what https://github.com/tgstation/tgstation-server/commit/6c8dc87c4af36620885b262175d7974aca2b3c2b fixed + + var url = serverClient.Url; + var token = serverClient.Token.Bearer; + // check that 400s are returned appropriately + using var httpClient = new HttpClient(); + + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.User.Substring(1))) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.IsFalse(content.Contains("passwordHash")); + } + + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.User.Substring(1) + '/' + Routes.List + "?pageSize=100")) + { + request.Headers.Accept.Clear(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version); + request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); + using var response = await httpClient.SendAsync(request, cancellationToken); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.IsFalse(content.Contains("passwordHash")); + } + } + public Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken) => Task.WhenAll( TestRequestValidation(serverClient, cancellationToken), TestOAuthFails(serverClient, cancellationToken), TestServerInformation(clientFactory, serverClient, cancellationToken), - TestInvalidTransfers(serverClient, cancellationToken)); + TestInvalidTransfers(serverClient, cancellationToken), + RegressionTestForLeakedPasswordHashesBug(serverClient, cancellationToken)); } } From b0bdf62f0288bff83dd454d64f4ce9f33a90f5e1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 22:41:51 -0500 Subject: [PATCH 136/154] API version bump --- build/Version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/Version.props b/build/Version.props index 563a1e6bb5..222901410b 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,8 +5,8 @@ 4.7.0 2.2.0 - 8.1.0 - 9.1.0 + 8.1.1 + 9.1.1 5.2.10 1.1.0 1.2.0 From 81f42c0431cdf76509eca0afae94a138d3030dbf Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 30 Dec 2020 23:48:43 -0500 Subject: [PATCH 137/154] Fix documentation typo --- src/Tgstation.Server.Host/Controllers/UserGroupController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index 72475374bc..b23c8ceeb7 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -94,7 +94,7 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Update a new . + /// Update a . /// /// The to update. /// The for the operation. From 716ccab4621aec48dbbd72e38824c34ada6e3d4a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 31 Dec 2020 00:24:19 -0500 Subject: [PATCH 138/154] Fixed reading UserGroups with POST --- .../Controllers/UserGroupController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index b23c8ceeb7..5fde888df0 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -136,6 +136,12 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.ReadUsers)) + return Json(new UserGroup + { + Id = currentGroup.Id + }); + return Json(currentGroup.ToApi(true)); } From 09dc016118b4d09e67ae5db4686d85dc20916b97 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 31 Dec 2020 00:24:27 -0500 Subject: [PATCH 139/154] Test fix --- tests/Tgstation.Server.Tests/UsersTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index 8a3f86ae48..5861ea0f0a 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -164,7 +164,7 @@ namespace Tgstation.Server.Tests { Id = testUser2.Id, PermissionSet = testUser2.PermissionSet, - Group = new UserGroup + Group = new Api.Models.Internal.UserGroup { Id = group.Id }, From 58401ef319be9851d6f1cb227194a4f38f8184a1 Mon Sep 17 00:00:00 2001 From: alexkar598 <25136265+alexkar598@users.noreply.github.com> Date: Thu, 31 Dec 2020 01:09:09 -0500 Subject: [PATCH 140/154] Update ControlPanelVersion.props --- build/ControlPanelVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ControlPanelVersion.props b/build/ControlPanelVersion.props index d6e0a945fe..df855e77ac 100644 --- a/build/ControlPanelVersion.props +++ b/build/ControlPanelVersion.props @@ -1,6 +1,6 @@ - 1.0.1 + 2.0.0 From dbf8db60bffdf724a2786e0823211e0e8c142aae Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 31 Dec 2020 16:47:41 -0500 Subject: [PATCH 141/154] Add a 5 second delay between swarm registration attempts --- src/Tgstation.Server.Host/Components/InstanceManager.cs | 3 +++ src/Tgstation.Server.Host/Swarm/SwarmService.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 4e272e0012..f4526b2e09 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -565,6 +565,9 @@ namespace Tgstation.Server.Host.Components if (registrationResult == SwarmRegistrationResult.VersionMismatch) throw new InvalidOperationException("Swarm controller's TGS version does not match our own!"); + + if (registrationResult != SwarmRegistrationResult.Success) + await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken); } while (registrationResult != SwarmRegistrationResult.Success && !cancellationToken.IsCancellationRequested); } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 643e85af87..919cd51d28 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -821,6 +821,8 @@ namespace Tgstation.Server.Host.Swarm logger.LogError("Swarm re-registration failed, controller's TGS version has changed!"); break; } + + await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken); } // we could do something here... but what? From b50ff9d5698ed284c00ac7e997c20d69e65961cf Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 31 Dec 2020 22:52:13 -0500 Subject: [PATCH 142/154] Log the registering node's address --- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 919cd51d28..4ed9ae94fd 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -1172,7 +1172,7 @@ namespace Tgstation.Server.Host.Swarm } } - logger.LogInformation("Registered node {0} with ID {1}", node.Identifier, registrationId); + logger.LogInformation("Registered node {0} ({1}) with ID {2}", node.Identifier, node.Address, registrationId); MarkServersDirty(); return true; } From a4ade2b78a9b5cb76ae88919f57dc2a05b99a409 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 31 Dec 2020 23:22:49 -0500 Subject: [PATCH 143/154] Fix race condition between swarm server startup and health checking --- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 4ed9ae94fd..3cf7efc2b4 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -41,6 +41,11 @@ namespace Tgstation.Server.Host.Swarm /// const int UpdateCommitTimeoutMinutes = 10; + /// + /// Number of seconds between triggering and a health check being performed. + /// + const int SecondsToDelayForcedHealthChecks = 15; + /// /// See for the swarm system. /// @@ -1060,7 +1065,13 @@ namespace Tgstation.Server.Host.Swarm await awakeningTask.ConfigureAwait(false); - if (!swarmController && !nextForceHealthCheckTask.IsCompleted) + if (nextForceHealthCheckTask.IsCompleted && swarmController) + { + // Intentionally wait a few seconds for the other server to start up before interogating it + logger.LogTrace("Next health check triggering in {0}s...", SecondsToDelayForcedHealthChecks); + await asyncDelayer.Delay(TimeSpan.FromSeconds(SecondsToDelayForcedHealthChecks), cancellationToken); + } + else if (!swarmController && !nextForceHealthCheckTask.IsCompleted) { if (!lastControllerHealthCheck.HasValue) { From 940566d4b0140e0ce4d58c566a72a3100b47fc29 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 1 Jan 2021 11:26:06 -0500 Subject: [PATCH 144/154] Fix SQLite down migrations Fuck SQLite --- .../Migrations/20201222175532_SLAddSwarmIdentifer.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs b/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs index bd8751bf63..5d6c11f937 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20201222175532_SLAddSwarmIdentifer.cs @@ -62,6 +62,14 @@ namespace Tgstation.Server.Host.Database.Migrations migrationBuilder.DropTable( name: "Instances_down"); + + migrationBuilder.RenameTable( + name: "Instances", + newName: "Instances_down"); + + migrationBuilder.RenameTable( + name: "Instances_down", + newName: "Instances"); } } } From 2715e200295267e26dd1b258553ccdea945e1c0d Mon Sep 17 00:00:00 2001 From: alexkar598 <25136265+alexkar598@users.noreply.github.com> Date: Fri, 1 Jan 2021 12:53:23 -0500 Subject: [PATCH 145/154] Update ControlPanelVersion.props --- build/ControlPanelVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ControlPanelVersion.props b/build/ControlPanelVersion.props index df855e77ac..ba79100b84 100644 --- a/build/ControlPanelVersion.props +++ b/build/ControlPanelVersion.props @@ -1,6 +1,6 @@ - 2.0.0 + 2.0.1 From 6897fdb705d3e6b85341371652ac48eaa8b930be Mon Sep 17 00:00:00 2001 From: alexkar598 <25136265+alexkar598@users.noreply.github.com> Date: Fri, 1 Jan 2021 17:02:58 -0500 Subject: [PATCH 146/154] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36be4c01b0..642ac2513a 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi "Keycloak": { "ClientId": "...", "ClientSecret": "...", - "RedirectUrl": "..." + "RedirectUrl": "...", "ServerUrl": "..." } } From b8cce88cbfe23f293a907f71aaecf1c510ebc9cc Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 1 Jan 2021 21:35:35 -0500 Subject: [PATCH 147/154] Fix TotalPages calculation error --- src/Tgstation.Server.Host/Controllers/ApiController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index b48ac68881..0c37a75142 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -413,7 +413,7 @@ namespace Tgstation.Server.Host.Controllers { Content = finalResults, PageSize = pageSize, - TotalPages = (ushort)((totalResults % pageSize) + 1) + TotalPages = (ushort)((totalResults / pageSize) + 1) }); } } From 8cfd57370f6025a5dce8d7735b41c3de31a54336 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 1 Jan 2021 22:39:53 -0500 Subject: [PATCH 148/154] Fix being unable to properly create instance permission sets [APIDeploy] --- build/Version.props | 2 +- .../InstancePermissionSetController.cs | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/build/Version.props b/build/Version.props index 222901410b..d4a35b4ba5 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,7 +5,7 @@ 4.7.0 2.2.0 - 8.1.1 + 8.1.2 9.1.1 5.2.10 1.1.0 diff --git a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs index 5a154b339a..58b95e35a4 100644 --- a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs @@ -48,27 +48,44 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the of the request. /// created successfully. + /// The does not exist. [HttpPut] [TgsAuthorize(InstancePermissionSetRights.Create)] [ProducesResponseType(typeof(Api.Models.InstancePermissionSet), 201)] + [ProducesResponseType(typeof(ErrorMessage), 410)] +#pragma warning disable CA1506 public async Task Create([FromBody] Api.Models.InstancePermissionSet model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); - var userCanonicalName = await DatabaseContext - .Users + var existingPermissionSet = await DatabaseContext + .PermissionSets .AsQueryable() .Where(x => x.Id == model.PermissionSetId) - .Select(x => x.CanonicalName) + .Select(x => new Models.PermissionSet + { + UserId = x.UserId + }) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - if (userCanonicalName == default) - return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); + if (existingPermissionSet == default) + return Gone(); - if (userCanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) - return Forbid(); + if (existingPermissionSet.UserId.HasValue) + { + var userCanonicalName = await DatabaseContext + .Users + .AsQueryable() + .Where(x => x.Id == existingPermissionSet.UserId.Value) + .Select(x => x.CanonicalName) + .FirstAsync(cancellationToken) + .ConfigureAwait(false); + + if (userCanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + return Forbid(); + } var dbUser = new Models.InstancePermissionSet { @@ -88,6 +105,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return Created(dbUser.ToApi()); } + #pragma warning restore CA1506 /// /// Update the permissions for an . From 4885b2c5b68f50e96dfeee97d597aa06f5273480 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 1 Jan 2021 23:14:02 -0500 Subject: [PATCH 149/154] Fix /Configuration pagination --- .../Controllers/ConfigurationController.cs | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 1e07d538ce..f4688a200c 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -186,45 +187,40 @@ namespace Tgstation.Server.Host.Controllers [FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) - => Paginated( - async () => - { - if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity)) - return new PaginatableResult( - Forbid()); + => WithComponentInstance( + instance => Paginated( + async () => + { + if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity)) + return new PaginatableResult( + Forbid()); - try - { - return new PaginatableResult( - await WithComponentInstance( - async instance => - { - var result = await instance - .Configuration - .ListDirectory(directoryPath, systemIdentity, cancellationToken) - .ConfigureAwait(false); - if (result == null) - return Gone(); + try + { + var result = await instance + .Configuration + .ListDirectory(directoryPath, systemIdentity, cancellationToken) + .ConfigureAwait(false); + if (result == null) + return new PaginatableResult(Gone()); - return Json(result); - }) - .ConfigureAwait(false)); - } - catch (NotImplementedException) - { - return new PaginatableResult( - RequiresPosixSystemIdentity()); - } - catch (UnauthorizedAccessException) - { - return new PaginatableResult( - Forbid()); - } - }, - null, - page, - pageSize, - cancellationToken); + return new PaginatableResult(result.AsQueryable()); + } + catch (NotImplementedException) + { + return new PaginatableResult( + RequiresPosixSystemIdentity()); + } + catch (UnauthorizedAccessException) + { + return new PaginatableResult( + Forbid()); + } + }, + null, + page, + pageSize, + cancellationToken)); /// /// Get the contents of the root configuration directory. From 229104bc41d2cfea6e0231c312dcf4af00fa282e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 2 Jan 2021 16:56:38 -0500 Subject: [PATCH 150/154] Minor in-memory repository test improvement --- src/Tgstation.Server.Host/Components/InstanceFactory.cs | 2 +- .../Components/Repository/ILibGit2RepositoryFactory.cs | 6 +++--- .../Components/Repository/LibGit2RepositoryFactory.cs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 14a641696c..cd04474fb4 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -374,6 +374,6 @@ namespace Tgstation.Server.Host.Components /// /// Test that the is functional. /// - private void CheckSystemCompatibility() => repositoryFactory.CreateInMemory().Dispose(); + private void CheckSystemCompatibility() => repositoryFactory.CreateInMemory(); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs index 086ca8d93e..047671181d 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ILibGit2RepositoryFactory.cs @@ -11,10 +11,10 @@ namespace Tgstation.Server.Host.Components.Repository interface ILibGit2RepositoryFactory : ICredentialsProvider { /// - /// Create an in-memeory . + /// Create and destory an in-memeory . /// - /// A new in-memory . - LibGit2Sharp.IRepository CreateInMemory(); + /// Used as a test of the libgit2 native library. + void CreateInMemory(); /// /// Load a from a given . diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 137d6be054..182ca3c3dc 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -28,10 +28,11 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public LibGit2Sharp.IRepository CreateInMemory() + public void CreateInMemory() { logger.LogTrace("Creating in-memory libgit2 repository..."); - return new LibGit2Sharp.Repository(); + using var _ = new LibGit2Sharp.Repository(); + logger.LogTrace("Success"); } /// From a7d0a6894ee0f979263f901617f5dda2a79daeb3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 2 Jan 2021 17:45:30 -0500 Subject: [PATCH 151/154] Update to LibGit2Sharp preview 96 --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 3951c10411..f47a4814b7 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -67,7 +67,7 @@ - + From e1c568f1a1d7ee884a1cc1db6a351582a081d304 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 2 Jan 2021 18:50:11 -0500 Subject: [PATCH 152/154] Revert "Update to LibGit2Sharp preview 96" This reverts commit a7d0a6894ee0f979263f901617f5dda2a79daeb3. Let's not tempt fate this close to release --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index f47a4814b7..3951c10411 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -67,7 +67,7 @@ - + From fbb76c1291a6a1388c3c78004a2faa9e3e58cc8c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 2 Jan 2021 18:52:41 -0500 Subject: [PATCH 153/154] Switch from Ubuntu to Debian for Docker image --- build/Dockerfile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/build/Dockerfile b/build/Dockerfile index 63efba1465..6a1d46b45d 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,10 +1,10 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-bionic AS build +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build # install node and npm # replace shell with bash so we can source files RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | sh -ENV NODE_VERSION=10.13.0 +ENV NODE_VERSION 10.13.0 ENV NVM_DIR /root/.nvm RUN . $NVM_DIR/nvm.sh \ @@ -12,7 +12,7 @@ RUN . $NVM_DIR/nvm.sh \ && nvm use $NODE_VERSION \ && apt-get update \ && apt-get install -y \ - dos2unix \ + dos2unix \ && rm -rf /var/lib/apt/lists/* ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules @@ -56,14 +56,18 @@ RUN dotnet publish -c Release -o /app WORKDIR /repo/src/Tgstation.Server.Host RUN dotnet publish -c Release -o /app/lib/Default && mv /app/lib/Default/appsettings* /app -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-bionic +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim #needed for byond RUN apt-get update \ && apt-get install -y \ gcc-multilib \ gdb \ - && rm -rf /var/lib/apt/lists/* + multiarch-support \ + && rm -rf /var/lib/apt/lists/* \ + && curl http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.0.0_1.0.1t-1+deb8u12_amd64.deb --output libssl1.0.0.deb \ + && dpkg -i libssl1.0.0.deb \ + && rm libssl1.0.0.deb EXPOSE 5000 From 76ef4ff63c7e71cae64122767d1350f71eecb915 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 2 Jan 2021 20:10:46 -0500 Subject: [PATCH 154/154] Clean up the build --- src/Tgstation.Server.Host/Program.cs | 4 +--- src/Tgstation.Server.Host/Swarm/SwarmService.cs | 2 +- .../Components/Repository/TestRepositoryFactory.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs index ab0b58943d..fa83a096a9 100644 --- a/src/Tgstation.Server.Host/Program.cs +++ b/src/Tgstation.Server.Host/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -14,7 +14,6 @@ namespace Tgstation.Server.Host /// static class Program { -#pragma warning disable SA1401 // Fields must be private /// /// The expected host watchdog . /// @@ -24,7 +23,6 @@ namespace Tgstation.Server.Host /// The to use. /// internal static IServerFactory ServerFactory = Application.CreateDefaultServerFactory(); -#pragma warning restore SA1401 // Fields must be private /// /// Entrypoint for the diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 3cf7efc2b4..d52f3fb2b6 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -975,7 +975,7 @@ namespace Tgstation.Server.Host.Swarm var request = new HttpRequestMessage( httpMethod, - swarmServer.Address + subroute.Substring(1)); + swarmServer.Address + subroute[1..]); request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfiguration.PrivateKey); request.Headers.UserAgent.Clear(); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index 43b4c66124..a57e899721 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests [TestMethod] public void TestInMemoryRepoCreation() { - new LibGit2RepositoryFactory(Mock.Of>()).CreateInMemory().Dispose(); + new LibGit2RepositoryFactory(Mock.Of>()).CreateInMemory(); } [TestMethod]