diff --git a/build/ControlPanelVersion.props b/build/ControlPanelVersion.props
index c2a4c37602..02d75e7142 100644
--- a/build/ControlPanelVersion.props
+++ b/build/ControlPanelVersion.props
@@ -1,6 +1,6 @@
- 4.25.5
+ 4.26.2
diff --git a/build/TestCommon.props b/build/TestCommon.props
index 5ee30f1147..5562510f72 100644
--- a/build/TestCommon.props
+++ b/build/TestCommon.props
@@ -15,8 +15,8 @@
-
-
+
+
diff --git a/build/Version.props b/build/Version.props
index 5059cb754d..349d3241c5 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -7,8 +7,8 @@
5.0.0
10.0.0
7.0.0
- 12.0.0
- 14.0.0
+ 13.0.0
+ 15.0.0
7.0.0
5.7.0
1.4.0
diff --git a/build/analyzers.ruleset b/build/analyzers.ruleset
index 0c5a955227..034747c138 100644
--- a/build/analyzers.ruleset
+++ b/build/analyzers.ruleset
@@ -1,4 +1,4 @@
-
+
@@ -88,7 +88,7 @@
-
+
@@ -971,7 +971,7 @@
-
+
diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm
index de420a2a32..07ce3b6845 100644
--- a/src/DMAPI/tgs/core/datum.dm
+++ b/src/DMAPI/tgs/core/datum.dm
@@ -12,8 +12,13 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
src.version = version
/datum/tgs_api/proc/TerminateWorld()
- del(world)
- sleep(1) // https://www.byond.com/forum/post/2894866
+ while(TRUE)
+ TGS_DEBUG_LOG("About to terminate world. Tick: [world.time], sleep_offline: [world.sleep_offline]")
+ world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866
+ del(world)
+ world.sleep_offline = FALSE // just in case, this is BYOND after all...
+ sleep(1)
+ TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]")
/datum/tgs_api/latest
parent_type = /datum/tgs_api/v5
diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs
index 7fe6edafc3..01e61cb373 100644
--- a/src/Tgstation.Server.Api/ApiHeaders.cs
+++ b/src/Tgstation.Server.Api/ApiHeaders.cs
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Tgstation.Server.Api.Models;
+using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Properties;
using Tgstation.Server.Common.Extensions;
@@ -57,6 +58,11 @@ namespace Tgstation.Server.Api
///
public const string ApplicationJsonMime = "application/json";
+ ///
+ /// Added to in netstandard2.1. Can't use because of lack of .NET Framework support.
+ ///
+ const string TextEventStreamMime = "text/event-stream";
+
///
/// Get the version of the the caller is using.
///
@@ -93,9 +99,9 @@ namespace Tgstation.Server.Api
public Version ApiVersion { get; }
///
- /// The client's JWT.
+ /// The client's .
///
- public string? Token { get; }
+ public TokenResponse? Token { get; }
///
/// The client's username.
@@ -107,13 +113,18 @@ namespace Tgstation.Server.Api
///
public string? Password { get; }
+ ///
+ /// The OAuth code in use.
+ ///
+ public string? OAuthCode { get; }
+
///
/// The the is for, if any.
///
public OAuthProvider? OAuthProvider { get; }
///
- /// If the header uses password or TGS JWT authentication.
+ /// If the header uses OAuth or TGS JWT authentication.
///
public bool IsTokenAuthentication => Token != null && !OAuthProvider.HasValue;
@@ -129,16 +140,31 @@ namespace Tgstation.Server.Api
///
/// The value of .
/// The value of .
- /// The value of .
- public ApiHeaders(ProductHeaderValue userAgent, string token, OAuthProvider? oauthProvider = null)
+ public ApiHeaders(ProductHeaderValue userAgent, TokenResponse token)
: this(userAgent, token, null, null)
{
if (userAgent == null)
throw new ArgumentNullException(nameof(userAgent));
if (token == null)
throw new ArgumentNullException(nameof(token));
+ if (token.Bearer == null)
+ throw new InvalidOperationException("token.Bearer must be set!");
+ }
- OAuthProvider = oauthProvider;
+ ///
+ /// Initializes a new instance of the class. Used for token authentication.
+ ///
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ public ApiHeaders(ProductHeaderValue userAgent, string oAuthCode, OAuthProvider oAuthProvider)
+ : this(userAgent, null, null, null)
+ {
+ if (userAgent == null)
+ throw new ArgumentNullException(nameof(userAgent));
+
+ OAuthCode = oAuthCode ?? throw new ArgumentNullException(nameof(oAuthCode));
+ OAuthProvider = oAuthProvider;
}
///
@@ -163,48 +189,57 @@ namespace Tgstation.Server.Api
///
/// The containing the serialized .
/// If a missing should be ignored.
+ /// If is a valid accept.
/// Thrown if the constitue invalid .
#pragma warning disable CA1502 // TODO: Decomplexify
- public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth = false)
+ public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth, bool allowEventStreamAccept)
{
if (requestHeaders == null)
throw new ArgumentNullException(nameof(requestHeaders));
- var badHeaders = HeaderTypes.None;
+ var badHeaders = HeaderErrorTypes.None;
var errorBuilder = new StringBuilder();
-
- void AddError(HeaderTypes headerType, string message)
+ var multipleErrors = false;
+ void AddError(HeaderErrorTypes headerType, string message)
{
- if (badHeaders != HeaderTypes.None)
+ if (badHeaders != HeaderErrorTypes.None)
+ {
+ multipleErrors = true;
errorBuilder.AppendLine();
+ }
+
badHeaders |= headerType;
errorBuilder.Append(message);
}
var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJsonMime);
- if (!requestHeaders.Accept.Any(x => jsonAccept.IsSubsetOf(x)))
- AddError(HeaderTypes.Accept, $"Client does not accept {ApplicationJsonMime}!");
+ var eventStreamAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(TextEventStreamMime);
+ if (!requestHeaders.Accept.Any(jsonAccept.IsSubsetOf))
+ if (!allowEventStreamAccept)
+ AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime}!");
+ else if (!requestHeaders.Accept.Any(eventStreamAccept.IsSubsetOf))
+ AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime} or {TextEventStreamMime}!");
if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0)
- AddError(HeaderTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
+ AddError(HeaderErrorTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
else
{
RawUserAgent = userAgentValues.First();
if (String.IsNullOrWhiteSpace(RawUserAgent))
- AddError(HeaderTypes.UserAgent, $"Malformed {HeaderNames.UserAgent} header!");
+ AddError(HeaderErrorTypes.UserAgent, $"Malformed {HeaderNames.UserAgent} header!");
}
// make sure the api header matches ours
Version? apiVersion = null;
if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
- AddError(HeaderTypes.Api, $"Missing {ApiVersionHeader} header!");
+ AddError(HeaderErrorTypes.Api, $"Missing {ApiVersionHeader} header!");
else if (!Version.TryParse(apiUserAgent.Product.Version, out apiVersion))
- AddError(HeaderTypes.Api, $"Malformed {ApiVersionHeader} header!");
+ AddError(HeaderErrorTypes.Api, $"Malformed {ApiVersionHeader} header!");
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
{
if (!ignoreMissingAuth)
- AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!");
+ AddError(HeaderErrorTypes.AuthorizationMissing, $"Missing {HeaderNames.Authorization} header!");
}
else
{
@@ -212,13 +247,13 @@ namespace Tgstation.Server.Api
var splits = new List(auth.Split(' '));
var scheme = splits.First();
if (String.IsNullOrWhiteSpace(scheme))
- AddError(HeaderTypes.Authorization, "Missing authentication scheme!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication scheme!");
else
{
splits.RemoveAt(0);
var parameter = String.Concat(splits);
if (String.IsNullOrEmpty(parameter))
- AddError(HeaderTypes.Authorization, "Missing authentication parameter!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication parameter!");
else
{
if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
@@ -237,14 +272,30 @@ namespace Tgstation.Server.Api
if (Enum.TryParse(oauthProviderString, out var oauthProvider))
OAuthProvider = oauthProvider;
else
- AddError(HeaderTypes.OAuthProvider, "Invalid OAuth provider!");
+ AddError(HeaderErrorTypes.OAuthProvider, "Invalid OAuth provider!");
}
else
- AddError(HeaderTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!");
+ AddError(HeaderErrorTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!");
- goto case BearerAuthenticationScheme;
+ OAuthCode = parameter;
+ break;
case BearerAuthenticationScheme:
- Token = parameter;
+ Token = new TokenResponse
+ {
+ Bearer = parameter,
+ };
+
+ try
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ Token.ExpiresAt = Token.ParseJwt().ValidTo;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+ catch (ArgumentException ex) when (ex is not ArgumentNullException)
+ {
+ AddError(HeaderErrorTypes.AuthorizationInvalid, $"Invalid JWT: {ex.Message}");
+ }
+
break;
case BasicAuthenticationScheme:
string badBasicAuthHeaderMessage = $"Invalid basic {HeaderNames.Authorization} header!";
@@ -256,14 +307,14 @@ namespace Tgstation.Server.Api
}
catch
{
- AddError(HeaderTypes.Authorization, badBasicAuthHeaderMessage);
+ AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
break;
}
var basicAuthSplits = joinedString.Split(ColonSeparator, StringSplitOptions.RemoveEmptyEntries);
if (basicAuthSplits.Length < 2)
{
- AddError(HeaderTypes.Authorization, badBasicAuthHeaderMessage);
+ AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
break;
}
@@ -271,15 +322,20 @@ namespace Tgstation.Server.Api
Password = String.Concat(basicAuthSplits.Skip(1));
break;
default:
- AddError(HeaderTypes.Authorization, "Invalid authentication scheme!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid authentication scheme!");
break;
}
}
}
}
- if (badHeaders != HeaderTypes.None)
+ if (badHeaders != HeaderErrorTypes.None)
+ {
+ if (multipleErrors)
+ errorBuilder.Insert(0, $"Multiple header validation errors occurred:{Environment.NewLine}");
+
throw new HeadersException(badHeaders, errorBuilder.ToString());
+ }
ApiVersion = apiVersion!.Semver();
}
@@ -292,7 +348,7 @@ namespace Tgstation.Server.Api
/// The value of .
/// The value of .
/// The value of .
- ApiHeaders(ProductHeaderValue userAgent, string? token, string? username, string? password)
+ ApiHeaders(ProductHeaderValue userAgent, TokenResponse? token, string? username, string? password)
{
RawUserAgent = userAgent?.ToString();
Token = token;
@@ -322,10 +378,10 @@ namespace Tgstation.Server.Api
headers.Clear();
headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJsonMime));
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
- headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
+ headers.Add(ApiVersionHeader, CreateApiVersionHeader());
if (OAuthProvider.HasValue)
{
- headers.Authorization = new AuthenticationHeaderValue(OAuthAuthenticationScheme, Token);
+ headers.Authorization = new AuthenticationHeaderValue(OAuthAuthenticationScheme, OAuthCode!);
headers.Add(OAuthProviderHeader, OAuthProvider.ToString());
}
else if (!IsTokenAuthentication)
@@ -333,11 +389,32 @@ namespace Tgstation.Server.Api
BasicAuthenticationScheme,
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
else
- headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token);
+ headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token!.Bearer);
instanceId ??= InstanceId;
if (instanceId.HasValue)
headers.Add(InstanceIdHeader, instanceId.Value.ToString(CultureInfo.InvariantCulture));
}
+
+ ///
+ /// Adds the necessary for a SignalR hub connection.
+ ///
+ /// The headers to write to.
+ public void SetHubConnectionHeaders(IDictionary headers)
+ {
+ if (headers == null)
+ throw new ArgumentNullException(nameof(headers));
+
+ headers.Add(HeaderNames.UserAgent, RawUserAgent ?? throw new InvalidOperationException("Missing UserAgent!"));
+ headers.Add(HeaderNames.Accept, ApplicationJsonMime);
+ headers.Add(ApiVersionHeader, CreateApiVersionHeader());
+ }
+
+ ///
+ /// Create the ified for of the .
+ ///
+ /// A representing the .
+ string CreateApiVersionHeader()
+ => new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString();
}
}
diff --git a/src/Tgstation.Server.Api/HeaderErrorTypes.cs b/src/Tgstation.Server.Api/HeaderErrorTypes.cs
new file mode 100644
index 0000000000..414b1e22d3
--- /dev/null
+++ b/src/Tgstation.Server.Api/HeaderErrorTypes.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace Tgstation.Server.Api
+{
+ ///
+ /// Types of individual errors.
+ ///
+ [Flags]
+ public enum HeaderErrorTypes
+ {
+ ///
+ /// No header errors.
+ ///
+ None = 0,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ UserAgent = 1 << 0,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ Accept = 1 << 1,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ Api = 1 << 2,
+
+ ///
+ /// The header is invalid.
+ ///
+ AuthorizationInvalid = 1 << 3,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ OAuthProvider = 1 << 4,
+
+ ///
+ /// The header is missing.
+ ///
+ AuthorizationMissing = 1 << 5,
+ }
+}
diff --git a/src/Tgstation.Server.Api/HeaderTypes.cs b/src/Tgstation.Server.Api/HeaderTypes.cs
deleted file mode 100644
index a5fdca325d..0000000000
--- a/src/Tgstation.Server.Api/HeaderTypes.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-
-namespace Tgstation.Server.Api
-{
- ///
- /// Types of individual .
- ///
- [Flags]
- public enum HeaderTypes
- {
- ///
- /// No headers.
- ///
- None = 0,
-
- ///
- /// header.
- ///
- UserAgent = 1 << 0,
-
- ///
- /// header.
- ///
- Accept = 1 << 1,
-
- ///
- /// .
- ///
- Api = 1 << 2,
-
- ///
- ///
- ///
- Authorization = 1 << 3,
-
- ///
- /// .
- ///
- OAuthProvider = 1 << 4,
- }
-}
diff --git a/src/Tgstation.Server.Api/HeadersException.cs b/src/Tgstation.Server.Api/HeadersException.cs
index 6287ee1c88..352d2ce9d8 100644
--- a/src/Tgstation.Server.Api/HeadersException.cs
+++ b/src/Tgstation.Server.Api/HeadersException.cs
@@ -8,19 +8,19 @@ namespace Tgstation.Server.Api
public sealed class HeadersException : Exception
{
///
- /// The s that are missing or malformed.
+ /// The s that are missing or malformed.
///
- public HeaderTypes MissingOrMalformedHeaders { get; }
+ public HeaderErrorTypes ParseErrors { get; }
///
/// Initializes a new instance of the class.
///
- /// The value of .
+ /// The value of .
/// The error message.
- public HeadersException(HeaderTypes missingOrMalformedHeaders, string message)
+ public HeadersException(HeaderErrorTypes parseErrors, string message)
: base(message)
{
- MissingOrMalformedHeaders = missingOrMalformedHeaders;
+ ParseErrors = parseErrors;
}
///
diff --git a/src/Tgstation.Server.Api/Hubs/IJobsHub.cs b/src/Tgstation.Server.Api/Hubs/IJobsHub.cs
new file mode 100644
index 0000000000..4c595362cb
--- /dev/null
+++ b/src/Tgstation.Server.Api/Hubs/IJobsHub.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+using Tgstation.Server.Api.Models.Response;
+
+namespace Tgstation.Server.Api.Hubs
+{
+ ///
+ /// SignalR client methods for receiving s.
+ ///
+ public interface IJobsHub
+ {
+ ///
+ /// Push a update to the client.
+ ///
+ /// The to push.
+ /// The for the operation.
+ /// A representing the running operation.
+ Task ReceiveJobUpdate(JobResponse job, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs
index 4dba0e189c..d002d80e5c 100644
--- a/src/Tgstation.Server.Api/Models/ErrorCode.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs
@@ -326,7 +326,7 @@ namespace Tgstation.Server.Api.Models
///
/// The DMAPI never validated itself
///
- [Description("The server did not validate the DMAPI! This can occur if your world is encountering runtime errors during startup.")]
+ [Description("DMAPI validation failed! See FAQ at https://github.com/tgstation/tgstation-server/discussions/1695")]
DeploymentNeverValidated,
///
diff --git a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs
index 863e034d09..da6af69640 100644
--- a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs
@@ -1,6 +1,4 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
+using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
@@ -31,14 +29,6 @@ namespace Tgstation.Server.Api.Models.Internal
[Required]
public EngineRights? EngineRights { get; set; }
- ///
- /// The legacy of the .
- ///
- [Required]
- [Obsolete("Use EngineRights instead")]
- [NotMapped]
- public EngineRights? ByondRights { get; set; }
-
///
/// The of the .
///
diff --git a/src/Tgstation.Server.Api/Models/Internal/Job.cs b/src/Tgstation.Server.Api/Models/Internal/Job.cs
index 54661a2eac..8b3b210c60 100644
--- a/src/Tgstation.Server.Api/Models/Internal/Job.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/Job.cs
@@ -10,9 +10,16 @@ namespace Tgstation.Server.Api.Models.Internal
///
public class Job : EntityId
{
+ ///
+ /// The .
+ ///
+ [Required]
+ public JobCode? JobCode { get; set; }
+
///
/// English description of the .
///
+ /// May not match the listed on the .
[Required]
public string? Description { get; set; }
diff --git a/src/Tgstation.Server.Api/Models/JobCode.cs b/src/Tgstation.Server.Api/Models/JobCode.cs
new file mode 100644
index 0000000000..e82600b302
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/JobCode.cs
@@ -0,0 +1,112 @@
+using System.ComponentModel;
+
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ /// The different types of .
+ ///
+ public enum JobCode : byte
+ {
+ ///
+ /// This catch-all code is applied to jobs that were created on a tgstation-server before v5.17.0.
+ ///
+ [Description("Legacy job")]
+ Unknown,
+
+ ///
+ /// When the instance is being moved.
+ ///
+ [Description("Instance move")]
+ Move,
+
+ ///
+ /// When the repository is cloning.
+ ///
+ [Description("Clone repository")]
+ RepositoryClone,
+
+ ///
+ /// When the repository is being manually updated.
+ ///
+ [Description("Update repository")]
+ RepositoryUpdate,
+
+ ///
+ /// When the repository is being automatically updated.
+ ///
+ [Description("Scheduled repository update")]
+ RepositoryAutoUpdate,
+
+ ///
+ /// When the repository is being deleted.
+ ///
+ [Description("Delete repository")]
+ RepositoryDelete,
+
+ ///
+ /// When a new official engine version is being installed.
+ ///
+ [Description("Install engine version")]
+ EngineOfficialInstall,
+
+ ///
+ /// When a new custom engine version is being installed.
+ ///
+ [Description("Install custom engine version")]
+ EngineCustomInstall,
+
+ ///
+ /// When an installed engine version is being deleted.
+ ///
+ [Description("Delete installed engine version")]
+ EngineDelete,
+
+ ///
+ /// When a deployment is manually triggered.
+ ///
+ [Description("Compile active repository code")]
+ Deployment,
+
+ ///
+ /// When a deployment is automatically triggered.
+ ///
+ [Description("Scheduled code deployment")]
+ AutomaticDeployment,
+
+ ///
+ /// When the watchdog is started manually.
+ ///
+ [Description("Launch Watchdog")]
+ WatchdogLaunch,
+
+ ///
+ /// When the watchdog is restarted manually.
+ ///
+ [Description("Restart Watchdog")]
+ WatchdogRestart,
+
+ ///
+ /// When a the watchdog is dumping the game server process.
+ ///
+ [Description("Create DreamDaemon Process Dump")]
+ WatchdogDump,
+
+ ///
+ /// When the watchdog starts due to an instance being onlined.
+ ///
+ [Description("Instance startup watchdog launch")]
+ StartupWatchdogLaunch,
+
+ ///
+ /// When the watchdog reattaches due to an instance being onlined.
+ ///
+ [Description("Instance startup watchdog reattach")]
+ StartupWatchdogReattach,
+
+ ///
+ /// When a chat bot connects/reconnects.
+ ///
+ [Description("Reconnect chat bot")]
+ ReconnectChatBot,
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Request/UserCreateRequest.cs b/src/Tgstation.Server.Api/Models/Request/UserCreateRequest.cs
index 5f3b4e09da..9c35ee12ce 100644
--- a/src/Tgstation.Server.Api/Models/Request/UserCreateRequest.cs
+++ b/src/Tgstation.Server.Api/Models/Request/UserCreateRequest.cs
@@ -3,7 +3,6 @@
///
/// For creating a user.
///
-#pragma warning disable CA1501
public sealed class UserCreateRequest : UserUpdateRequest
{
}
diff --git a/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs b/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs
index 0ad0283749..7f3415bb5b 100644
--- a/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs
+++ b/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs
@@ -17,12 +17,6 @@ namespace Tgstation.Server.Api.Models.Response
///
public RevisionInformation? RevisionInformation { get; set; }
- ///
- /// The the was made with.
- ///
- [Obsolete("Use EngineVersion instead.")]
- public string? ByondVersion { get; set; }
-
///
/// The the was made with.
///
diff --git a/src/Tgstation.Server.Api/Models/Response/JobResponse.cs b/src/Tgstation.Server.Api/Models/Response/JobResponse.cs
index eb194af473..053fe1f9aa 100644
--- a/src/Tgstation.Server.Api/Models/Response/JobResponse.cs
+++ b/src/Tgstation.Server.Api/Models/Response/JobResponse.cs
@@ -5,6 +5,11 @@
///
public sealed class JobResponse : Internal.Job
{
+ ///
+ /// The of the .
+ ///
+ public long? InstanceId { get; set; }
+
///
/// The that started the job.
///
diff --git a/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs b/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs
index cd143ede8c..8f7b01762b 100644
--- a/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs
+++ b/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs
@@ -1,5 +1,7 @@
using System;
+using Microsoft.IdentityModel.JsonWebTokens;
+
namespace Tgstation.Server.Api.Models.Response
{
///
@@ -15,6 +17,13 @@ namespace Tgstation.Server.Api.Models.Response
///
/// When the expires.
///
- public DateTimeOffset ExpiresAt { get; set; }
+ [Obsolete("Will be removed in a future API version")]
+ public DateTimeOffset? ExpiresAt { get; set; }
+
+ ///
+ /// Parses the as a .
+ ///
+ /// A new based on .
+ public JsonWebToken ParseJwt() => new (Bearer);
}
}
diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
index 9df78bb81b..2f60e9a0a0 100644
--- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs
+++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
namespace Tgstation.Server.Api.Rights
{
@@ -32,6 +33,15 @@ namespace Tgstation.Server.Api.Rights
/// The of the given .
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
+ ///
+ /// Map a given to its respective .
+ ///
+ /// The of the right.
+ /// The of .
+ public static RightsType TypeToRight()
+ where TRight : Enum
+ => TypeMap.First(kvp => kvp.Value == typeof(TRight)).Key;
+
///
/// Gets the role claim name used for a given .
///
diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs
index ece56b6876..e4a5f75a0a 100644
--- a/src/Tgstation.Server.Api/Routes.cs
+++ b/src/Tgstation.Server.Api/Routes.cs
@@ -12,6 +12,11 @@ namespace Tgstation.Server.Api
///
public const string Root = "/";
+ ///
+ /// The root route of all hubs.
+ ///
+ public const string HubsRoot = Root + "hubs";
+
///
/// The server administration controller.
///
@@ -102,6 +107,11 @@ namespace Tgstation.Server.Api
///
public const string List = "List";
+ ///
+ /// The root route of all hubs.
+ ///
+ public const string JobsHub = HubsRoot + "/jobs";
+
///
/// Apply an postfix to a .
///
diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
index 582c079b6e..e499846add 100644
--- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
+++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
@@ -26,6 +26,8 @@
+
+
diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs
index a235496592..4f6be11b5a 100644
--- a/src/Tgstation.Server.Client/ApiClient.cs
+++ b/src/Tgstation.Server.Client/ApiClient.cs
@@ -11,6 +11,10 @@ using System.Threading;
using System.Threading.Tasks;
using System.Web;
+using Microsoft.AspNetCore.Http.Connections;
+using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
@@ -20,6 +24,7 @@ using Newtonsoft.Json.Serialization;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
+using Tgstation.Server.Client.Extensions;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
@@ -51,6 +56,18 @@ namespace Tgstation.Server.Client
set => httpClient.Timeout = value;
}
+ ///
+ /// The to use.
+ ///
+ static readonly JsonSerializerSettings SerializerSettings = new ()
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new[]
+ {
+ new VersionConverter(),
+ },
+ };
+
///
/// The for the .
///
@@ -61,6 +78,11 @@ namespace Tgstation.Server.Client
///
readonly List requestLoggers;
+ ///
+ /// List of s created by the .
+ ///
+ readonly List hubConnections;
+
///
/// Backing field for .
///
@@ -82,14 +104,9 @@ namespace Tgstation.Server.Client
ApiHeaders headers;
///
- /// Get the to use.
+ /// If the is disposed.
///
- /// A new instance.
- static JsonSerializerSettings GetSerializerSettings() => new ()
- {
- ContractResolver = new CamelCasePropertyNamesContractResolver(),
- Converters = new[] { new VersionConverter() },
- };
+ bool disposed;
///
/// Handle a bad HTTP .
@@ -102,7 +119,7 @@ namespace Tgstation.Server.Client
try
{
// check if json serializes to an error message
- errorMessage = JsonConvert.DeserializeObject(json, GetSerializerSettings());
+ errorMessage = JsonConvert.DeserializeObject(json, SerializerSettings);
}
catch (JsonException)
{
@@ -149,7 +166,12 @@ namespace Tgstation.Server.Client
/// The value of .
/// The value of .
/// The value of .
- public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders, bool authless)
+ public ApiClient(
+ IHttpClient httpClient,
+ Uri url,
+ ApiHeaders apiHeaders,
+ ApiHeaders? tokenRefreshHeaders,
+ bool authless)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Url = url ?? throw new ArgumentNullException(nameof(url));
@@ -158,12 +180,27 @@ namespace Tgstation.Server.Client
this.authless = authless;
requestLoggers = new List();
+ hubConnections = new List();
semaphoreSlim = new SemaphoreSlim(1);
}
///
- public void Dispose()
+ public async ValueTask DisposeAsync()
{
+ List localHubConnections;
+ lock (hubConnections)
+ {
+ if (disposed)
+ return;
+
+ disposed = true;
+
+ localHubConnections = hubConnections.ToList();
+ hubConnections.Clear();
+ }
+
+ await ValueTaskExtensions.WhenAll(hubConnections.Select(connection => connection.DisposeAsync()));
+
httpClient.Dispose();
semaphoreSlim.Dispose();
}
@@ -294,6 +331,130 @@ namespace Tgstation.Server.Client
}
}
+ ///
+ /// Attempt to refresh the stored Bearer token in .
+ ///
+ /// The for the operation.
+ /// A resulting in if the refresh was successful, if a refresh is unable to be performed.
+ public async ValueTask RefreshToken(CancellationToken cancellationToken)
+ {
+ if (tokenRefreshHeaders == null)
+ return false;
+
+ var startingToken = headers.Token;
+ await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (startingToken != headers.Token)
+ return true;
+
+ var token = await RunRequest
public InstanceWrapper()
{
- Uid = Guid.NewGuid();
+ Uid = Interlocked.Increment(ref instanceWrapperInstances);
}
///
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index a7165eed4f..2f046830d0 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -636,10 +636,12 @@ namespace Tgstation.Server.Host.Components.Session
return;
}
- Logger.LogDebug("Server will terminated in 10s if it does not exit...");
- var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(10), CancellationToken.None); // DCT: None available
- var completedTask = await Task.WhenAny(process.Lifetime, delayTask);
- if (completedTask == delayTask)
+ const int GracePeriodSeconds = 30;
+ Logger.LogDebug("Server will terminated in {gracePeriodSeconds}s if it does not exit...", GracePeriodSeconds);
+ var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(GracePeriodSeconds), CancellationToken.None); // DCT: None available
+ await Task.WhenAny(process.Lifetime, delayTask);
+
+ if (!process.Lifetime.IsCompleted)
{
Logger.LogWarning("DMAPI took too long to shutdown server after validation request!");
process.Terminate();
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
index eddcd63973..5e22bb2a52 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
@@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
///
static readonly IReadOnlyDictionary> EventTypeScriptFileNameMap = new Dictionary>(
Enum.GetValues(typeof(EventType))
- .OfType()
+ .Cast()
.Select(
eventType => new KeyValuePair>(
eventType,
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs
index 35d3e508e7..9d8f8cbda8 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs
@@ -29,11 +29,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
protected SwappableDmbProvider ActiveSwappable { get; private set; }
- ///
- /// The for the pointing to the Game directory.
- ///
- protected IIOManager GameIOManager { get; }
-
///
/// The for the .
///
@@ -64,10 +59,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the .
/// The for the .
/// The for the .
- /// The for the .
+ /// The 'Diagnostics' for the .
/// The for the .
/// The for the .
- /// The value of .
+ /// The 'Game' for the .
/// The value of .
/// The for the .
/// The for the .
@@ -101,6 +96,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
+ gameIOManager,
logger,
initialLaunchParameters,
instance,
@@ -108,7 +104,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
try
{
- GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
LinkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
deploymentCleanupTasks = new List();
@@ -236,14 +231,14 @@ namespace Tgstation.Server.Host.Components.Watchdog
IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
bool canSeamlesslySwap = true;
- if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
+ if (compileJobProvider.CompileJob.EngineVersion != ActiveCompileJob.EngineVersion)
{
// have to do a graceful restart
Logger.LogDebug(
"Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
compileJobProvider.CompileJob.Id,
- compileJobProvider.CompileJob.ByondVersion,
- ActiveCompileJob.ByondVersion);
+ compileJobProvider.CompileJob.EngineVersion,
+ ActiveCompileJob.EngineVersion);
canSeamlesslySwap = false;
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
index 046587273b..2fd1d3a5c7 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
@@ -50,9 +50,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the .
/// The for the .
/// The for the .
- /// The for the .
+ /// The 'Diagnostics' for the .
/// The for the .
/// The for the .
+ /// The 'Game' for the .
/// The for the .
/// The for the .
/// The for the .
@@ -68,6 +69,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
+ IIOManager gameIOManager,
ILogger logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance instance,
@@ -83,6 +85,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
+ gameIOManager,
logger,
initialLaunchParameters,
instance,
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
index b7b72a31bd..97fad794f3 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -96,6 +96,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
protected IAsyncDelayer AsyncDelayer { get; }
+ ///
+ /// The for the pointing to the Game directory.
+ ///
+ protected IIOManager GameIOManager { get; }
+
///
/// The for the .
///
@@ -184,6 +189,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The value of .
/// The initial value of . May be modified.
/// The value of .
@@ -199,6 +205,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
+ IIOManager gameIOManager,
ILogger logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance metadata,
@@ -213,6 +220,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
+ GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
@@ -369,16 +377,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (!autoStart && !reattaching)
return;
- var job = new Models.Job
- {
- Instance = new Models.Instance
- {
- Id = metadata.Id,
- },
- Description = $"Instance startup watchdog {(reattaching ? "reattach" : "launch")}",
- CancelRight = (ulong)DreamDaemonRights.Shutdown,
- CancelRightsType = RightsType.DreamDaemon,
- };
+ var job = Models.Job.Create(
+ reattaching
+ ? JobCode.StartupWatchdogReattach
+ : JobCode.StartupWatchdogLaunch,
+ null,
+ metadata,
+ DreamDaemonRights.Shutdown);
await jobManager.RegisterOperation(
job,
async (core, databaseContextFactory, paramJob, progressFunction, ct) =>
@@ -679,6 +684,15 @@ namespace Tgstation.Server.Host.Components.Watchdog
metadata,
newCompileJob);
+ var eventTask = eventConsumer.HandleEvent(
+ EventType.DeploymentActivation,
+ new List
+ {
+ GameIOManager.ResolvePath(newCompileJob.DirectoryName.ToString()),
+ },
+ false,
+ cancellationToken);
+
try
{
await remoteDeploymentManager.ApplyDeployment(newCompileJob, cancellationToken);
@@ -687,6 +701,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
Logger.LogWarning(ex, "Failed to apply remote deployment!");
}
+
+ await eventTask;
}
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs
index 2fb7182755..e9a37ecb2b 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs
@@ -90,6 +90,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerfactory,
+ gameIOManager,
LoggerFactory.CreateLogger(),
settings,
instance,
diff --git a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs
index 3685be3ec8..aa798cfed3 100644
--- a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs
+++ b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs
@@ -57,16 +57,17 @@ namespace Tgstation.Server.Host.Configuration
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
ArgumentNullException.ThrowIfNull(platformIdentifier);
- var directoryToUse = platformIdentifier.IsWindows
- ? Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) // C:/ProgramData
- : "/var/log"; // :pain:
+ if (!String.IsNullOrEmpty(Directory))
+ return Directory;
- return !String.IsNullOrEmpty(Directory)
- ? Directory
- : ioManager.ConcatPath(
- directoryToUse,
+ return platformIdentifier.IsWindows
+ ? ioManager.ConcatPath(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
assemblyInformationProvider.VersionPrefix,
- "logs");
+ "logs")
+ : ioManager.ConcatPath(
+ "/var/log",
+ assemblyInformationProvider.VersionPrefix);
}
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index b742a2ae18..c1da69a69d 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -18,6 +18,7 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
@@ -25,6 +26,7 @@ using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Transfer;
+using Tgstation.Server.Host.Utils;
using Tgstation.Server.Host.Utils.GitHub;
namespace Tgstation.Server.Host.Controllers
@@ -84,7 +86,7 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The value of .
/// The value of .
/// The value of .
@@ -94,9 +96,10 @@ namespace Tgstation.Server.Host.Controllers
/// The value of .
/// The for the .
/// The containing value of .
+ /// The for the .
public AdministrationController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
IGitHubServiceFactory gitHubServiceFactory,
IServerControl serverControl,
IServerUpdateInitiator serverUpdateInitiator,
@@ -105,10 +108,12 @@ namespace Tgstation.Server.Host.Controllers
IPlatformIdentifier platformIdentifier,
IFileTransferTicketProvider fileTransferService,
ILogger logger,
- IOptions fileLoggingConfigurationOptions)
+ IOptions fileLoggingConfigurationOptions,
+ IApiHeadersProvider apiHeadersProvider)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
+ apiHeadersProvider,
logger,
true)
{
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index c0f37fbf82..ffa4e84960 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -3,13 +3,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
-using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Logging;
@@ -23,6 +21,7 @@ using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Common.Extensions;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
@@ -34,9 +33,7 @@ namespace Tgstation.Server.Host.Controllers
///
/// Base for API functions.
///
- [Produces(MediaTypeNames.Application.Json)]
- [ApiController]
- public abstract class ApiController : Controller
+ public abstract class ApiController : ApiControllerBase
{
///
/// Default size of results.
@@ -51,7 +48,12 @@ namespace Tgstation.Server.Host.Controllers
///
/// The for the operation.
///
- protected ApiHeaders ApiHeaders { get; private set; }
+ protected ApiHeaders ApiHeaders => ApiHeadersProvider.ApiHeaders;
+
+ ///
+ /// The containing value of .
+ ///
+ protected IApiHeadersProvider ApiHeadersProvider { get; }
///
/// The for the operation.
@@ -82,69 +84,43 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The value of .
- /// The for the .
+ /// The for the .
/// The value of .
+ /// The value of ..
/// The value of .
protected ApiController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
+ IApiHeadersProvider apiHeadersProvider,
ILogger logger,
bool requireHeaders)
{
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
- ArgumentNullException.ThrowIfNull(authenticationContextFactory);
+ AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
+ ApiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
- AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
+
Instance = AuthenticationContext?.InstancePermissionSet?.Instance;
this.requireHeaders = requireHeaders;
}
///
#pragma warning disable CA1506 // TODO: Decomplexify
- public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken)
{
- ArgumentNullException.ThrowIfNull(context);
-
- // ALL valid token and login requests that match a route go through this function
- // 404 is returned before
- if (AuthenticationContext != null && AuthenticationContext.User == null)
- {
- // valid token, expired password
- await Unauthorized().ExecuteResultAsync(context);
- return;
- }
+ ArgumentNullException.ThrowIfNull(executeAction);
// validate the headers
- try
- {
- ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
-
- if (!ApiHeaders.Compatible())
- {
- await this.StatusCode(
- HttpStatusCode.UpgradeRequired,
- new ErrorMessageResponse(ErrorCode.ApiMismatch))
- .ExecuteResultAsync(context);
- return;
- }
-
- var errorCase = await ValidateRequest(context.HttpContext.RequestAborted);
- if (errorCase != null)
- {
- await errorCase.ExecuteResultAsync(context);
- return;
- }
- }
- catch (HeadersException)
+ if (ApiHeaders == null)
{
if (requireHeaders)
- {
- await HeadersIssue(false)
- .ExecuteResultAsync(context);
- return;
- }
+ return HeadersIssue(ApiHeadersProvider.HeadersException);
}
+ var errorCase = await ValidateRequest(cancellationToken);
+ if (errorCase != null)
+ return errorCase;
+
if (ModelState?.IsValid == false)
{
var errorMessages = ModelState
@@ -158,15 +134,11 @@ namespace Tgstation.Server.Host.Controllers
.Where(x => !x.EndsWith(" field is required.", StringComparison.Ordinal));
if (errorMessages.Any())
- {
- await BadRequest(
+ return BadRequest(
new ErrorMessageResponse(ErrorCode.ModelValidationFailure)
{
AdditionalData = String.Join(Environment.NewLine, errorMessages),
- })
- .ExecuteResultAsync(context);
- return;
- }
+ });
ModelState.Clear();
}
@@ -174,7 +146,7 @@ namespace Tgstation.Server.Host.Controllers
using (ApiHeaders?.InstanceId != null
? LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, ApiHeaders.InstanceId)
: null)
- using (AuthenticationContext != null
+ using (AuthenticationContext.Valid
? LogContext.PushProperty(SerilogContextHelper.UserIdContextProperty, AuthenticationContext.User.Id)
: null)
using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}"))
@@ -182,15 +154,14 @@ namespace Tgstation.Server.Host.Controllers
if (ApiHeaders != null)
{
var isGet = HttpMethods.IsGet(Request.Method);
- if (!(isGet && Request.Path.StartsWithSegments(Routes.Jobs, StringComparison.OrdinalIgnoreCase)))
- Logger.Log(
- isGet
- ? LogLevel.Trace
- : LogLevel.Debug,
- "Starting API request: Version: {clientApiVersion}. {userAgentHeaderName}: {clientUserAgent}",
- ApiHeaders.ApiVersion.Semver(),
- HeaderNames.UserAgent,
- ApiHeaders.RawUserAgent);
+ Logger.Log(
+ isGet
+ ? LogLevel.Trace
+ : LogLevel.Debug,
+ "Starting API request: Version: {clientApiVersion}. {userAgentHeaderName}: {clientUserAgent}",
+ ApiHeaders.ApiVersion.Semver(),
+ HeaderNames.UserAgent,
+ ApiHeaders.RawUserAgent);
}
else if (Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgents))
Logger.LogDebug(
@@ -201,17 +172,26 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogDebug(
"Starting unauthorized API request. No {userAgentHeaderName}!",
HeaderNames.UserAgent);
- await base.OnActionExecutionAsync(context, next);
+
+ await executeAction();
}
+
+ return null;
}
#pragma warning restore CA1506
///
/// Generic 404 response.
///
- /// An with .
+ /// A with an appropriate .
protected new NotFoundObjectResult NotFound() => NotFound(new ErrorMessageResponse(ErrorCode.ResourceNeverPresent));
+ ///
+ /// Generic 401 response.
+ ///
+ /// An with .
+ protected new ObjectResult Unauthorized() => this.StatusCode(HttpStatusCode.Unauthorized, null);
+
///
/// Generic 501 response.
///
@@ -264,27 +244,19 @@ namespace Tgstation.Server.Host.Controllers
///
/// Response for missing/Invalid headers.
///
- /// Whether or not errors due to missing should be thrown.
+ /// The that occurred while trying to parse the .
/// The appropriate .
- protected IActionResult HeadersIssue(bool ignoreMissingAuth)
+ protected IActionResult HeadersIssue(HeadersException headersException)
{
- HeadersException headersException;
- try
- {
- _ = new ApiHeaders(Request.GetTypedHeaders(), ignoreMissingAuth);
+ if (headersException == null)
throw new InvalidOperationException("Expected a header parse exception!");
- }
- catch (HeadersException ex)
- {
- headersException = ex;
- }
var errorMessage = new ErrorMessageResponse(ErrorCode.BadHeaders)
{
AdditionalData = headersException.Message,
};
- if (headersException.MissingOrMalformedHeaders.HasFlag(HeaderTypes.Accept))
+ if (headersException.ParseErrors.HasFlag(HeaderErrorTypes.Accept))
return this.StatusCode(HttpStatusCode.NotAcceptable, errorMessage);
return BadRequest(errorMessage);
diff --git a/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs b/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs
new file mode 100644
index 0000000000..9a8b119808
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Tgstation.Server.Host.Controllers
+{
+ ///
+ /// Base class for all API style controllers.
+ ///
+ [Produces(MediaTypeNames.Application.Json)]
+ [ApiController]
+ public abstract class ApiControllerBase : Controller
+ {
+ ///
+ public sealed override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ // never cache an API response
+ Response.Headers.Add(HeaderNames.CacheControl, new StringValues("no-cache"));
+
+ var errorCase = await HookExecuteAction(
+ () => base.OnActionExecutionAsync(context, next),
+ Request.HttpContext.RequestAborted);
+
+ if (errorCase != null)
+ await errorCase.ExecuteResultAsync(context);
+ }
+
+ ///
+ /// Hook for executing a request.
+ ///
+ /// A that should be invoked and its response awaited to continue normal execution of the request. Should NOT be called if this method returns a non- value.
+ /// The for the operation.
+ /// A resulting in an that, if not , is executed.
+ protected virtual async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(executeAction);
+
+ await executeAction();
+ return null;
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs
index 19c2794f04..53e64e08dc 100644
--- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs
@@ -4,6 +4,7 @@ using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -20,10 +21,8 @@ namespace Tgstation.Server.Host.Controllers
/// for receiving DMAPI requests from DreamDaemon.
///
[Route("/Bridge")]
- [Produces(MediaTypeNames.Application.Json)]
- [ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
- public class BridgeController : Controller
+ public sealed class BridgeController : ApiControllerBase
{
///
/// If the content of bridge requests and responses should be logged.
@@ -76,6 +75,7 @@ namespace Tgstation.Server.Host.Controllers
/// The for the operation.
/// A resulting in the for the operation.
[HttpGet]
+ [AllowAnonymous]
public async ValueTask Process([FromQuery] string data, CancellationToken cancellationToken)
{
// Nothing to see here
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
index c1365b1f84..7f9682c43a 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -19,11 +19,12 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
-
+using Tgstation.Server.Host.Utils;
using Z.EntityFramework.Plus;
namespace Tgstation.Server.Host.Controllers
@@ -39,19 +40,22 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
+ /// The for the .
public ChatController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
- IInstanceManager instanceManager)
+ IInstanceManager instanceManager,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
}
diff --git a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs
index 45e3829bbb..eef1af8b18 100644
--- a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs
@@ -40,18 +40,21 @@ namespace Tgstation.Server.Host.Controllers
///
/// The value of .
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
+ /// The for the .
/// The value of .
protected ComponentInterfacingController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
- bool useInstanceRequestHeader = false)
+ IApiHeadersProvider apiHeaders,
+ bool useInstanceRequestHeader)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
+ apiHeaders,
logger,
true)
{
diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
index b71703af41..37b7e89ed1 100644
--- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
@@ -12,10 +12,12 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -34,21 +36,24 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
+ /// The for the .
public ConfigurationController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
- IIOManager ioManager)
+ IIOManager ioManager,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
}
@@ -98,10 +103,6 @@ namespace Tgstation.Server.Host.Controllers
AdditionalData = e.Message,
});
}
- catch (NotImplementedException ex)
- {
- return RequiresPosixSystemIdentity(ex);
- }
}
///
@@ -145,10 +146,6 @@ namespace Tgstation.Server.Host.Controllers
AdditionalData = e.Message,
});
}
- catch (NotImplementedException ex)
- {
- return RequiresPosixSystemIdentity(ex);
- }
}
///
@@ -191,11 +188,6 @@ namespace Tgstation.Server.Host.Controllers
return new PaginatableResult(result);
}
- catch (NotImplementedException ex)
- {
- return new PaginatableResult(
- RequiresPosixSystemIdentity(ex));
- }
catch (UnauthorizedAccessException)
{
return new PaginatableResult(
@@ -282,10 +274,6 @@ namespace Tgstation.Server.Host.Controllers
Message = e.Message,
});
}
- catch (NotImplementedException ex)
- {
- return RequiresPosixSystemIdentity(ex);
- }
catch (UnauthorizedAccessException)
{
return Forbid();
@@ -330,10 +318,6 @@ namespace Tgstation.Server.Host.Controllers
: Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationDirectoryNotEmpty));
});
}
- catch (NotImplementedException ex)
- {
- return RequiresPosixSystemIdentity(ex);
- }
catch (UnauthorizedAccessException)
{
return Forbid();
diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs
index c1f35f87e5..08d8f74dad 100644
--- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs
@@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Controllers
///
/// The value of the route.
/// The to use.
- [Route("{**appRoute}")]
+ [Route("/{**appRoute}")]
[HttpGet]
public IActionResult Get([FromRoute] string appRoute)
{
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index ba6e1d2caf..ea39745845 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -47,23 +47,26 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
/// The value of .
+ /// The for the .
public DreamDaemonController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
IJobManager jobManager,
- IPortAllocator portAllocator)
+ IPortAllocator portAllocator,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
@@ -84,14 +87,7 @@ namespace Tgstation.Server.Host.Controllers
if (instance.Watchdog.Status != WatchdogStatus.Offline)
return Conflict(new ErrorMessageResponse(ErrorCode.WatchdogRunning));
- var job = new Job
- {
- Description = "Launch Watchdog",
- CancelRight = (ulong)DreamDaemonRights.Shutdown,
- CancelRightsType = RightsType.DreamDaemon,
- Instance = Instance,
- StartedBy = AuthenticationContext.User,
- };
+ var job = Job.Create(JobCode.WatchdogLaunch, AuthenticationContext.User, Instance, DreamDaemonRights.Shutdown);
await jobManager.RegisterOperation(
job,
(core, databaseContextFactory, paramJob, progressHandler, innerCt) => core.Watchdog.Launch(innerCt),
@@ -267,14 +263,7 @@ namespace Tgstation.Server.Host.Controllers
public ValueTask Restart(CancellationToken cancellationToken)
=> WithComponentInstance(async instance =>
{
- var job = new Job
- {
- Instance = Instance,
- CancelRightsType = RightsType.DreamDaemon,
- CancelRight = (ulong)DreamDaemonRights.Shutdown,
- StartedBy = AuthenticationContext.User,
- Description = "Restart Watchdog",
- };
+ var job = Job.Create(JobCode.WatchdogRestart, AuthenticationContext.User, Instance, DreamDaemonRights.Shutdown);
var watchdog = instance.Watchdog;
@@ -300,14 +289,7 @@ namespace Tgstation.Server.Host.Controllers
public ValueTask CreateDump(CancellationToken cancellationToken)
=> WithComponentInstance(async instance =>
{
- var job = new Job
- {
- Instance = Instance,
- CancelRightsType = RightsType.DreamDaemon,
- CancelRight = (ulong)DreamDaemonRights.CreateDump,
- StartedBy = AuthenticationContext.User,
- Description = "Create DreamDaemon Process Dump",
- };
+ var job = Job.Create(JobCode.WatchdogDump, AuthenticationContext.User, Instance, DreamDaemonRights.CreateDump);
var watchdog = instance.Watchdog;
diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
index 3c4431a3e1..714713763f 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
@@ -13,6 +13,7 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
@@ -43,23 +44,26 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
/// The value of .
+ /// The for the .
public DreamMakerController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
IJobManager jobManager,
- IPortAllocator portAllocator)
+ IPortAllocator portAllocator,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
@@ -139,14 +143,7 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(JobResponse), 202)]
public async ValueTask Create(CancellationToken cancellationToken)
{
- var job = new Job
- {
- Description = "Compile active repository code",
- StartedBy = AuthenticationContext.User,
- CancelRightsType = RightsType.DreamMaker,
- CancelRight = (ulong)DreamMakerRights.CancelCompile,
- Instance = Instance,
- };
+ var job = Job.Create(JobCode.Deployment, AuthenticationContext.User, Instance, DreamMakerRights.CancelCompile);
await jobManager.RegisterOperation(
job,
@@ -257,6 +254,8 @@ namespace Tgstation.Server.Host.Controllers
.AsQueryable()
.Include(x => x.Job)
.ThenInclude(x => x.StartedBy)
+ .Include(x => x.Job)
+ .ThenInclude(x => x.Instance)
.Include(x => x.RevisionInformation)
.ThenInclude(x => x.PrimaryTestMerge)
.ThenInclude(x => x.MergedBy)
diff --git a/src/Tgstation.Server.Host/Controllers/EngineController.cs b/src/Tgstation.Server.Host/Controllers/EngineController.cs
index 2dcf3b6497..75c7ef289b 100644
--- a/src/Tgstation.Server.Host/Controllers/EngineController.cs
+++ b/src/Tgstation.Server.Host/Controllers/EngineController.cs
@@ -14,12 +14,14 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Transfer;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -50,23 +52,26 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
/// The value of .
+ /// The for the .
public EngineController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
IJobManager jobManager,
- IFileTransferTicketProvider fileTransferService)
+ IFileTransferTicketProvider fileTransferService,
+ IApiHeadersProvider apiHeadersProvider)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeadersProvider)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
@@ -203,14 +208,14 @@ namespace Tgstation.Server.Host.Controllers
Instance.Id);
// run the install through the job manager
- var job = new Models.Job
- {
- Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}{model.EngineVersion.Engine.Value} version {model.EngineVersion.Version}",
- StartedBy = AuthenticationContext.User,
- CancelRightsType = RightsType.Engine,
- CancelRight = (ulong)EngineRights.CancelInstall,
- Instance = Instance,
- };
+ var job = Models.Job.Create(
+ uploadingZip
+ ? JobCode.EngineCustomInstall
+ : JobCode.EngineOfficialInstall,
+ AuthenticationContext.User,
+ Instance,
+ EngineRights.CancelInstall);
+ job.Description += $" {model.EngineVersion}";
IFileUploadTicket fileUploadTicket = null;
if (uploadingZip)
@@ -309,19 +314,16 @@ namespace Tgstation.Server.Host.Controllers
var isByondVersion = model.EngineVersion.Engine.Value == EngineType.Byond;
// run the install through the job manager
- var job = new Models.Job
- {
- Description = $"Delete installed engine version {model.EngineVersion}",
- StartedBy = AuthenticationContext.User,
- CancelRightsType = RightsType.Engine,
- CancelRight = (ulong)(
- isByondVersion
- ? model.EngineVersion.Version.Build != -1
- ? EngineRights.InstallOfficialOrChangeActiveByondVersion
- : EngineRights.InstallCustomByondVersion
- : EngineRights.InstallCustomOpenDreamVersion | EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion),
- Instance = Instance,
- };
+ var cancelRight = isByondVersion
+ ? model.EngineVersion.CustomIteration.HasValue
+ ? EngineRights.InstallCustomByondVersion
+ : EngineRights.InstallOfficialOrChangeActiveByondVersion
+ : model.EngineVersion.CustomIteration.HasValue
+ ? EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion
+ : EngineRights.InstallCustomOpenDreamVersion;
+
+ var job = Models.Job.Create(JobCode.EngineDelete, AuthenticationContext.User, Instance, cancelRight);
+ job.Description += $" {model.EngineVersion}";
await jobManager.RegisterOperation(
job,
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index 8aa6d3d03b..659a379591 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -1,17 +1,16 @@
using System;
using System.Linq;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
+
using Octokit;
using Tgstation.Server.Api;
@@ -21,12 +20,12 @@ using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
-using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.Swarm;
using Tgstation.Server.Host.System;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -95,7 +94,7 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The value of .
/// The value of .
/// The value of .
@@ -108,9 +107,10 @@ namespace Tgstation.Server.Host.Controllers
/// The containing the value of .
/// The containing the value of .
/// The for the .
+ /// The for the .
public HomeController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ITokenFactory tokenFactory,
ISystemIdentityFactory systemIdentityFactory,
ICryptographySuite cryptographySuite,
@@ -122,10 +122,12 @@ namespace Tgstation.Server.Host.Controllers
IServerControl serverControl,
IOptions generalConfigurationOptions,
IOptions controlPanelConfigurationOptions,
- ILogger logger)
+ ILogger logger,
+ IApiHeadersProvider apiHeadersProvider)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
+ apiHeadersProvider,
logger,
false)
{
@@ -160,6 +162,8 @@ namespace Tgstation.Server.Host.Controllers
HeaderNames.Vary,
new StringValues(ApiHeaders.ApiVersionHeader));
+ // if they tried to authenticate in any form and failed, let them know immediately
+ bool failIfUnauthed;
if (ApiHeaders == null)
{
if (controlPanelConfiguration.Enable && !Request.Headers.TryGetValue(ApiHeaders.ApiVersionHeader, out _))
@@ -176,17 +180,20 @@ namespace Tgstation.Server.Host.Controllers
try
{
// we only allow authorization header issues
- var headers = new ApiHeaders(Request.GetTypedHeaders(), true);
- if (!headers.Compatible())
- return this.StatusCode(
- HttpStatusCode.UpgradeRequired,
- new ErrorMessageResponse(ErrorCode.ApiMismatch));
+ ApiHeadersProvider.CreateAuthlessHeaders();
}
- catch (HeadersException)
+ catch (HeadersException ex)
{
- return HeadersIssue(true);
+ return HeadersIssue(ex);
}
+
+ failIfUnauthed = Request.Headers.Authorization.Any();
}
+ else
+ failIfUnauthed = ApiHeaders.Token != null;
+
+ if (failIfUnauthed && !AuthenticationContext.Valid)
+ return Unauthorized();
return Json(new ServerInformationResponse
{
@@ -223,8 +230,8 @@ namespace Tgstation.Server.Host.Controllers
{
if (ApiHeaders == null)
{
- Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS bearer token\""));
- return HeadersIssue(false);
+ Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues($"basic realm=\"Create TGS {ApiHeaders.BearerAuthenticationScheme} token\""));
+ return HeadersIssue(ApiHeadersProvider.HeadersException);
}
if (ApiHeaders.IsTokenAuthentication)
@@ -239,9 +246,9 @@ namespace Tgstation.Server.Host.Controllers
// 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);
}
- catch (NotImplementedException ex)
+ catch (NotImplementedException)
{
- RequiresPosixSystemIdentity(ex);
+ // Intentionally suppressed
}
using (systemIdentity)
@@ -261,7 +268,7 @@ namespace Tgstation.Server.Host.Controllers
return BadRequest(new ErrorMessageResponse(ErrorCode.OAuthProviderDisabled));
externalUserId = await validator
- .ValidateResponseCode(ApiHeaders.Token, cancellationToken);
+ .ValidateResponseCode(ApiHeaders.OAuthCode!, cancellationToken);
Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId);
}
@@ -369,11 +376,11 @@ namespace Tgstation.Server.Host.Controllers
return Forbid();
}
- var token = await tokenFactory.CreateToken(user, oAuthLogin, cancellationToken);
+ var token = tokenFactory.CreateToken(user, oAuthLogin);
if (usingSystemIdentity)
{
// expire the identity slightly after the auth token in case of lag
- var identExpiry = token.ExpiresAt;
+ var identExpiry = token.ParseJwt().ValidTo;
identExpiry += tokenFactory.ValidationParameters.ClockSkew;
identExpiry += TimeSpan.FromSeconds(15);
identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry);
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index 494b27db2b..d63396daf9 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -19,6 +19,7 @@ using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
@@ -67,6 +68,11 @@ namespace Tgstation.Server.Host.Controllers
///
readonly IPortAllocator portAllocator;
+ ///
+ /// The for the .
+ ///
+ readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
+
///
/// The for the .
///
@@ -81,36 +87,44 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
/// The value of .
/// The value of .
- /// The value of .
+ /// The value of .
+ /// The value of .
/// The containing the value of .
/// The containing the value of .
+ /// The for the .
public InstanceController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
IJobManager jobManager,
IIOManager ioManager,
IPortAllocator portAllocator,
IPlatformIdentifier platformIdentifier,
+ IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
IOptions generalConfigurationOptions,
- IOptions swarmConfigurationOptions)
+ IOptions swarmConfigurationOptions,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders,
+ false)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
+ this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
+
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
}
@@ -262,6 +276,10 @@ namespace Tgstation.Server.Host.Controllers
newInstance.Id,
newInstance.Path);
+ await permissionsUpdateNotifyee.InstancePermissionSetCreated(
+ newInstance.InstancePermissionSets.First(),
+ cancellationToken);
+
var api = newInstance.ToApi();
api.Accessible = true; // instances are always accessible by their creator
return attached ? Json(api) : Created(api);
@@ -337,10 +355,7 @@ namespace Tgstation.Server.Host.Controllers
var moveJob = await InstanceQuery()
.SelectMany(x => x.Jobs).
Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
- .Select(x => new Job
- {
- Id = x.Id,
- }).FirstOrDefaultAsync(cancellationToken);
+ .Select(x => new Job(x.Id.Value)).FirstOrDefaultAsync(cancellationToken);
if (moveJob != default)
{
@@ -472,14 +487,9 @@ namespace Tgstation.Server.Host.Controllers
var moving = originalModelPath != null;
if (moving)
{
- var job = new Job
- {
- Description = $"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}",
- Instance = originalModel,
- CancelRightsType = RightsType.InstanceManager,
- CancelRight = (ulong)InstanceManagerRights.Relocate,
- StartedBy = AuthenticationContext.User,
- };
+ var description = $"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}";
+ var job = Job.Create(JobCode.Move, AuthenticationContext.User, originalModel, InstanceManagerRights.Relocate);
+ job.Description = description;
await jobManager.RegisterOperation(
job,
@@ -622,7 +632,9 @@ namespace Tgstation.Server.Host.Controllers
var moveJob = await QueryForUser()
.SelectMany(x => x.Jobs)
.Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
- .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
+ .Include(x => x.StartedBy)
+ .ThenInclude(x => x.CreatedBy)
+ .Include(x => x.Instance)
.FirstOrDefaultAsync(cancellationToken);
api.MoveJob = moveJob?.ToApi();
await CheckAccessible(api, cancellationToken);
@@ -763,6 +775,7 @@ namespace Tgstation.Server.Host.Controllers
{
permissionSetToModify ??= new InstancePermissionSet()
{
+ PermissionSet = AuthenticationContext.PermissionSet,
PermissionSetId = AuthenticationContext.PermissionSet.Id.Value,
};
permissionSetToModify.EngineRights = RightsHelper.AllRights();
diff --git a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs
index f25bcea52d..13defd8048 100644
--- a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs
@@ -13,10 +13,12 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
using Z.EntityFramework.Plus;
@@ -28,24 +30,35 @@ namespace Tgstation.Server.Host.Controllers
[Route(Routes.InstancePermissionSet)]
public sealed class InstancePermissionSetController : InstanceRequiredController
{
+ ///
+ /// The for the .
+ ///
+ readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
+
///
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
+ /// The value of .
+ /// The for the .
public InstancePermissionSetController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
- IInstanceManager instanceManager)
+ IInstanceManager instanceManager,
+ IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
+ this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
}
///
@@ -71,6 +84,7 @@ namespace Tgstation.Server.Host.Controllers
.Where(x => x.Id == model.PermissionSetId)
.Select(x => new Models.PermissionSet
{
+ Id = x.Id,
UserId = x.UserId,
})
.FirstOrDefaultAsync(cancellationToken);
@@ -93,9 +107,7 @@ namespace Tgstation.Server.Host.Controllers
var dbUser = new InstancePermissionSet
{
-#pragma warning disable CS0618 // Type or member is obsolete
- EngineRights = RightsHelper.Clamp(model.EngineRights ?? model.ByondRights ?? EngineRights.None),
-#pragma warning restore CS0618 // Type or member is obsolete
+ EngineRights = RightsHelper.Clamp(model.EngineRights ?? EngineRights.None),
ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None),
ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? ConfigurationRights.None),
DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None),
@@ -109,6 +121,10 @@ namespace Tgstation.Server.Host.Controllers
DatabaseContext.InstancePermissionSets.Add(dbUser);
await DatabaseContext.Save(cancellationToken);
+
+ // needs to be set for next call
+ dbUser.PermissionSet = existingPermissionSet;
+ await permissionsUpdateNotifyee.InstancePermissionSetCreated(dbUser, cancellationToken);
return Created(dbUser.ToApi());
}
#pragma warning restore CA1506
@@ -140,9 +156,7 @@ namespace Tgstation.Server.Host.Controllers
if (originalPermissionSet == null)
return this.Gone();
-#pragma warning disable CS0618 // Type or member is obsolete
- originalPermissionSet.EngineRights = RightsHelper.Clamp(model.EngineRights ?? model.ByondRights ?? originalPermissionSet.EngineRights.Value);
-#pragma warning restore CS0618 // Type or member is obsolete
+ originalPermissionSet.EngineRights = RightsHelper.Clamp(model.EngineRights ?? originalPermissionSet.EngineRights.Value);
originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights.Value);
originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights.Value);
originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights.Value);
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs
index 1f119500f5..cdf3f0e139 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs
@@ -3,6 +3,7 @@
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -15,19 +16,22 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
+ /// The for the .
protected InstanceRequiredController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
- IInstanceManager instanceManager)
+ IInstanceManager instanceManager,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
instanceManager,
+ apiHeaders,
true)
{
}
diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs
index 84f4cc5084..521a560a0b 100644
--- a/src/Tgstation.Server.Host/Controllers/JobController.cs
+++ b/src/Tgstation.Server.Host/Controllers/JobController.cs
@@ -11,11 +11,13 @@ using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -34,21 +36,24 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
+ /// The for the .
public JobController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
- IJobManager jobManager)
+ IJobManager jobManager,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
@@ -73,6 +78,7 @@ namespace Tgstation.Server.Host.Controllers
.AsQueryable()
.Include(x => x.StartedBy)
.Include(x => x.CancelledBy)
+ .Include(x => x.Instance)
.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue)
.OrderByDescending(x => x.StartedAt))),
AddJobProgressResponseTransformer,
@@ -100,6 +106,7 @@ namespace Tgstation.Server.Host.Controllers
.AsQueryable()
.Include(x => x.StartedBy)
.Include(x => x.CancelledBy)
+ .Include(x => x.Instance)
.Where(x => x.Instance.Id == Instance.Id)
.OrderByDescending(x => x.StartedAt))),
AddJobProgressResponseTransformer,
@@ -127,6 +134,7 @@ namespace Tgstation.Server.Host.Controllers
.Jobs
.AsQueryable()
.Include(x => x.StartedBy)
+ .Include(x => x.Instance)
.Where(x => x.Id == id && x.Instance.Id == Instance.Id)
.FirstOrDefaultAsync(cancellationToken);
if (job == default)
@@ -162,6 +170,7 @@ namespace Tgstation.Server.Host.Controllers
.Where(x => x.Id == id && x.Instance.Id == Instance.Id)
.Include(x => x.StartedBy)
.Include(x => x.CancelledBy)
+ .Include(x => x.Instance)
.FirstOrDefaultAsync(cancellationToken);
if (job == default)
return NotFound();
diff --git a/src/Tgstation.Server.Host/Controllers/README.md b/src/Tgstation.Server.Host/Controllers/README.md
index fa7f1c5a1f..e1214f91f2 100644
--- a/src/Tgstation.Server.Host/Controllers/README.md
+++ b/src/Tgstation.Server.Host/Controllers/README.md
@@ -12,4 +12,3 @@ Some notable exceptions:
- Returns 401 If an `IAuthenticationContext` could not be created for a request.
- [BridgeController](./BridgeController.cs) is a special controller accessible only from localhost and is used to receive bridge request from DreamDaemon
- [HomeController](./HomeController.cs) contains the code to initially log in and generate an API token for a user.
-- [TgsAuthorizeAttribute](./TgsAuthorizeAttribute.cs) is a special attribute applied to controller methods to define which rights are required to run a verb.
diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
index 0b02d7d0c1..37766ce0fd 100644
--- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
@@ -22,6 +22,7 @@ using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -46,23 +47,26 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The for the .
/// The for the .
/// The value of .
/// The value of .
+ /// The for the .
public RepositoryController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ILogger logger,
IInstanceManager instanceManager,
ILoggerFactory loggerFactory,
- IJobManager jobManager)
+ IJobManager jobManager,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
logger,
- instanceManager)
+ instanceManager,
+ apiHeaders)
{
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
@@ -135,20 +139,15 @@ namespace Tgstation.Server.Host.Controllers
if (repo != null)
return Conflict(new ErrorMessageResponse(ErrorCode.RepoExists));
- var job = new Job
- {
- Description = String.Format(
+ var description = String.Format(
CultureInfo.InvariantCulture,
"Clone{1} repository {0}",
origin,
cloneBranch != null
? $"\"{cloneBranch}\" branch of"
- : String.Empty),
- StartedBy = AuthenticationContext.User,
- CancelRightsType = RightsType.Repository,
- CancelRight = (ulong)RepositoryRights.CancelClone,
- Instance = Instance,
- };
+ : String.Empty);
+ var job = Job.Create(JobCode.RepositoryClone, AuthenticationContext.User, Instance, RepositoryRights.CancelClone);
+ job.Description = description;
var api = currentModel.ToApi();
await DatabaseContext.Save(cancellationToken);
@@ -218,12 +217,7 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogInformation("Instance {instanceId} repository delete initiated by user {userId}", Instance.Id, AuthenticationContext.User.Id.Value);
- var job = new Job
- {
- Description = "Delete repository",
- StartedBy = AuthenticationContext.User,
- Instance = Instance,
- };
+ var job = Job.Create(JobCode.RepositoryDelete, AuthenticationContext.User, Instance);
var api = currentModel.ToApi();
await jobManager.RegisterOperation(
job,
@@ -450,14 +444,8 @@ namespace Tgstation.Server.Host.Controllers
if (description == null)
return Json(api); // no git changes
- var job = new Job
- {
- Description = description,
- StartedBy = AuthenticationContext.User,
- Instance = Instance,
- CancelRightsType = RightsType.Repository,
- CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
- };
+ var job = Job.Create(JobCode.RepositoryUpdate, AuthenticationContext.User, Instance, RepositoryRights.CancelPendingChanges);
+ job.Description = description;
var repositoryUpdater = new RepositoryUpdateService(
model,
diff --git a/src/Tgstation.Server.Host/Controllers/LimitedStreamResult.cs b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResult.cs
similarity index 96%
rename from src/Tgstation.Server.Host/Controllers/LimitedStreamResult.cs
rename to src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResult.cs
index 734a1731a6..998f5779c5 100644
--- a/src/Tgstation.Server.Host/Controllers/LimitedStreamResult.cs
+++ b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResult.cs
@@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
using Tgstation.Server.Host.IO;
-namespace Tgstation.Server.Host.Controllers
+namespace Tgstation.Server.Host.Controllers.Results
{
///
/// Very similar to except it's contains a fix for https://github.com/dotnet/aspnetcore/issues/28189.
diff --git a/src/Tgstation.Server.Host/Controllers/LimitedStreamResultExecutor.cs b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs
similarity index 97%
rename from src/Tgstation.Server.Host/Controllers/LimitedStreamResultExecutor.cs
rename to src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs
index 7a8169c1d3..3178db355b 100644
--- a/src/Tgstation.Server.Host/Controllers/LimitedStreamResultExecutor.cs
+++ b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
-namespace Tgstation.Server.Host.Controllers
+namespace Tgstation.Server.Host.Controllers.Results
{
///
/// for s.
diff --git a/src/Tgstation.Server.Host/Controllers/PaginatableResult.cs b/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs
similarity index 96%
rename from src/Tgstation.Server.Host/Controllers/PaginatableResult.cs
rename to src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs
index ae19ba913e..b936d0f3e6 100644
--- a/src/Tgstation.Server.Host/Controllers/PaginatableResult.cs
+++ b/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs
@@ -3,7 +3,7 @@ using System.Linq;
using Microsoft.AspNetCore.Mvc;
-namespace Tgstation.Server.Host.Controllers
+namespace Tgstation.Server.Host.Controllers.Results
{
///
/// Helper for returning paginated models.
diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs
index 4829854ac5..2512eda8dd 100644
--- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs
+++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs
@@ -5,8 +5,8 @@ using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -25,10 +25,9 @@ namespace Tgstation.Server.Host.Controllers
/// For swarm server communication.
///
[Route(SwarmConstants.ControllerRoute)]
- [Produces(MediaTypeNames.Application.Json)]
- [ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
- public sealed class SwarmController : Controller
+ [AllowAnonymous] // We have custom private key auth
+ public sealed class SwarmController : ApiControllerBase
{
///
/// Get the current registration from the .
@@ -211,7 +210,7 @@ namespace Tgstation.Server.Host.Controllers
}
///
- public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken)
{
using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}"))
{
@@ -219,8 +218,7 @@ namespace Tgstation.Server.Host.Controllers
if (swarmConfiguration.PrivateKey == null)
{
logger.LogDebug("Attempted swarm request without private key configured!");
- await Forbid().ExecuteResultAsync(context);
- return;
+ return Forbid();
}
if (!(Request.Headers.TryGetValue(SwarmConstants.ApiKeyHeader, out var apiKeyHeaderValues)
@@ -228,8 +226,7 @@ namespace Tgstation.Server.Host.Controllers
&& apiKeyHeaderValues.First() == swarmConfiguration.PrivateKey))
{
logger.LogDebug("Unauthorized swarm request!");
- await Unauthorized().ExecuteResultAsync(context);
- return;
+ return Unauthorized();
}
if (!(Request.Headers.TryGetValue(SwarmConstants.RegistrationIdHeader, out var registrationHeaderValues)
@@ -237,8 +234,7 @@ namespace Tgstation.Server.Host.Controllers
&& Guid.TryParse(registrationHeaderValues.First(), out var registrationId)))
{
logger.LogDebug("Swarm request without registration ID!");
- await BadRequest().ExecuteResultAsync(context);
- return;
+ return BadRequest();
}
// we validate the registration itself on a case-by-case basis
@@ -252,12 +248,12 @@ namespace Tgstation.Server.Host.Controllers
"Swarm request model validation failed!{newLine}{messages}",
Environment.NewLine,
String.Join(Environment.NewLine, errorMessages));
- await BadRequest().ExecuteResultAsync(context);
- return;
+ return BadRequest();
}
logger.LogTrace("Starting swarm request processing...");
- await base.OnActionExecutionAsync(context, next);
+ await executeAction();
+ return null;
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/TransferController.cs b/src/Tgstation.Server.Host/Controllers/TransferController.cs
index be12cfda91..8a0a2ebcd3 100644
--- a/src/Tgstation.Server.Host/Controllers/TransferController.cs
+++ b/src/Tgstation.Server.Host/Controllers/TransferController.cs
@@ -9,10 +9,12 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Transfer;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -32,17 +34,20 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The value of .
/// The for the .
+ /// The for the .
public TransferController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
IFileTransferStreamHandler fileTransferService,
- ILogger logger)
+ ILogger logger,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
+ apiHeaders,
logger,
true)
{
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index 04559bde3c..08c61e05fe 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -15,10 +15,12 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Controllers
{
@@ -38,6 +40,11 @@ namespace Tgstation.Server.Host.Controllers
///
readonly ICryptographySuite cryptographySuite;
+ ///
+ /// The for the .
+ ///
+ readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
+
///
/// The for the .
///
@@ -47,26 +54,32 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The value of .
/// The value of .
+ /// The value of .
/// The for the .
/// The containing the value of .
+ /// The for the .
public UserController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
ISystemIdentityFactory systemIdentityFactory,
ICryptographySuite cryptographySuite,
+ IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
ILogger logger,
- IOptions generalConfigurationOptions)
+ IOptions generalConfigurationOptions,
+ IApiHeadersProvider apiHeaders)
: base(
databaseContext,
- authenticationContextFactory,
+ authenticationContext,
+ apiHeaders,
logger,
true)
{
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
+ this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
@@ -238,13 +251,17 @@ namespace Tgstation.Server.Host.Controllers
if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
return BadRequest(new ErrorMessageResponse(ErrorCode.UserNameChange));
+ bool userWasDisabled;
if (model.Enabled.HasValue)
{
- if (originalUser.Enabled.Value && !model.Enabled.Value)
+ userWasDisabled = originalUser.Enabled.Value && !model.Enabled.Value;
+ if (userWasDisabled)
originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow;
originalUser.Enabled = model.Enabled.Value;
}
+ else
+ userWasDisabled = false;
if (model.OAuthConnections != null
&& (model.OAuthConnections.Count != originalUser.OAuthConnections.Count
@@ -280,7 +297,7 @@ namespace Tgstation.Server.Host.Controllers
DatabaseContext.Groups.Attach(originalUser.Group);
if (originalUser.PermissionSet != null)
{
- Logger.LogInformation("Deleting permission set {0}...", originalUser.PermissionSet.Id);
+ Logger.LogInformation("Deleting permission set {permissionSetId}...", originalUser.PermissionSet.Id);
DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet);
originalUser.PermissionSet = null;
}
@@ -308,7 +325,10 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken);
- Logger.LogInformation("Updated user {0} ({1})", originalUser.Name, originalUser.Id);
+ Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id);
+
+ if (userWasDisabled)
+ await permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken);
// return id only if not a self update and cannot read users
var canReadBack = AuthenticationContext.User.Id == originalUser.Id
diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs
index 14269e3254..3f2c5ea3a2 100644
--- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs
@@ -14,10 +14,12 @@ using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
using Z.EntityFramework.Plus;
@@ -38,19 +40,22 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The for the .
+ /// The for the .
/// The containing the value of .
/// The for the .
+ /// The for the .
public UserGroupController(
IDatabaseContext databaseContext,
- IAuthenticationContextFactory authenticationContextFactory,
+ IAuthenticationContext authenticationContext,
IOptions generalConfigurationOptions,
- ILogger logger)
+ ILogger logger,
+ IApiHeadersProvider apiHeaders)
: base(
- databaseContext,
- authenticationContextFactory,
- logger,
- true)
+ databaseContext,
+ authenticationContext,
+ apiHeaders,
+ logger,
+ true)
{
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
@@ -93,7 +98,7 @@ namespace Tgstation.Server.Host.Controllers
DatabaseContext.Groups.Add(dbGroup);
await DatabaseContext.Save(cancellationToken);
- Logger.LogInformation("Created new user group {0} ({1})", dbGroup.Name, dbGroup.Id);
+ Logger.LogInformation("Created new user group {groupName} ({groupId})", dbGroup.Name, dbGroup.Id);
return Created(dbGroup.ToApi(true));
}
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 573321d7ac..795ffe54ce 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -2,17 +2,23 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Threading.Tasks;
using Cyberboss.AspNetCore.AsyncInitializer;
using Elastic.CommonSchema.Serilog;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -27,6 +33,7 @@ using Serilog.Formatting.Display;
using Serilog.Sinks.Elasticsearch;
using Tgstation.Server.Api;
+using Tgstation.Server.Api.Hubs;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components;
@@ -41,6 +48,7 @@ using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Controllers;
+using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Extensions.Converters;
@@ -115,7 +123,7 @@ namespace Tgstation.Server.Host.Core
}
///
- /// Configure the 's services.
+ /// Configure the 's .
///
/// The to configure.
/// The needed for configuration.
@@ -217,50 +225,48 @@ namespace Tgstation.Server.Host.Core
postSetupServices.InternalConfiguration,
postSetupServices.FileLoggingConfiguration);
- // 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.MapInboundClaims = false;
- 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),
- };
- });
+ // configure authentication pipeline
+ ConfigureAuthenticationPipeline(services);
// add mvc, configure the json serializer settings
+ var jsonVersionConverterList = new List
+ {
+ new VersionConverter(),
+ };
+
+ void ConfigureNewtonsoftJsonSerializerSettingsForApi(JsonSerializerSettings settings)
+ {
+ settings.NullValueHandling = NullValueHandling.Ignore;
+ settings.CheckAdditionalContent = true;
+ settings.MissingMemberHandling = MissingMemberHandling.Error;
+ settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
+ settings.Converters = jsonVersionConverterList;
+ }
+
services
.AddMvc(options =>
{
- options.EnableEndpointRouting = false;
options.ReturnHttpNotAcceptable = true;
options.RespectBrowserAcceptHeader = true;
})
.AddNewtonsoftJson(options =>
{
options.AllowInputFormatterExceptionMessages = true;
- options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
- options.SerializerSettings.CheckAdditionalContent = true;
- options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
- options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
- options.SerializerSettings.Converters = new List
- {
- new VersionConverter(),
- };
+ ConfigureNewtonsoftJsonSerializerSettingsForApi(options.SerializerSettings);
});
+ services.AddSignalR(
+ options =>
+ {
+ options.AddFilter();
+ })
+ .AddNewtonsoftJsonProtocol(options =>
+ {
+ ConfigureNewtonsoftJsonSerializerSettingsForApi(options.PayloadSerializerSettings);
+ });
+
+ services.AddHub();
+
if (postSetupServices.GeneralConfiguration.HostApiDocumentation)
{
string GetDocumentationFilePath(string assemblyLocation) => ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
@@ -319,9 +325,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton();
services.AddSingleton();
- // configure security services
- services.AddScoped();
- services.AddScoped();
+ // configure other security services
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -422,8 +426,13 @@ namespace Tgstation.Server.Host.Core
services.AddGitHub();
// configure root services
- services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetRequiredService());
+ services.AddSingleton(provider => provider.GetRequiredService());
services.AddSingleton(x => x.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetRequiredService());
+ services.AddSingleton(x => x.GetRequiredService()); // bit of a hack, but we need this to load immediated
services.AddSingleton();
services.AddSingleton(x => x.GetRequiredService());
@@ -494,30 +503,6 @@ namespace Tgstation.Server.Host.Core
logger.LogTrace("Swagger API generation enabled");
}
- // Set up CORS based on configuration if necessary
- Action corsBuilder = null;
- if (controlPanelConfiguration.AllowAnyOrigin)
- {
- logger.LogTrace("Access-Control-Allow-Origin: *");
- corsBuilder = builder => builder.AllowAnyOrigin();
- }
- else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
- {
- logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
- corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
- }
-
- var originalBuilder = corsBuilder;
- corsBuilder = builder =>
- {
- builder
- .AllowAnyHeader()
- .AllowAnyMethod()
- .SetPreflightMaxAge(TimeSpan.FromDays(1));
- originalBuilder?.Invoke(builder);
- };
- applicationBuilder.UseCors(corsBuilder);
-
// spa loading if necessary
if (controlPanelConfiguration.Enable)
{
@@ -531,22 +516,68 @@ namespace Tgstation.Server.Host.Core
}
else
#if NO_WEBPANEL
- logger.LogTrace("Web control panel was not included in TGS build!");
+ logger.LogDebug("Web control panel was not included in TGS build!");
#else
logger.LogTrace("Web control panel disabled!");
#endif
- // Do not cache a single thing beyond this point, it's all API
- applicationBuilder.UseDisabledClientCache();
+ // Enable endpoint routing
+ applicationBuilder.UseRouting();
+
+ // Set up CORS based on configuration if necessary
+ Action corsBuilder = null;
+ if (controlPanelConfiguration.AllowAnyOrigin)
+ {
+ logger.LogTrace("Access-Control-Allow-Origin: *");
+ corsBuilder = builder => builder.SetIsOriginAllowed(_ => true);
+ }
+ else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
+ {
+ logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins));
+ corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
+ }
+
+ var originalBuilder = corsBuilder;
+ corsBuilder = builder =>
+ {
+ builder
+ .AllowAnyHeader()
+ .AllowAnyMethod()
+ .AllowCredentials()
+ .SetPreflightMaxAge(TimeSpan.FromDays(1));
+ originalBuilder?.Invoke(builder);
+ };
+ applicationBuilder.UseCors(corsBuilder);
+
+ // validate the API version
+ applicationBuilder.UseApiCompatibility();
// authenticate JWT tokens using our security pipeline if present, returns 401 if bad
applicationBuilder.UseAuthentication();
+ // enable authorization on endpoints
+ applicationBuilder.UseAuthorization();
+
// suppress and log database exceptions
applicationBuilder.UseDbConflictHandling();
- // majority of handling is done in the controllers
- applicationBuilder.UseMvc();
+ // setup endpoints
+ applicationBuilder.UseEndpoints(endpoints =>
+ {
+ // access to the signalR jobs hub
+ endpoints.MapHub(
+ Routes.JobsHub,
+ options =>
+ {
+ options.Transports = HttpTransportType.ServerSentEvents;
+ options.CloseOnAuthenticationExpiration = true;
+ })
+ .RequireAuthorization()
+ .RequireCors(corsBuilder);
+
+ // majority of handling is done in the controllers
+ endpoints.MapControllers();
+ });
// 404 anything that gets this far
// End of request pipeline setup
@@ -561,5 +592,60 @@ namespace Tgstation.Server.Host.Core
///
protected override void ConfigureHostedService(IServiceCollection services)
=> services.AddSingleton(x => x.GetRequiredService());
+
+ ///
+ /// Configure the for the authentication pipeline.
+ ///
+ /// The to configure.
+ void ConfigureAuthenticationPipeline(IServiceCollection services)
+ {
+ services.AddHttpContextAccessor();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped(provider => provider.GetRequiredService());
+
+ // what if you
+ // wanted to just do this:
+ // return provider.GetRequiredService().CurrentAuthenticationContext
+ // But M$ said
+ // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
+ services.AddScoped(provider => provider
+ .GetRequiredService()
+ .HttpContext
+ .RequestServices
+ .GetRequiredService()
+ .CurrentAuthenticationContext);
+ services.AddScoped();
+ services.AddScoped();
+
+ 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 ?? throw new InvalidOperationException("tokenFactory not initialized!");
+ jwtBearerOptions.MapInboundClaims = false;
+ jwtBearerOptions.Events = new JwtBearerEvents
+ {
+ OnMessageReceived = context =>
+ {
+ if (String.IsNullOrWhiteSpace(context.Token))
+ {
+ var accessToken = context.Request.Query["access_token"];
+ var path = context.HttpContext.Request.Path;
+
+ if (!String.IsNullOrWhiteSpace(accessToken) &&
+ path.StartsWithSegments(Routes.HubsRoot, StringComparison.OrdinalIgnoreCase))
+ {
+ context.Token = accessToken;
+ }
+ }
+
+ return Task.CompletedTask;
+ },
+ };
+ });
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
index 938f34af7e..cca6313b53 100644
--- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
@@ -423,6 +423,15 @@ namespace Tgstation.Server.Host.Database
string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
if (targetVersion < new Version(6, 0, 0))
+ targetMigration = currentDatabaseType switch
+ {
+ DatabaseType.MySql => nameof(MYAddJobCodes),
+ DatabaseType.PostgresSql => nameof(PGAddJobCodes),
+ DatabaseType.SqlServer => nameof(MSAddJobCodes),
+ DatabaseType.Sqlite => nameof(SLAddJobCodes),
+ _ => BadDatabaseType(),
+ };
+ if (targetVersion < new Version(5, 17, 0))
targetMigration = currentDatabaseType switch
{
DatabaseType.MySql => nameof(MYAddMapThreads),
@@ -431,7 +440,6 @@ namespace Tgstation.Server.Host.Database
DatabaseType.Sqlite => nameof(SLAddMapThreads),
_ => BadDatabaseType(),
};
-
if (targetVersion < new Version(5, 13, 0))
targetMigration = currentDatabaseType switch
{
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs
new file mode 100644
index 0000000000..556f6d1009
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs
@@ -0,0 +1,1075 @@
+//
+using System;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ [DbContext(typeof(SqlServerDatabaseContext))]
+ [Migration("20231105004801_MSAddJobCodes")]
+ partial class MSAddJobCodes
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ChannelLimit")
+ .HasColumnType("int");
+
+ b.Property("ConnectionString")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ChatSettingsId")
+ .HasColumnType("bigint");
+
+ b.Property("DiscordChannelId")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("IrcChannel")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("IsAdminChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsSystemChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsUpdatesChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsWatchdogChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Tag")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ 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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ByondVersion")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DMApiMajorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiMinorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiPatchVersion")
+ .HasColumnType("int");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DmeName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("GitHubDeploymentId")
+ .HasColumnType("int");
+
+ b.Property("GitHubRepoId")
+ .HasColumnType("bigint");
+
+ b.Property("JobId")
+ .HasColumnType("bigint");
+
+ b.Property("MinimumSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("Output")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RepositoryOrigin")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DirectoryName");
+
+ b.HasIndex("JobId")
+ .IsUnique();
+
+ b.HasIndex("RevisionInformationId");
+
+ b.ToTable("CompileJobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AdditionalParameters")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AllowWebClient")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("AutoStart")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("DumpOnHealthCheckRestart")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("HealthCheckSeconds")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("LogOutput")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("MapThreads")
+ .HasColumnType("bigint");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("SecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("StartProfiler")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("StartupTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("TopicRequestTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("Visibility")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamDaemonSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ApiValidationPort")
+ .HasColumnType("int");
+
+ b.Property("ApiValidationSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("ProjectName")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RequireDMApiValidation")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Timeout")
+ .IsRequired()
+ .HasColumnType("time");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamMakerSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AutoUpdateInterval")
+ .HasColumnType("bigint");
+
+ b.Property("ChatBotLimit")
+ .HasColumnType("int");
+
+ b.Property("ConfigurationType")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Online")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("SwarmIdentifer")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Path", "SwarmIdentifer")
+ .IsUnique()
+ .HasFilter("[SwarmIdentifer] IS NOT NULL");
+
+ b.ToTable("Instances");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ByondRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("ChatBotRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("ConfigurationRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("DreamDaemonRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("DreamMakerRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("InstancePermissionSetRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("PermissionSetId")
+ .HasColumnType("bigint");
+
+ b.Property("RepositoryRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("PermissionSetId", "InstanceId")
+ .IsUnique();
+
+ b.ToTable("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ 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("JobCode")
+ .HasColumnType("tinyint");
+
+ 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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ExternalUserId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Provider")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Provider", "ExternalUserId")
+ .IsUnique();
+
+ b.ToTable("OAuthConnections");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AdministrationRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("GroupId")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceManagerRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId")
+ .IsUnique()
+ .HasFilter("[GroupId] IS NOT NULL");
+
+ b.HasIndex("UserId")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("PermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccessIdentifier")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompileJobId")
+ .HasColumnType("bigint");
+
+ b.Property("InitialCompileJobId")
+ .HasColumnType("bigint");
+
+ b.Property("LaunchSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("LaunchVisibility")
+ .HasColumnType("int");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("ProcessId")
+ .HasColumnType("int");
+
+ b.Property("RebootState")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompileJobId");
+
+ b.HasIndex("InitialCompileJobId");
+
+ b.ToTable("ReattachInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AccessToken")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AccessUser")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AutoUpdatesKeepTestMerges")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("AutoUpdatesSynchronize")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("CommitterEmail")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CommitterName")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ 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.Property("UpdateSubmodules")
+ .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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ 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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("OriginCommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetimeoffset");
+
+ 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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Author")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BodyAtMerge")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Comment")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MergedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("MergedById")
+ .HasColumnType("bigint");
+
+ b.Property("Number")
+ .HasColumnType("int");
+
+ b.Property("PrimaryRevisionInformationId")
+ .IsRequired()
+ .HasColumnType("bigint");
+
+ b.Property("TargetCommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(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");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CanonicalName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreatedAt")
+ .IsRequired()
+ .HasColumnType("datetimeoffset");
+
+ b.Property("CreatedById")
+ .HasColumnType("bigint");
+
+ b.Property("Enabled")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("GroupId")
+ .HasColumnType("bigint");
+
+ b.Property("LastPasswordUpdate")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SystemIdentifier")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CanonicalName")
+ .IsUnique();
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("SystemIdentifier")
+ .IsUnique()
+ .HasFilter("[SystemIdentifier] IS NOT NULL");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("ChatSettings")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
+ .WithMany("Channels")
+ .HasForeignKey("ChatSettingsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ChatSettings");
+ });
+
+ 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();
+
+ b.Navigation("Job");
+
+ b.Navigation("RevisionInformation");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("DreamDaemonSettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("DreamMakerSettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("InstancePermissionSets")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet")
+ .WithMany("InstancePermissionSets")
+ .HasForeignKey("PermissionSetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+
+ b.Navigation("PermissionSet");
+ });
+
+ 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();
+
+ b.Navigation("CancelledBy");
+
+ b.Navigation("Instance");
+
+ b.Navigation("StartedBy");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "User")
+ .WithMany("OAuthConnections")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
+ .WithOne("PermissionSet")
+ .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("Tgstation.Server.Host.Models.User", "User")
+ .WithOne("PermissionSet")
+ .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Group");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
+ .WithMany()
+ .HasForeignKey("CompileJobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob")
+ .WithMany()
+ .HasForeignKey("InitialCompileJobId");
+
+ b.Navigation("CompileJob");
+
+ b.Navigation("InitialCompileJob");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("RepositorySettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ 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();
+
+ b.Navigation("RevisionInformation");
+
+ b.Navigation("TestMerge");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("RevisionInformations")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ 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();
+
+ b.Navigation("MergedBy");
+
+ b.Navigation("PrimaryRevisionInformation");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
+ .WithMany("CreatedUsers")
+ .HasForeignKey("CreatedById");
+
+ b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
+ .WithMany("Users")
+ .HasForeignKey("GroupId");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("Group");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Navigation("Channels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Navigation("ChatSettings");
+
+ b.Navigation("DreamDaemonSettings");
+
+ b.Navigation("DreamMakerSettings");
+
+ b.Navigation("InstancePermissionSets");
+
+ b.Navigation("Jobs");
+
+ b.Navigation("RepositorySettings");
+
+ b.Navigation("RevisionInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.Navigation("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.Navigation("ActiveTestMerges");
+
+ b.Navigation("CompileJobs");
+
+ b.Navigation("PrimaryTestMerge");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.Navigation("RevisonInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.Navigation("CreatedUsers");
+
+ b.Navigation("OAuthConnections");
+
+ b.Navigation("PermissionSet");
+
+ b.Navigation("TestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b =>
+ {
+ b.Navigation("PermissionSet")
+ .IsRequired();
+
+ b.Navigation("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs
new file mode 100644
index 0000000000..87636a060f
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs
@@ -0,0 +1,33 @@
+using System;
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ ///
+ public partial class MSAddJobCodes : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(migrationBuilder);
+ migrationBuilder.AddColumn(
+ name: "JobCode",
+ table: "Jobs",
+ type: "tinyint",
+ nullable: false,
+ defaultValue: (byte)0);
+ }
+
+ ///