From 79700c7d0e9821dad7edbb515ff0b991abca9860 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 22 Nov 2020 21:11:00 -0500 Subject: [PATCH 01/10] 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 03ebbde7d97234b06976224b3f61afc28f59a2df Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 23 Nov 2020 12:20:43 -0500 Subject: [PATCH 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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); - } - } -}