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