mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Merge branch 'dev' into V6
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
|
||||
<TgsControlPanelVersion>4.25.5</TgsControlPanelVersion>
|
||||
<TgsControlPanelVersion>4.26.0</TgsControlPanelVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<!-- Usage: Dependency mocking for tests -->
|
||||
<!-- Pinned: Moq is OVER https://github.com/moq/moq/issues/1372 -->
|
||||
<PackageReference Include="Moq" Version="4.20.2" />
|
||||
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
|
||||
<PackageReference Include="Moq" Version="4.20.69" />
|
||||
<!-- Usage: MSTest execution -->
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<!-- Usage: MSTest asserts etc... -->
|
||||
|
||||
+3
-3
@@ -7,9 +7,9 @@
|
||||
<TgsConfigVersion>5.0.0</TgsConfigVersion>
|
||||
<TgsApiVersion>10.0.0</TgsApiVersion>
|
||||
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
|
||||
<TgsApiLibraryVersion>12.0.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>14.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>6.6.1</TgsDmapiVersion>
|
||||
<TgsApiLibraryVersion>13.0.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>16.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>6.6.2</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.6.2</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.4.0</TgsHostWatchdogVersion>
|
||||
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="myrules" Description="My rule set" ToolsVersion="17.0">
|
||||
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
|
||||
<Rule Id="UseConfigureAwait" Action="Warning" />
|
||||
@@ -88,7 +88,7 @@
|
||||
<Rule Id="CA1414" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1500" Action="Warning" />
|
||||
<Rule Id="CA1501" Action="Warning" />
|
||||
<Rule Id="CA1501" Action="None" />
|
||||
<Rule Id="CA1502" Action="Warning" />
|
||||
<Rule Id="CA1504" Action="Warning" />
|
||||
<Rule Id="CA1505" Action="Warning" />
|
||||
@@ -971,7 +971,7 @@
|
||||
<Rule Id="CA1033" Action="Warning" />
|
||||
<Rule Id="CA1050" Action="Warning" />
|
||||
<Rule Id="CA1060" Action="Warning" />
|
||||
<Rule Id="CA1501" Action="Warning" />
|
||||
<Rule Id="CA1501" Action="None" />
|
||||
<Rule Id="CA1502" Action="Warning" />
|
||||
<Rule Id="CA1505" Action="Warning" />
|
||||
<Rule Id="CA1506" Action="Warning" />
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// tgstation-server DMAPI
|
||||
|
||||
#define TGS_DMAPI_VERSION "6.6.1"
|
||||
#define TGS_DMAPI_VERSION "6.6.2"
|
||||
|
||||
// All functions and datums outside this document are subject to change with any version and should not be relied on.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public const string ApplicationJsonMime = "application/json";
|
||||
|
||||
/// <summary>
|
||||
/// Added to <see cref="MediaTypeNames.Application"/> in netstandard2.1. Can't use because of lack of .NET Framework support.
|
||||
/// </summary>
|
||||
const string TextEventStreamMime = "text/event-stream";
|
||||
|
||||
/// <summary>
|
||||
/// Get the version of the <see cref="Api"/> the caller is using.
|
||||
/// </summary>
|
||||
@@ -93,9 +99,9 @@ namespace Tgstation.Server.Api
|
||||
public Version ApiVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The client's JWT.
|
||||
/// The client's <see cref="TokenResponse"/>.
|
||||
/// </summary>
|
||||
public string? Token { get; }
|
||||
public TokenResponse? Token { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The client's username.
|
||||
@@ -107,13 +113,18 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public string? Password { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The OAuth code in use.
|
||||
/// </summary>
|
||||
public string? OAuthCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.OAuthProvider"/> the <see cref="Token"/> is for, if any.
|
||||
/// </summary>
|
||||
public OAuthProvider? OAuthProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the header uses password or TGS JWT authentication.
|
||||
/// If the header uses OAuth or TGS JWT authentication.
|
||||
/// </summary>
|
||||
public bool IsTokenAuthentication => Token != null && !OAuthProvider.HasValue;
|
||||
|
||||
@@ -129,16 +140,31 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
/// <param name="userAgent">The value of <see cref="UserAgent"/>.</param>
|
||||
/// <param name="token">The value of <see cref="Token"/>.</param>
|
||||
/// <param name="oauthProvider">The value of <see cref="OAuthProvider"/>.</param>
|
||||
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;
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiHeaders"/> class. Used for token authentication.
|
||||
/// </summary>
|
||||
/// <param name="userAgent">The value of <see cref="UserAgent"/>.</param>
|
||||
/// <param name="oAuthCode">The value of <see cref="OAuthCode"/>.</param>
|
||||
/// <param name="oAuthProvider">The value of <see cref="OAuthProvider"/>.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -163,48 +189,57 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> containing the serialized <see cref="ApiHeaders"/>.</param>
|
||||
/// <param name="ignoreMissingAuth">If a missing <see cref="HeaderNames.Authorization"/> should be ignored.</param>
|
||||
/// <param name="allowEventStreamAccept">If <see cref="TextEventStreamMime"/> is a valid accept.</param>
|
||||
/// <exception cref="HeadersException">Thrown if the <paramref name="requestHeaders"/> constitue invalid <see cref="ApiHeaders"/>.</exception>
|
||||
#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<string>(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<OAuthProvider>(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
|
||||
/// <param name="token">The value of <see cref="Token"/>.</param>
|
||||
/// <param name="username">The value of <see cref="Username"/>.</param>
|
||||
/// <param name="password">The value of <see cref="Password"/>.</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <paramref name="headers"/> necessary for a SignalR hub connection.
|
||||
/// </summary>
|
||||
/// <param name="headers">The headers <see cref="IDictionary{TKey, TValue}"/> to write to.</param>
|
||||
public void SetHubConnectionHeaders(IDictionary<string, string> 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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the <see cref="string"/>ified for of the <see cref="ApiVersionHeader"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="string"/> representing the <see cref="ApiVersion"/>.</returns>
|
||||
string CreateApiVersionHeader()
|
||||
=> new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Types of individual <see cref="ApiHeaders"/> errors.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum HeaderErrorTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// No header errors.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.Net.Http.Headers.HeaderNames.UserAgent"/> header is missing or invalid.
|
||||
/// </summary>
|
||||
UserAgent = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.Net.Http.Headers.HeaderNames.Accept"/> header is missing or invalid.
|
||||
/// </summary>
|
||||
Accept = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ApiHeaders.ApiVersionHeader"/> header is missing or invalid.
|
||||
/// </summary>
|
||||
Api = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.Net.Http.Headers.HeaderNames.Authorization"/> header is invalid.
|
||||
/// </summary>
|
||||
AuthorizationInvalid = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ApiHeaders.OAuthProviderHeader"/> header is missing or invalid.
|
||||
/// </summary>
|
||||
OAuthProvider = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.Net.Http.Headers.HeaderNames.Authorization"/> header is missing.
|
||||
/// </summary>
|
||||
AuthorizationMissing = 1 << 5,
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Types of individual <see cref="ApiHeaders"/>.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum HeaderTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// No headers.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.UserAgent"/> header.
|
||||
/// </summary>
|
||||
UserAgent = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.Accept"/> header.
|
||||
/// </summary>
|
||||
Accept = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ApiHeaders.ApiVersionHeader"/>.
|
||||
/// </summary>
|
||||
Api = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.Authorization"/>
|
||||
/// </summary>
|
||||
Authorization = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ApiHeaders.OAuthProviderHeader"/>.
|
||||
/// </summary>
|
||||
OAuthProvider = 1 << 4,
|
||||
}
|
||||
}
|
||||
@@ -8,19 +8,19 @@ namespace Tgstation.Server.Api
|
||||
public sealed class HeadersException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="HeaderTypes"/>s that are missing or malformed.
|
||||
/// The <see cref="HeaderErrorTypes"/>s that are missing or malformed.
|
||||
/// </summary>
|
||||
public HeaderTypes MissingOrMalformedHeaders { get; }
|
||||
public HeaderErrorTypes ParseErrors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeadersException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="missingOrMalformedHeaders">The value of <see cref="MissingOrMalformedHeaders"/>.</param>
|
||||
/// <param name="parseErrors">The value of <see cref="ParseErrors"/>.</param>
|
||||
/// <param name="message">The error message.</param>
|
||||
public HeadersException(HeaderTypes missingOrMalformedHeaders, string message)
|
||||
public HeadersException(HeaderErrorTypes parseErrors, string message)
|
||||
: base(message)
|
||||
{
|
||||
MissingOrMalformedHeaders = missingOrMalformedHeaders;
|
||||
ParseErrors = parseErrors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Api.Hubs
|
||||
{
|
||||
/// <summary>
|
||||
/// SignalR client methods for receiving <see cref="JobResponse"/>s.
|
||||
/// </summary>
|
||||
public interface IJobsHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Push a <paramref name="job"/> update to the client.
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="JobResponse"/> to push.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
Task ReceiveJobUpdate(JobResponse job, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <summary>
|
||||
/// The DMAPI never validated itself
|
||||
/// </summary>
|
||||
[Description("DreamDaemon 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")]
|
||||
DreamMakerNeverValidated,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,9 +10,16 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// </summary>
|
||||
public class Job : EntityId
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Models.JobCode"/>.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public JobCode? JobCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// English description of the <see cref="Job"/>.
|
||||
/// </summary>
|
||||
/// <remarks>May not match the <see cref="System.ComponentModel.DescriptionAttribute"/> listed on the <see cref="Models.JobCode"/>.</remarks>
|
||||
[Required]
|
||||
public string? Description { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// The different types of <see cref="Response.JobResponse"/>.
|
||||
/// </summary>
|
||||
public enum JobCode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// This catch-all code is applied to jobs that were created on a tgstation-server before v5.17.0.
|
||||
/// </summary>
|
||||
[Description("Legacy job")]
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// When the instance is being moved.
|
||||
/// </summary>
|
||||
[Description("Instance move")]
|
||||
Move,
|
||||
|
||||
/// <summary>
|
||||
/// When the repository is cloning.
|
||||
/// </summary>
|
||||
[Description("Clone repository")]
|
||||
RepositoryClone,
|
||||
|
||||
/// <summary>
|
||||
/// When the repository is being manually updated.
|
||||
/// </summary>
|
||||
[Description("Update repository")]
|
||||
RepositoryUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// When the repository is being automatically updated.
|
||||
/// </summary>
|
||||
[Description("Scheduled repository update")]
|
||||
RepositoryAutoUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// When the repository is being deleted.
|
||||
/// </summary>
|
||||
[Description("Delete repository")]
|
||||
RepositoryDelete,
|
||||
|
||||
/// <summary>
|
||||
/// When a new BYOND version is being installed.
|
||||
/// </summary>
|
||||
[Description("Install BYOND version")]
|
||||
ByondOfficialInstall,
|
||||
|
||||
/// <summary>
|
||||
/// When a new BYOND version is being installed.
|
||||
/// </summary>
|
||||
[Description("Install custom BYOND version")]
|
||||
ByondCustomInstall,
|
||||
|
||||
/// <summary>
|
||||
/// When an installed BYOND version is being deleted.
|
||||
/// </summary>
|
||||
[Description("Delete installed BYOND version")]
|
||||
ByondDelete,
|
||||
|
||||
/// <summary>
|
||||
/// When a deployment is manually triggered.
|
||||
/// </summary>
|
||||
[Description("Compile active repository code")]
|
||||
Deployment,
|
||||
|
||||
/// <summary>
|
||||
/// When a deployment is automatically triggered.
|
||||
/// </summary>
|
||||
[Description("Scheduled code deployment")]
|
||||
AutomaticDeployment,
|
||||
|
||||
/// <summary>
|
||||
/// When the watchdog is started manually.
|
||||
/// </summary>
|
||||
[Description("Launch DreamDaemon")]
|
||||
WatchdogLaunch,
|
||||
|
||||
/// <summary>
|
||||
/// When the watchdog is restarted manually.
|
||||
/// </summary>
|
||||
[Description("Restart Watchdog")]
|
||||
WatchdogRestart,
|
||||
|
||||
/// <summary>
|
||||
/// When a the watchdog is dumping the game server process.
|
||||
/// </summary>
|
||||
[Description("Create DreamDaemon Process Dump")]
|
||||
WatchdogDump,
|
||||
|
||||
/// <summary>
|
||||
/// When the watchdog starts due to an instance being onlined.
|
||||
/// </summary>
|
||||
[Description("Instance startup watchdog launch")]
|
||||
StartupWatchdogLaunch,
|
||||
|
||||
/// <summary>
|
||||
/// When the watchdog reattaches due to an instance being onlined.
|
||||
/// </summary>
|
||||
[Description("Instance startup watchdog reattach")]
|
||||
StartupWatchdogReattach,
|
||||
|
||||
/// <summary>
|
||||
/// When a chat bot connects/reconnects.
|
||||
/// </summary>
|
||||
[Description("Reconnect chat bot")]
|
||||
ReconnectChatBot,
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
/// <summary>
|
||||
/// For creating a user.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1501
|
||||
public sealed class UserCreateRequest : UserUpdateRequest
|
||||
{
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
/// </summary>
|
||||
public sealed class JobResponse : Internal.Job
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="EntityId.Id"/> of the <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
public long? InstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="UserResponse"/> that started the job.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Response
|
||||
{
|
||||
/// <summary>
|
||||
@@ -15,6 +17,13 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// <summary>
|
||||
/// When the <see cref="TokenResponse"/> expires.
|
||||
/// </summary>
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
[Obsolete("Will be removed in a future API version")]
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses the <see cref="Bearer"/> as a <see cref="JsonWebToken"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="JsonWebToken"/> based on <see cref="Bearer"/>.</returns>
|
||||
public JsonWebToken ParseJwt() => new (Bearer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <returns>The <see cref="Enum"/> <see cref="Type"/> of the given <paramref name="rightsType"/>.</returns>
|
||||
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
|
||||
|
||||
/// <summary>
|
||||
/// Map a given <typeparamref name="TRight"/> to its respective <see cref="RightsType"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRight">The <see cref="Type"/> of the right.</typeparam>
|
||||
/// <returns>The <see cref="RightsType"/> of <typeparamref name="TRight"/>.</returns>
|
||||
public static RightsType TypeToRight<TRight>()
|
||||
where TRight : Enum
|
||||
=> TypeMap.First(kvp => kvp.Value == typeof(TRight)).Key;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the role claim name used for a given <paramref name="right"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public const string Root = "/";
|
||||
|
||||
/// <summary>
|
||||
/// The root route of all hubs.
|
||||
/// </summary>
|
||||
public const string HubsRoot = Root + "hubs";
|
||||
|
||||
/// <summary>
|
||||
/// The server administration controller.
|
||||
/// </summary>
|
||||
@@ -102,6 +107,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public const string List = "List";
|
||||
|
||||
/// <summary>
|
||||
/// The root route of all hubs.
|
||||
/// </summary>
|
||||
public const string JobsHub = HubsRoot + "/jobs";
|
||||
|
||||
/// <summary>
|
||||
/// Apply an <paramref name="id"/> postfix to a <paramref name="route"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
<ItemGroup>
|
||||
<!-- Usage: HTTP constants reference -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
|
||||
<!-- Usage: Decoding the 'nbf' property of JWTs -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.2" />
|
||||
<!-- Usage: Primary JSON library -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<!-- Usage: Data model annotating -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="JsonSerializerSettings"/> to use.
|
||||
/// </summary>
|
||||
static readonly JsonSerializerSettings SerializerSettings = new ()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
Converters = new[]
|
||||
{
|
||||
new VersionConverter(),
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IHttpClient"/> for the <see cref="ApiClient"/>.
|
||||
/// </summary>
|
||||
@@ -61,6 +78,11 @@ namespace Tgstation.Server.Client
|
||||
/// </summary>
|
||||
readonly List<IRequestLogger> requestLoggers;
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="HubConnection"/>s created by the <see cref="ApiClient"/>.
|
||||
/// </summary>
|
||||
readonly List<HubConnection> hubConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="Headers"/>.
|
||||
/// </summary>
|
||||
@@ -82,14 +104,9 @@ namespace Tgstation.Server.Client
|
||||
ApiHeaders headers;
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="JsonSerializerSettings"/> to use.
|
||||
/// If the <see cref="ApiClient"/> is disposed.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="JsonSerializerSettings"/> instance.</returns>
|
||||
static JsonSerializerSettings GetSerializerSettings() => new ()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
Converters = new[] { new VersionConverter() },
|
||||
};
|
||||
bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Handle a bad HTTP <paramref name="response"/>.
|
||||
@@ -102,7 +119,7 @@ namespace Tgstation.Server.Client
|
||||
try
|
||||
{
|
||||
// check if json serializes to an error message
|
||||
errorMessage = JsonConvert.DeserializeObject<ErrorMessageResponse>(json, GetSerializerSettings());
|
||||
errorMessage = JsonConvert.DeserializeObject<ErrorMessageResponse>(json, SerializerSettings);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
@@ -149,7 +166,12 @@ namespace Tgstation.Server.Client
|
||||
/// <param name="apiHeaders">The value of <see cref="Headers"/>.</param>
|
||||
/// <param name="tokenRefreshHeaders">The value of <see cref="tokenRefreshHeaders"/>.</param>
|
||||
/// <param name="authless">The value of <see cref="authless"/>.</param>
|
||||
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<IRequestLogger>();
|
||||
hubConnections = new List<HubConnection>();
|
||||
semaphoreSlim = new SemaphoreSlim(1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
List<HubConnection> 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,128 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to refresh the stored Bearer token in <see cref="Headers"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the refresh was successful, <see langword="false"/> if a refresh is unable to be performed.</returns>
|
||||
public async ValueTask<bool> 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<object, TokenResponse>(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false);
|
||||
headers = new ApiHeaders(headers.UserAgent!, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IAsyncDisposable> CreateHubConnection<THubImplementation>(
|
||||
THubImplementation hubImplementation,
|
||||
IRetryPolicy? retryPolicy,
|
||||
Action<ILoggingBuilder>? loggingConfigureAction,
|
||||
CancellationToken cancellationToken)
|
||||
where THubImplementation : class
|
||||
{
|
||||
if (hubImplementation == null)
|
||||
throw new ArgumentNullException(nameof(hubImplementation));
|
||||
|
||||
retryPolicy ??= new InfiniteThirtySecondMaxRetryPolicy();
|
||||
|
||||
HubConnection? hubConnection = null;
|
||||
var hubConnectionBuilder = new HubConnectionBuilder()
|
||||
.AddNewtonsoftJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerSettings = SerializerSettings;
|
||||
})
|
||||
.WithAutomaticReconnect(retryPolicy)
|
||||
.WithUrl(
|
||||
new Uri(Url, Routes.JobsHub),
|
||||
HttpTransportType.ServerSentEvents,
|
||||
options =>
|
||||
{
|
||||
options.AccessTokenProvider = async () =>
|
||||
{
|
||||
// DCT: None available.
|
||||
if (Headers.Token == null
|
||||
|| (Headers.Token.ParseJwt().ValidTo <= DateTime.UtcNow
|
||||
&& !await RefreshToken(CancellationToken.None)))
|
||||
{
|
||||
_ = hubConnection!.StopAsync(); // DCT: None available.
|
||||
return null;
|
||||
}
|
||||
|
||||
return Headers.Token.Bearer;
|
||||
};
|
||||
|
||||
options.CloseTimeout = Timeout;
|
||||
|
||||
Headers.SetHubConnectionHeaders(options.Headers);
|
||||
});
|
||||
|
||||
if (loggingConfigureAction != null)
|
||||
hubConnectionBuilder.ConfigureLogging(loggingConfigureAction);
|
||||
|
||||
hubConnection = hubConnectionBuilder.Build();
|
||||
try
|
||||
{
|
||||
hubConnection.Closed += async (error) =>
|
||||
{
|
||||
if (error is HttpRequestException httpRequestException)
|
||||
{
|
||||
// .StatusCode isn't in netstandard but fuck the police
|
||||
var property = error.GetType().GetProperty("StatusCode");
|
||||
if (property != null)
|
||||
{
|
||||
var statusCode = (HttpStatusCode?)property.GetValue(error);
|
||||
if (statusCode == HttpStatusCode.Unauthorized
|
||||
&& !await RefreshToken(CancellationToken.None))
|
||||
_ = hubConnection!.StopAsync();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
hubConnection.ProxyOn(hubImplementation);
|
||||
|
||||
Task startTask;
|
||||
lock (hubConnections)
|
||||
{
|
||||
if (disposed)
|
||||
throw new ObjectDisposedException(nameof(ApiClient));
|
||||
|
||||
hubConnections.Add(hubConnection);
|
||||
startTask = hubConnection.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await startTask;
|
||||
|
||||
return hubConnection;
|
||||
}
|
||||
catch
|
||||
{
|
||||
bool needsDispose;
|
||||
lock (hubConnections)
|
||||
needsDispose = hubConnections.Remove(hubConnection);
|
||||
|
||||
if (needsDispose)
|
||||
await hubConnection.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main request method.
|
||||
/// </summary>
|
||||
@@ -305,6 +464,7 @@ namespace Tgstation.Server.Client
|
||||
/// <param name="tokenRefresh">If this is a token refresh operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the response on success.</returns>
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
protected virtual async ValueTask<TResult> RunRequest<TResult>(
|
||||
string route,
|
||||
HttpContent? content,
|
||||
@@ -322,7 +482,7 @@ namespace Tgstation.Server.Client
|
||||
|
||||
HttpResponseMessage response;
|
||||
var fullUri = new Uri(Url, route);
|
||||
var serializerSettings = GetSerializerSettings();
|
||||
var serializerSettings = SerializerSettings;
|
||||
var fileDownload = typeof(TResult) == typeof(Stream);
|
||||
using (var request = new HttpRequestMessage(method, fullUri))
|
||||
{
|
||||
@@ -336,6 +496,21 @@ namespace Tgstation.Server.Client
|
||||
|
||||
if (authless)
|
||||
request.Headers.Remove(HeaderNames.Authorization);
|
||||
else
|
||||
{
|
||||
var bearer = headersToUse.Token?.Bearer;
|
||||
if (bearer != null)
|
||||
{
|
||||
var parsed = headersToUse.Token!.ParseJwt();
|
||||
var nbf = parsed.ValidFrom;
|
||||
var now = DateTime.UtcNow;
|
||||
if (nbf >= now)
|
||||
{
|
||||
var delay = (nbf - now).Add(TimeSpan.FromMilliseconds(1));
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileDownload)
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Octet));
|
||||
@@ -392,38 +567,7 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to refresh the bearer token in the <see cref="headers"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the refresh was successful, <see langword="false"/> otherwise.</returns>
|
||||
async ValueTask<bool> 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<object, TokenResponse>(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false);
|
||||
headers = new ApiHeaders(headers.UserAgent!, token.Bearer!);
|
||||
}
|
||||
catch (ClientException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#pragma warning restore CA1506
|
||||
|
||||
/// <summary>
|
||||
/// Main request method.
|
||||
@@ -449,7 +593,7 @@ namespace Tgstation.Server.Client
|
||||
HttpContent? content = null;
|
||||
if (body != null)
|
||||
content = new StringContent(
|
||||
JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, GetSerializerSettings()),
|
||||
JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, SerializerSettings),
|
||||
Encoding.UTF8,
|
||||
ApiHeaders.ApplicationJsonMime);
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace Tgstation.Server.Client.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="HubConnection"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
static class HubConnectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply a given <paramref name="proxy"/> to a given <paramref name="hubConnection"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClientProxy">The strongly typed client proxy.</typeparam>
|
||||
/// <param name="hubConnection">The <see cref="HubConnection"/> to proxy on.</param>
|
||||
/// <param name="proxy">The <typeparamref name="TClientProxy"/> to forward operations to.</param>
|
||||
public static void ProxyOn<TClientProxy>(this HubConnection hubConnection, TClientProxy proxy)
|
||||
where TClientProxy : class
|
||||
{
|
||||
if (hubConnection == null)
|
||||
throw new ArgumentNullException(nameof(hubConnection));
|
||||
|
||||
if (proxy == null)
|
||||
throw new ArgumentNullException(nameof(proxy));
|
||||
|
||||
ProxyOn(hubConnection, typeof(TClientProxy), proxy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a given <paramref name="proxyObject"/> to a given <paramref name="hubConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="hubConnection">The <see cref="HubConnection"/> to proxy on.</param>
|
||||
/// <param name="proxyType">The <see cref="Type"/> of <paramref name="proxyObject"/>.</param>
|
||||
/// <param name="proxyObject">The <see cref="object"/> to forward operations to.</param>
|
||||
static void ProxyOn(this HubConnection hubConnection, Type proxyType, object proxyObject)
|
||||
{
|
||||
var clientMethods = proxyType.GetMethods();
|
||||
var cancellationTokenType = typeof(CancellationToken);
|
||||
foreach (var clientMethod in clientMethods)
|
||||
{
|
||||
var parametersList = clientMethod
|
||||
.GetParameters()
|
||||
.Select(parameterInfo => parameterInfo.ParameterType)
|
||||
.ToList();
|
||||
|
||||
var cancellationTokenIndex = parametersList.IndexOf(cancellationTokenType);
|
||||
if (cancellationTokenIndex != -1)
|
||||
{
|
||||
parametersList.RemoveAt(cancellationTokenIndex);
|
||||
#if DEBUG
|
||||
if (parametersList.IndexOf(cancellationTokenType) != -1)
|
||||
throw new InvalidOperationException("Cannot ProxyOn a method with multiple CancellationToken parameters!");
|
||||
#endif
|
||||
}
|
||||
|
||||
var parameters = parametersList.ToArray();
|
||||
|
||||
object?[] AddCancellationTokenToParametersArray(object?[] parametersArray)
|
||||
{
|
||||
if (cancellationTokenIndex == -1)
|
||||
return parametersArray;
|
||||
|
||||
var newList = parametersArray.ToList();
|
||||
newList.Insert(cancellationTokenIndex, CancellationToken.None);
|
||||
return newList.ToArray();
|
||||
}
|
||||
|
||||
var returnType = clientMethod.ReturnType;
|
||||
if (returnType != typeof(Task))
|
||||
{
|
||||
if (returnType.BaseType != typeof(Task))
|
||||
throw new InvalidOperationException($"Return type {returnType} of {proxyType.FullName}.{clientMethod.Name} is not supported! Only Task and derivatives are supported.");
|
||||
|
||||
var resultProperty = returnType.GetProperty(nameof(Task<object>.Result));
|
||||
hubConnection.On(
|
||||
clientMethod.Name,
|
||||
parameters,
|
||||
async (parameterArray, _) =>
|
||||
{
|
||||
var task = (Task)clientMethod.Invoke(proxyObject, AddCancellationTokenToParametersArray(parameterArray));
|
||||
await task;
|
||||
return resultProperty.GetValue(task);
|
||||
},
|
||||
hubConnection);
|
||||
}
|
||||
else
|
||||
hubConnection.On(
|
||||
clientMethod.Name,
|
||||
parameters,
|
||||
(parameterArray) =>
|
||||
{
|
||||
return (Task)clientMethod.Invoke(proxyObject, AddCancellationTokenToParametersArray(parameterArray));
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var inheritedInterface in proxyType.GetInterfaces())
|
||||
ProxyOn(hubConnection, inheritedInterface, proxyObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
@@ -12,7 +15,7 @@ namespace Tgstation.Server.Client
|
||||
/// <summary>
|
||||
/// Web interface for the API.
|
||||
/// </summary>
|
||||
interface IApiClient : IDisposable
|
||||
interface IApiClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ApiHeaders"/> the <see cref="IApiClient"/> uses.
|
||||
@@ -35,6 +38,22 @@ namespace Tgstation.Server.Client
|
||||
/// <param name="requestLogger">The <see cref="IRequestLogger"/> to add.</param>
|
||||
void AddRequestLogger(IRequestLogger requestLogger);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all job updates available to the <see cref="IServerClient"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="THubImplementation">The <see cref="Type"/> of the hub being implemented.</typeparam>
|
||||
/// <param name="hubImplementation">The <typeparamref name="THubImplementation"/> to use for proxying the methods of the hub connection.</param>
|
||||
/// <param name="retryPolicy">The optional <see cref="IRetryPolicy"/> to use for the backing connection. The default retry policy waits for 1, 2, 4, 8, and 16 seconds, then 30s repeatedly.</param>
|
||||
/// <param name="loggingConfigureAction">The optional <see cref="Action{T1}"/> used to configure a <see cref="ILoggingBuilder"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>An <see cref="IAsyncDisposable"/> representing the lifetime of the subscription.</returns>
|
||||
ValueTask<IAsyncDisposable> CreateHubConnection<THubImplementation>(
|
||||
THubImplementation hubImplementation,
|
||||
IRetryPolicy? retryPolicy,
|
||||
Action<ILoggingBuilder>? loggingConfigureAction,
|
||||
CancellationToken cancellationToken)
|
||||
where THubImplementation : class;
|
||||
|
||||
/// <summary>
|
||||
/// Run an HTTP PUT request.
|
||||
/// </summary>
|
||||
|
||||
@@ -17,6 +17,10 @@ namespace Tgstation.Server.Client
|
||||
/// <param name="tokenRefreshHeaders">The <see cref="ApiHeaders"/> to use to generate a new <see cref="Api.Models.Response.TokenResponse"/>.</param>
|
||||
/// <param name="authless">If there should be no authentication performed.</param>
|
||||
/// <returns>A new <see cref="IApiClient"/>.</returns>
|
||||
IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders, bool authless);
|
||||
IApiClient CreateApiClient(
|
||||
Uri url,
|
||||
ApiHeaders apiHeaders,
|
||||
ApiHeaders? tokenRefreshHeaders,
|
||||
bool authless);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Client
|
||||
@@ -9,7 +13,7 @@ namespace Tgstation.Server.Client
|
||||
/// <summary>
|
||||
/// Main client for communicating with a server.
|
||||
/// </summary>
|
||||
public interface IServerClient : IDisposable
|
||||
public interface IServerClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The connected server <see cref="Uri"/>.
|
||||
@@ -53,6 +57,20 @@ namespace Tgstation.Server.Client
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ServerInformationResponse"/> of the target server.</returns>
|
||||
ValueTask<ServerInformationResponse> ServerInformation(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all job updates available to the <see cref="IServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="jobsReceiver">The <see cref="IJobsHub"/> to use to subscribe to updates.</param>
|
||||
/// <param name="retryPolicy">The optional <see cref="IRetryPolicy"/> to use for the backing connection. The default retry policy waits for 1, 2, 4, 8, and 16 seconds, then 30s repeatedly.</param>
|
||||
/// <param name="loggingConfigureAction">The optional <see cref="Action{T1}"/> used to configure a <see cref="ILoggingBuilder"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>An <see cref="IAsyncDisposable"/> representing the lifetime of the subscription.</returns>
|
||||
ValueTask<IAsyncDisposable> SubscribeToJobUpdates(
|
||||
IJobsHub jobsReceiver,
|
||||
IRetryPolicy? retryPolicy = null,
|
||||
Action<ILoggingBuilder>? loggingConfigureAction = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <paramref name="requestLogger"/> to the request pipeline.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IRetryPolicy"/> that returns seconds in powers of 2, maxing out at 30s.
|
||||
/// </summary>
|
||||
sealed class InfiniteThirtySecondMaxRetryPolicy : IRetryPolicy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeSpan? NextRetryDelay(RetryContext retryContext)
|
||||
{
|
||||
if (retryContext == null)
|
||||
throw new ArgumentNullException(nameof(retryContext));
|
||||
|
||||
return TimeSpan.FromSeconds(Math.Min(Math.Pow(2, retryContext.PreviousRetryCount), 30));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Client
|
||||
@@ -16,12 +20,8 @@ namespace Tgstation.Server.Client
|
||||
/// <inheritdoc />
|
||||
public TokenResponse Token
|
||||
{
|
||||
get => token;
|
||||
set
|
||||
{
|
||||
token = value ?? throw new InvalidOperationException("Cannot set a null Token!");
|
||||
apiClient.Headers = new ApiHeaders(apiClient.Headers.UserAgent!, token.Bearer!);
|
||||
}
|
||||
get => apiClient.Headers.Token ?? throw new InvalidOperationException("apiClient.Headers.Token was null!");
|
||||
set => apiClient.Headers = new ApiHeaders(apiClient.Headers.UserAgent!, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -48,23 +48,13 @@ namespace Tgstation.Server.Client
|
||||
/// </summary>
|
||||
readonly IApiClient apiClient;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="Token"/>.
|
||||
/// </summary>
|
||||
TokenResponse token;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The value of <see cref="apiClient"/>.</param>
|
||||
/// <param name="token">The value of <see cref="Token"/>.</param>
|
||||
public ServerClient(IApiClient apiClient, TokenResponse token)
|
||||
public ServerClient(IApiClient apiClient)
|
||||
{
|
||||
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||
this.token = token ?? throw new ArgumentNullException(nameof(token));
|
||||
|
||||
if (Token.Bearer != apiClient.Headers.Token)
|
||||
throw new ArgumentOutOfRangeException(nameof(token), token, "Provided token does not match apiClient headers!");
|
||||
|
||||
Instances = new InstanceManagerClient(apiClient);
|
||||
Users = new UsersClient(apiClient);
|
||||
@@ -73,12 +63,20 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => apiClient.Dispose();
|
||||
public ValueTask DisposeAsync() => apiClient.DisposeAsync();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<ServerInformationResponse> ServerInformation(CancellationToken cancellationToken) => apiClient.Read<ServerInformationResponse>(Routes.Root, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IAsyncDisposable> SubscribeToJobUpdates(
|
||||
IJobsHub jobsReceiver,
|
||||
IRetryPolicy? retryPolicy,
|
||||
Action<ILoggingBuilder>? loggingConfigureAction,
|
||||
CancellationToken cancellationToken)
|
||||
=> apiClient.CreateHubConnection(jobsReceiver, retryPolicy, loggingConfigureAction, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,15 @@ namespace Tgstation.Server.Client
|
||||
if (token.Bearer == null)
|
||||
throw new InvalidOperationException("token.Bearer should not be null!");
|
||||
|
||||
return new ServerClient(ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer), null, false), token);
|
||||
var serverClient = new ServerClient(
|
||||
ApiClientFactory.CreateApiClient(
|
||||
host,
|
||||
new ApiHeaders(
|
||||
productHeaderValue,
|
||||
token),
|
||||
null,
|
||||
false));
|
||||
return serverClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -112,7 +120,16 @@ namespace Tgstation.Server.Client
|
||||
TimeSpan? timeout = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var api = ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, "fake"), null, true);
|
||||
await using var api = ApiClientFactory.CreateApiClient(
|
||||
host,
|
||||
new ApiHeaders(
|
||||
productHeaderValue,
|
||||
new TokenResponse
|
||||
{
|
||||
Bearer = "unused",
|
||||
}),
|
||||
null,
|
||||
true);
|
||||
|
||||
if (requestLoggers != null)
|
||||
foreach (var requestLogger in requestLoggers)
|
||||
@@ -145,7 +162,7 @@ namespace Tgstation.Server.Client
|
||||
requestLoggers ??= Enumerable.Empty<IRequestLogger>();
|
||||
|
||||
TokenResponse token;
|
||||
using (var api = ApiClientFactory.CreateApiClient(host, loginHeaders, null, false))
|
||||
await using (var api = ApiClientFactory.CreateApiClient(host, loginHeaders, null, false))
|
||||
{
|
||||
foreach (var requestLogger in requestLoggers)
|
||||
api.AddRequestLogger(requestLogger);
|
||||
@@ -155,14 +172,13 @@ namespace Tgstation.Server.Client
|
||||
token = await api.Update<TokenResponse>(Routes.Root, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var apiHeaders = new ApiHeaders(productHeaderValue, token.Bearer!);
|
||||
var apiHeaders = new ApiHeaders(productHeaderValue, token);
|
||||
var client = new ServerClient(
|
||||
ApiClientFactory.CreateApiClient(
|
||||
host,
|
||||
apiHeaders,
|
||||
attemptLoginRefresh ? loginHeaders : null,
|
||||
false),
|
||||
token);
|
||||
false));
|
||||
if (timeout.HasValue)
|
||||
client.Timeout = timeout.Value;
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
<PackageReleaseNotes>$(TGS_NUGET_RELEASE_NOTES_CLIENT)</PackageReleaseNotes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Connecting to SignalR hubs in API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.0-rc.1.23421.29" />
|
||||
<!-- Usage: Using target JSON serializer for API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.0-rc.1.23421.29" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Common\Tgstation.Server.Common.csproj" /> <!-- Needed for explicit nuget versioning -->
|
||||
|
||||
@@ -372,7 +372,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
var completionString = errorMessage == null ? "Pending" : "Failed";
|
||||
|
||||
Embed CreateUpdatedEmbed(string message, Color color) => new Embed
|
||||
Embed CreateUpdatedEmbed(string message, Color color) => new ()
|
||||
{
|
||||
Author = embed.Author,
|
||||
Colour = color,
|
||||
|
||||
@@ -352,7 +352,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
dbChannel,
|
||||
new List<ChannelRepresentation>
|
||||
{
|
||||
new ChannelRepresentation
|
||||
new ()
|
||||
{
|
||||
RealId = id.Value,
|
||||
IsAdminChannel = dbChannel.IsAdminChannel == true,
|
||||
|
||||
@@ -273,13 +273,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
connectNow = false;
|
||||
if (!Connected)
|
||||
{
|
||||
var job = new Job
|
||||
{
|
||||
Description = $"Reconnect chat bot: {ChatBot.Name}",
|
||||
CancelRight = (ulong)ChatBotRights.WriteEnabled,
|
||||
CancelRightsType = RightsType.ChatBots,
|
||||
Instance = ChatBot.Instance,
|
||||
};
|
||||
var job = Job.Create(Api.Models.JobCode.ReconnectChatBot, null, ChatBot.Instance, ChatBotRights.WriteEnabled);
|
||||
job.Description += $": {ChatBot.Name}";
|
||||
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
|
||||
@@ -239,6 +239,8 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
.Where(x => x.Id == compileJob.Id)
|
||||
.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)
|
||||
|
||||
@@ -337,10 +337,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
async databaseContext =>
|
||||
{
|
||||
var fullJob = compileJob.Job;
|
||||
compileJob.Job = new Models.Job
|
||||
{
|
||||
Id = job.Id,
|
||||
};
|
||||
compileJob.Job = new Models.Job(job.Id.Value);
|
||||
var fullRevInfo = compileJob.RevisionInformation;
|
||||
compileJob.RevisionInformation = new Models.RevisionInformation
|
||||
{
|
||||
@@ -431,10 +428,10 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
.Where(x => x.Job.Instance.Id == metadata.Id)
|
||||
.OrderByDescending(x => x.Job.StoppedAt)
|
||||
.Take(10)
|
||||
.Select(x => new Models.Job
|
||||
.Select(x => new
|
||||
{
|
||||
StoppedAt = x.Job.StoppedAt,
|
||||
StartedAt = x.Job.StartedAt,
|
||||
x.Job.StoppedAt,
|
||||
x.Job.StartedAt,
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
|
||||
+2
-8
@@ -32,10 +32,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask FailDeployment(Models.CompileJob compileJob, string errorMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public override ValueTask FailDeployment(Models.CompileJob compileJob, string errorMessage, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(IRepository repository, Models.RepositorySettings repositorySettings, Models.RevisionInformation revisionInformation, CancellationToken cancellationToken)
|
||||
@@ -69,10 +66,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
=> String.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask MarkInactiveImpl(Models.CompileJob compileJob, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
protected override ValueTask MarkInactiveImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask StageDeploymentImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
@@ -161,5 +161,11 @@
|
||||
/// </summary>
|
||||
[EventScript("DeploymentCleanup")]
|
||||
DeploymentCleanup,
|
||||
|
||||
/// <summary>
|
||||
/// Whenever a deployment is about to be used by the game server. May fire multiple times per deployment. Parameters: Game directory path
|
||||
/// </summary>
|
||||
[EventScript("DeploymentActivation")]
|
||||
DeploymentActivation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <summary>
|
||||
/// A unique ID for the <see cref="IInstanceReference"/>.
|
||||
/// </summary>
|
||||
public Guid Uid { get; }
|
||||
public ulong Uid { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,17 +501,7 @@ namespace Tgstation.Server.Host.Components
|
||||
await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty<string>(), true, cancellationToken);
|
||||
try
|
||||
{
|
||||
var repositoryUpdateJob = new Job
|
||||
{
|
||||
Instance = new Models.Instance
|
||||
{
|
||||
Id = metadata.Id,
|
||||
},
|
||||
Description = "Scheduled repository update",
|
||||
CancelRightsType = RightsType.Repository,
|
||||
CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
|
||||
};
|
||||
|
||||
var repositoryUpdateJob = Job.Create(Api.Models.JobCode.RepositoryAutoUpdate, null, metadata, RepositoryRights.CancelPendingChanges);
|
||||
await jobManager.RegisterOperation(
|
||||
repositoryUpdateJob,
|
||||
RepositoryAutoUpdateJob,
|
||||
@@ -541,14 +531,7 @@ namespace Tgstation.Server.Host.Components
|
||||
}
|
||||
|
||||
// finally set up the job
|
||||
compileProcessJob = new Job
|
||||
{
|
||||
Instance = repositoryUpdateJob.Instance,
|
||||
Description = "Scheduled code deployment",
|
||||
CancelRightsType = RightsType.DreamMaker,
|
||||
CancelRight = (ulong)DreamMakerRights.CancelCompile,
|
||||
};
|
||||
|
||||
compileProcessJob = Job.Create(Api.Models.JobCode.AutomaticDeployment, null, metadata, DreamMakerRights.CancelCompile);
|
||||
await jobManager.RegisterOperation(
|
||||
compileProcessJob,
|
||||
(core, databaseContextFactory, job, progressReporter, jobCancellationToken) =>
|
||||
|
||||
@@ -351,10 +351,7 @@ namespace Tgstation.Server.Host.Components
|
||||
.Jobs
|
||||
.AsQueryable()
|
||||
.Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue)
|
||||
.Select(x => new Models.Job
|
||||
{
|
||||
Id = x.Id,
|
||||
})
|
||||
.Select(x => new Models.Job(x.Id.Value))
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var job in jobs)
|
||||
tasks.Add(jobService.CancelJob(job, user, true, cancellationToken));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Components.Byond;
|
||||
@@ -18,8 +17,13 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
sealed class InstanceWrapper : ReferenceCounter<IInstance>, IInstanceReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Static counter for <see cref="Uid"/>.
|
||||
/// </summary>
|
||||
static ulong instanceWrapperInstances;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Uid { get; }
|
||||
public ulong Uid { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRepositoryManager RepositoryManager => Instance.RepositoryManager;
|
||||
@@ -44,7 +48,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
public InstanceWrapper()
|
||||
{
|
||||
Uid = Guid.NewGuid();
|
||||
Uid = Interlocked.Increment(ref instanceWrapperInstances);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -689,10 +689,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();
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// </summary>
|
||||
static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>(
|
||||
Enum.GetValues(typeof(EventType))
|
||||
.OfType<EventType>()
|
||||
.Cast<EventType>()
|
||||
.Select(
|
||||
eventType => new KeyValuePair<EventType, string>(
|
||||
eventType,
|
||||
|
||||
@@ -29,11 +29,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
protected SwappableDmbProvider ActiveSwappable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="AdvancedWatchdog"/> pointing to the Game directory.
|
||||
/// </summary>
|
||||
protected IIOManager GameIOManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IFilesystemLinkFactory"/> for the <see cref="AdvancedWatchdog"/>.
|
||||
/// </summary>
|
||||
@@ -64,10 +59,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="diagnosticsIOManager">The 'Diagnostics' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="gameIOManager">The value of <see cref="GameIOManager"/>.</param>
|
||||
/// <param name="gameIOManager">The 'Game' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="linkFactory">The value of <see cref="LinkFactory"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="initialLaunchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
@@ -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<Task>();
|
||||
|
||||
@@ -50,9 +50,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="diagnosticsIOManager">The 'Diagnostics' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="gameIOManager">The 'Game' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="initialLaunchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
/// <param name="instance">The <see cref="Api.Models.Instance"/> for the <see cref="WatchdogBase"/>.</param>
|
||||
@@ -68,6 +69,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
IIOManager diagnosticsIOManager,
|
||||
IEventConsumer eventConsumer,
|
||||
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
|
||||
IIOManager gameIOManager,
|
||||
ILogger<BasicWatchdog> logger,
|
||||
DreamDaemonLaunchParameters initialLaunchParameters,
|
||||
Api.Models.Instance instance,
|
||||
@@ -83,6 +85,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
diagnosticsIOManager,
|
||||
eventConsumer,
|
||||
remoteDeploymentManagerFactory,
|
||||
gameIOManager,
|
||||
logger,
|
||||
initialLaunchParameters,
|
||||
instance,
|
||||
|
||||
@@ -96,6 +96,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
protected IAsyncDelayer AsyncDelayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/> pointing to the Game directory.
|
||||
/// </summary>
|
||||
protected IIOManager GameIOManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Instance"/> for the <see cref="WatchdogBase"/>.
|
||||
/// </summary>
|
||||
@@ -184,6 +189,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="diagnosticsIOManager">The value of <see cref="diagnosticsIOManager"/>.</param>
|
||||
/// <param name="eventConsumer">The value of <see cref="EventConsumer"/>.</param>
|
||||
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
|
||||
/// <param name="gameIOManager">The value of <see cref="GameIOManager"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
/// <param name="initialLaunchParameters">The initial value of <see cref="ActiveLaunchParameters"/>. May be modified.</param>
|
||||
/// <param name="metadata">The value of <see cref="metadata"/>.</param>
|
||||
@@ -199,6 +205,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
IIOManager diagnosticsIOManager,
|
||||
IEventConsumer eventConsumer,
|
||||
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
|
||||
IIOManager gameIOManager,
|
||||
ILogger<WatchdogBase> 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<string>
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -90,6 +90,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
diagnosticsIOManager,
|
||||
eventConsumer,
|
||||
remoteDeploymentManagerfactory,
|
||||
gameIOManager,
|
||||
LoggerFactory.CreateLogger<BasicWatchdog>(),
|
||||
settings,
|
||||
instance,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <see cref="AdministrationController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="gitHubServiceFactory">The value of <see cref="gitHubServiceFactory"/>.</param>
|
||||
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param>
|
||||
/// <param name="serverUpdateInitiator">The value of <see cref="serverUpdateInitiator"/>.</param>
|
||||
@@ -94,9 +96,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="fileLoggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="fileLoggingConfiguration"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
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<AdministrationController> logger,
|
||||
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions)
|
||||
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions,
|
||||
IApiHeadersProvider apiHeadersProvider)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
authenticationContext,
|
||||
apiHeadersProvider,
|
||||
logger,
|
||||
true)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Base <see cref="Controller"/> for API functions.
|
||||
/// </summary>
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ApiController]
|
||||
public abstract class ApiController : Controller
|
||||
public abstract class ApiController : ApiControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Default size of <see cref="Paginated{TModel}"/> results.
|
||||
@@ -51,7 +48,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// The <see cref="Api.ApiHeaders"/> for the operation.
|
||||
/// </summary>
|
||||
protected ApiHeaders ApiHeaders { get; private set; }
|
||||
protected ApiHeaders ApiHeaders => ApiHeadersProvider.ApiHeaders;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IApiHeadersProvider"/> containing value of <see cref="ApiHeaders"/>.
|
||||
/// </summary>
|
||||
protected IApiHeadersProvider ApiHeadersProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContext"/> for the operation.
|
||||
@@ -82,69 +84,43 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Initializes a new instance of the <see cref="ApiController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The value of <see cref="DatabaseContext"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The value of <see cref="ApiHeadersProvider"/>..</param>
|
||||
/// <param name="requireHeaders">The value of <see cref="requireHeaders"/>.</param>
|
||||
protected ApiController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
IApiHeadersProvider apiHeadersProvider,
|
||||
ILogger<ApiController> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
protected override async ValueTask<IActionResult> HookExecuteAction(Func<Task> 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
|
||||
|
||||
/// <summary>
|
||||
/// Generic 404 response.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="ObjectResult"/> with <see cref="HttpStatusCode.NotFound"/>.</returns>
|
||||
/// <returns>A <see cref="NotFoundObjectResult"/> with an appropriate <see cref="ErrorMessageResponse"/>.</returns>
|
||||
protected new NotFoundObjectResult NotFound() => NotFound(new ErrorMessageResponse(ErrorCode.ResourceNeverPresent));
|
||||
|
||||
/// <summary>
|
||||
/// Generic 401 response.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="ObjectResult"/> with <see cref="HttpStatusCode.NotFound"/>.</returns>
|
||||
protected new ObjectResult Unauthorized() => this.StatusCode(HttpStatusCode.Unauthorized, null);
|
||||
|
||||
/// <summary>
|
||||
/// Generic 501 response.
|
||||
/// </summary>
|
||||
@@ -264,27 +244,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Response for missing/Invalid headers.
|
||||
/// </summary>
|
||||
/// <param name="ignoreMissingAuth">Whether or not errors due to missing <see cref="HeaderNames.Authorization"/> should be thrown.</param>
|
||||
/// <param name="headersException">The <see cref="HeadersException"/> that occurred while trying to parse the <see cref="ApiHeaders"/>.</param>
|
||||
/// <returns>The appropriate <see cref="IActionResult"/>.</returns>
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all API style controllers.
|
||||
/// </summary>
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ApiController]
|
||||
public abstract class ApiControllerBase : Controller
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hook for executing a request.
|
||||
/// </summary>
|
||||
/// <param name="executeAction">A <see cref="Func{TResult}"/> 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-<see langword="null"/> value.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="IActionResult"/> that, if not <see langword="null"/>, is executed.</returns>
|
||||
protected virtual async ValueTask<IActionResult> HookExecuteAction(Func<Task> executeAction, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(executeAction);
|
||||
|
||||
await executeAction();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <see cref="Controller"/> for recieving DMAPI requests from DreamDaemon.
|
||||
/// </summary>
|
||||
[Route("/Bridge")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ApiController]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BridgeController : Controller
|
||||
public sealed class BridgeController : ApiControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// If the content of bridge requests and responses should be logged.
|
||||
@@ -76,6 +75,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async ValueTask<IActionResult> Process([FromQuery] string data, CancellationToken cancellationToken)
|
||||
{
|
||||
// Nothing to see here
|
||||
|
||||
@@ -13,12 +13,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
|
||||
{
|
||||
@@ -49,23 +51,26 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Initializes a new instance of the <see cref="ByondController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public ByondController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<ByondController> 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));
|
||||
@@ -190,14 +195,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
Instance.Id);
|
||||
|
||||
// run the install through the job manager
|
||||
var job = new Job
|
||||
{
|
||||
Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {version}",
|
||||
StartedBy = AuthenticationContext.User,
|
||||
CancelRightsType = RightsType.Byond,
|
||||
CancelRight = (ulong)ByondRights.CancelInstall,
|
||||
Instance = Instance,
|
||||
};
|
||||
var job = Job.Create(
|
||||
uploadingZip
|
||||
? JobCode.ByondCustomInstall
|
||||
: JobCode.ByondOfficialInstall,
|
||||
AuthenticationContext.User,
|
||||
Instance,
|
||||
ByondRights.CancelInstall);
|
||||
job.Description += $" {version}";
|
||||
|
||||
IFileUploadTicket fileUploadTicket = null;
|
||||
if (uploadingZip)
|
||||
@@ -299,14 +304,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var isCustomVersion = version.Build != -1;
|
||||
|
||||
// run the install through the job manager
|
||||
var job = new Job
|
||||
{
|
||||
Description = $"Delete installed BYOND version {version}",
|
||||
StartedBy = AuthenticationContext.User,
|
||||
CancelRightsType = RightsType.Byond,
|
||||
CancelRight = (ulong)(isCustomVersion ? ByondRights.InstallOfficialOrChangeActiveVersion : ByondRights.InstallCustomVersion),
|
||||
Instance = Instance,
|
||||
};
|
||||
var job = Job.Create(JobCode.ByondDelete, AuthenticationContext.User, Instance, isCustomVersion ? ByondRights.InstallOfficialOrChangeActiveVersion : ByondRights.InstallCustomVersion);
|
||||
job.Description += $" {version}";
|
||||
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
|
||||
@@ -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 <see cref="ChatController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public ChatController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<ChatController> logger,
|
||||
IInstanceManager instanceManager)
|
||||
IInstanceManager instanceManager,
|
||||
IApiHeadersProvider apiHeaders)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
authenticationContext,
|
||||
logger,
|
||||
instanceManager)
|
||||
instanceManager,
|
||||
apiHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -40,18 +40,21 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="useInstanceRequestHeader">The value of <see cref="useInstanceRequestHeader"/>.</param>
|
||||
protected ComponentInterfacingController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<ComponentInterfacingController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
bool useInstanceRequestHeader = false)
|
||||
IApiHeadersProvider apiHeaders,
|
||||
bool useInstanceRequestHeader)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
authenticationContext,
|
||||
apiHeaders,
|
||||
logger,
|
||||
true)
|
||||
{
|
||||
|
||||
@@ -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 <see cref="ConfigurationController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public ConfigurationController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<ConfigurationController> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,10 +146,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
AdditionalData = e.Message,
|
||||
});
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
return RequiresPosixSystemIdentity(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -191,11 +188,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
return new PaginatableResult<ConfigurationFileResponse>(result);
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
return new PaginatableResult<ConfigurationFileResponse>(
|
||||
RequiresPosixSystemIdentity(ex));
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return new PaginatableResult<ConfigurationFileResponse>(
|
||||
@@ -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();
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
/// <param name="appRoute">The value of the route.</param>
|
||||
/// <returns>The <see cref="VirtualFileResult"/> to use.</returns>
|
||||
[Route("{**appRoute}")]
|
||||
[Route("/{**appRoute}")]
|
||||
[HttpGet]
|
||||
public IActionResult Get([FromRoute] string appRoute)
|
||||
{
|
||||
|
||||
@@ -47,23 +47,26 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Initializes a new instance of the <see cref="DreamDaemonController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="IPortAllocator"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public DreamDaemonController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<DreamDaemonController> 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 DreamDaemon",
|
||||
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<IActionResult> 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<IActionResult> 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;
|
||||
|
||||
|
||||
@@ -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 <see cref="DreamMakerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="IPortAllocator"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public DreamMakerController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<DreamMakerController> 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<IActionResult> 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)
|
||||
|
||||
@@ -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 <see cref="HomeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="tokenFactory">The value of <see cref="tokenFactory"/>.</param>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
@@ -108,9 +107,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="controlPanelConfiguration"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
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<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
|
||||
ILogger<HomeController> logger)
|
||||
ILogger<HomeController> 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);
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IPortAllocator portAllocator;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPermissionsUpdateNotifyee"/> for the <see cref="InstanceController"/>.
|
||||
/// </summary>
|
||||
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceController"/>.
|
||||
/// </summary>
|
||||
@@ -81,36 +87,44 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Initializes a new instance of the <see cref="InstanceController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="IPortAllocator"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="portAllocator"/>.</param>
|
||||
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="swarmConfiguration"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
public InstanceController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<InstanceController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IJobManager jobManager,
|
||||
IIOManager ioManager,
|
||||
IPortAllocator portAllocator,
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions)
|
||||
IOptions<SwarmConfiguration> 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.ByondRights = RightsHelper.AllRights<ByondRights>();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IPermissionsUpdateNotifyee"/> for the <see cref="InstancePermissionSetController"/>.
|
||||
/// </summary>
|
||||
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstancePermissionSetController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public InstancePermissionSetController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<InstancePermissionSetController> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
@@ -107,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
|
||||
|
||||
@@ -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 <see cref="InstanceRequiredController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
protected InstanceRequiredController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<InstanceRequiredController> logger,
|
||||
IInstanceManager instanceManager)
|
||||
IInstanceManager instanceManager,
|
||||
IApiHeadersProvider apiHeaders)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
authenticationContext,
|
||||
logger,
|
||||
instanceManager,
|
||||
apiHeaders,
|
||||
true)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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 <see cref="JobController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public JobController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<JobController> 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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <see cref="RepositoryController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
public RepositoryController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ILogger<RepositoryController> 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,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
namespace Tgstation.Server.Host.Controllers.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Very similar to <see cref="FileStreamResult"/> except it's <see cref="IActionResultExecutor{TResult}"/> contains a fix for https://github.com/dotnet/aspnetcore/issues/28189.
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IActionResultExecutor{TResult}"/> for <see cref="LimitedStreamResult"/>s.
|
||||
+1
-1
@@ -3,7 +3,7 @@ using System.Linq;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
namespace Tgstation.Server.Host.Controllers.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper for returning paginated models.
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[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
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the current registration <see cref="Guid"/> from the <see cref="ControllerBase.Request"/>.
|
||||
@@ -211,7 +210,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
protected override async ValueTask<IActionResult> HookExecuteAction(Func<Task> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <see cref="TransferController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
public TransferController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
IFileTransferStreamHandler fileTransferService,
|
||||
ILogger<ApiController> logger)
|
||||
ILogger<ApiController> logger,
|
||||
IApiHeadersProvider apiHeaders)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
authenticationContext,
|
||||
apiHeaders,
|
||||
logger,
|
||||
true)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly ICryptographySuite cryptographySuite;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPermissionsUpdateNotifyee"/> for the <see cref="UserController"/>.
|
||||
/// </summary>
|
||||
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GeneralConfiguration"/> for the <see cref="UserController"/>.
|
||||
/// </summary>
|
||||
@@ -47,26 +54,32 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Initializes a new instance of the <see cref="UserController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
public UserController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ISystemIdentityFactory systemIdentityFactory,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
|
||||
ILogger<UserController> logger,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions)
|
||||
IOptions<GeneralConfiguration> 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
|
||||
|
||||
@@ -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 <see cref="UserGroupController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
public UserGroupController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IAuthenticationContext authenticationContext,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
ILogger<UserGroupController> logger)
|
||||
ILogger<UserGroupController> 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));
|
||||
}
|
||||
|
||||
@@ -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.Common.Http;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Components.Byond;
|
||||
@@ -39,6 +46,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;
|
||||
@@ -113,7 +121,7 @@ namespace Tgstation.Server.Host.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the <see cref="Application"/>'s services.
|
||||
/// Configure the <see cref="Application"/>'s <paramref name="services"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> needed for configuration.</param>
|
||||
@@ -215,50 +223,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<IClaimsInjector>()
|
||||
.InjectClaimsIntoContext(
|
||||
ctx,
|
||||
ctx.HttpContext.RequestAborted),
|
||||
};
|
||||
});
|
||||
// configure authentication pipeline
|
||||
ConfigureAuthenticationPipeline(services);
|
||||
|
||||
// add mvc, configure the json serializer settings
|
||||
var jsonVersionConverterList = new List<JsonConverter>
|
||||
{
|
||||
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<JsonConverter>
|
||||
{
|
||||
new VersionConverter(),
|
||||
};
|
||||
ConfigureNewtonsoftJsonSerializerSettingsForApi(options.SerializerSettings);
|
||||
});
|
||||
|
||||
services.AddSignalR(
|
||||
options =>
|
||||
{
|
||||
options.AddFilter<AuthorizationContextHubFilter>();
|
||||
})
|
||||
.AddNewtonsoftJsonProtocol(options =>
|
||||
{
|
||||
ConfigureNewtonsoftJsonSerializerSettingsForApi(options.PayloadSerializerSettings);
|
||||
});
|
||||
|
||||
services.AddHub<JobsHub, IJobsHub>();
|
||||
|
||||
if (postSetupServices.GeneralConfiguration.HostApiDocumentation)
|
||||
{
|
||||
string GetDocumentationFilePath(string assemblyLocation) => ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
|
||||
@@ -317,9 +323,7 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
|
||||
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
|
||||
|
||||
// configure security services
|
||||
services.AddScoped<IAuthenticationContextFactory, AuthenticationContextFactory>();
|
||||
services.AddScoped<IClaimsInjector, ClaimsInjector>();
|
||||
// configure other security services
|
||||
services.AddSingleton<IOAuthProviders, OAuthProviders>();
|
||||
services.AddSingleton<IIdentityCache, IdentityCache>();
|
||||
services.AddSingleton<ICryptographySuite, CryptographySuite>();
|
||||
@@ -395,8 +399,11 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddGitHub();
|
||||
|
||||
// configure root services
|
||||
services.AddSingleton<IJobService, JobService>();
|
||||
services.AddSingleton<JobService>();
|
||||
services.AddSingleton<IJobService>(provider => provider.GetRequiredService<JobService>());
|
||||
services.AddSingleton<IJobsHubUpdater>(provider => provider.GetRequiredService<JobService>());
|
||||
services.AddSingleton<IJobManager>(x => x.GetRequiredService<IJobService>());
|
||||
services.AddSingleton<IPermissionsUpdateNotifyee, JobsHubGroupMapper>();
|
||||
|
||||
services.AddSingleton<InstanceManager>();
|
||||
services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
|
||||
@@ -467,30 +474,6 @@ namespace Tgstation.Server.Host.Core
|
||||
logger.LogTrace("Swagger API generation enabled");
|
||||
}
|
||||
|
||||
// Set up CORS based on configuration if necessary
|
||||
Action<CorsPolicyBuilder> 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)
|
||||
{
|
||||
@@ -504,22 +487,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<CorsPolicyBuilder> 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<JobsHub>(
|
||||
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
|
||||
@@ -534,5 +563,60 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureHostedService(IServiceCollection services)
|
||||
=> services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
|
||||
|
||||
/// <summary>
|
||||
/// Configure the <paramref name="services"/> for the authentication pipeline.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
|
||||
void ConfigureAuthenticationPipeline(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
|
||||
services.AddScoped<AuthenticationContextFactory>();
|
||||
services.AddScoped<IAuthenticationContextFactory>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
|
||||
|
||||
// what if you
|
||||
// wanted to just do this:
|
||||
// return provider.GetRequiredService<AuthenticationContextFactory>().CurrentAuthenticationContext
|
||||
// But M$ said
|
||||
// https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs
|
||||
services.AddScoped(provider => provider
|
||||
.GetRequiredService<IHttpContextAccessor>()
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<AuthenticationContextFactory>()
|
||||
.CurrentAuthenticationContext);
|
||||
services.AddScoped<IClaimsTransformation, AuthenticationContextClaimsTransformation>();
|
||||
services.AddScoped<IAuthorizationFilter, AuthenticationContextAuthorizationFilter>();
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,22 +378,22 @@ namespace Tgstation.Server.Host.Database
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct MSSQL migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type MSLatestMigration = typeof(MSAddMapThreads);
|
||||
internal static readonly Type MSLatestMigration = typeof(MSAddJobCodes);
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct MYSQL migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type MYLatestMigration = typeof(MYAddMapThreads);
|
||||
internal static readonly Type MYLatestMigration = typeof(MYAddJobCodes);
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type PGLatestMigration = typeof(PGAddMapThreads);
|
||||
internal static readonly Type PGLatestMigration = typeof(PGAddJobCodes);
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct SQLite migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type SLLatestMigration = typeof(SLAddMapThreads);
|
||||
internal static readonly Type SLLatestMigration = typeof(SLAddJobCodes);
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502 // Cyclomatic complexity
|
||||
@@ -422,6 +422,15 @@ namespace Tgstation.Server.Host.Database
|
||||
|
||||
string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
|
||||
|
||||
if (targetVersion < new Version(5, 17, 0))
|
||||
targetMigration = currentDatabaseType switch
|
||||
{
|
||||
DatabaseType.MySql => nameof(MYAddMapThreads),
|
||||
DatabaseType.PostgresSql => nameof(PGAddMapThreads),
|
||||
DatabaseType.SqlServer => nameof(MSAddMapThreads),
|
||||
DatabaseType.Sqlite => nameof(SLAddMapThreads),
|
||||
_ => BadDatabaseType(),
|
||||
};
|
||||
if (targetVersion < new Version(5, 13, 0))
|
||||
targetMigration = currentDatabaseType switch
|
||||
{
|
||||
|
||||
+1075
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MSAddJobCodes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "JobCode",
|
||||
table: "Jobs",
|
||||
type: "tinyint",
|
||||
nullable: false,
|
||||
defaultValue: (byte)0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JobCode",
|
||||
table: "Jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MYAddJobCodes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "JobCode",
|
||||
table: "Jobs",
|
||||
type: "tinyint unsigned",
|
||||
nullable: false,
|
||||
defaultValue: (byte)0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JobCode",
|
||||
table: "Jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1069
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PGAddJobCodes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "JobCode",
|
||||
table: "Jobs",
|
||||
type: "smallint",
|
||||
nullable: false,
|
||||
defaultValue: (byte)0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JobCode",
|
||||
table: "Jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SLAddJobCodes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "JobCode",
|
||||
table: "Jobs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: (byte)0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JobCode",
|
||||
table: "Jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,11 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
[DbContext(typeof(MySqlDatabaseContext))]
|
||||
partial class MySqlDatabaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.7")
|
||||
.HasAnnotation("ProductVersion", "7.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
@@ -416,6 +415,9 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<byte>("JobCode")
|
||||
.HasColumnType("tinyint unsigned");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
+4
-2
@@ -11,12 +11,11 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
[DbContext(typeof(PostgresSqlDatabaseContext))]
|
||||
partial class PostgresSqlDatabaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.7")
|
||||
.HasAnnotation("ProductVersion", "7.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -396,6 +395,9 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<byte>("JobCode")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
+4
-2
@@ -11,12 +11,11 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
[DbContext(typeof(SqlServerDatabaseContext))]
|
||||
partial class SqlServerDatabaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.7")
|
||||
.HasAnnotation("ProductVersion", "7.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
@@ -399,6 +398,9 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<byte>("JobCode")
|
||||
.HasColumnType("tinyint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
@@ -11,11 +11,10 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
[DbContext(typeof(SqliteDatabaseContext))]
|
||||
partial class SqliteDatabaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
@@ -386,6 +385,9 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("JobCode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -8,8 +8,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using Serilog.Context;
|
||||
|
||||
@@ -66,20 +64,6 @@ namespace Tgstation.Server.Host.Extensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suppress any client side caching of API calls.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure.</param>
|
||||
public static void UseDisabledClientCache(this IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(applicationBuilder);
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
context.Response.Headers.Add(HeaderNames.CacheControl, new StringValues("no-cache"));
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suppress <see cref="global::System.Threading.Tasks.TaskCanceledException"/> warnings when a user aborts a request.
|
||||
/// </summary>
|
||||
@@ -135,6 +119,35 @@ namespace Tgstation.Server.Host.Extensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that the API version is the current major version if it's present in the headers.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure.</param>
|
||||
public static void UseApiCompatibility(this IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(applicationBuilder);
|
||||
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
var apiHeadersProvider = context.RequestServices.GetRequiredService<IApiHeadersProvider>();
|
||||
if (apiHeadersProvider.ApiHeaders?.Compatible() == false)
|
||||
{
|
||||
await new JsonResult(
|
||||
new ErrorMessageResponse(ErrorCode.ApiMismatch))
|
||||
{
|
||||
StatusCode = (int)HttpStatusCode.UpgradeRequired,
|
||||
}
|
||||
.ExecuteResultAsync(new ActionContext
|
||||
{
|
||||
HttpContext = context,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the X-Powered-By response header.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.Net.Http.Headers;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Controllers;
|
||||
using Tgstation.Server.Host.Controllers.Results;
|
||||
using Tgstation.Server.Host.Transfer;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
|
||||
@@ -16,6 +16,7 @@ using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
using Tgstation.Server.Host.Utils.GitHub;
|
||||
using Tgstation.Server.Host.Utils.SignalR;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
@@ -220,6 +221,23 @@ namespace Tgstation.Server.Host.Extensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to add the given <typeparamref name="THub"/> to services.
|
||||
/// </summary>
|
||||
/// <typeparam name="THub">The <see cref="Type"/> of the <see cref="Microsoft.AspNetCore.SignalR.Hub{T}"/> being added.</typeparam>
|
||||
/// <typeparam name="THubMethods">The implementation <see cref="Type"/> of the <typeparamref name="THub"/>.</typeparam>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the <typeparamref name="THub"/> to.</param>
|
||||
public static void AddHub<THub, THubMethods>(this IServiceCollection services)
|
||||
where THub : ConnectionMappingHub<THub, THubMethods>
|
||||
where THubMethods : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.TryAddSingleton(typeof(ComprehensiveHubContext<,>));
|
||||
services.AddSingleton<IConnectionMappedHubContext<THub, THubMethods>>(provider => provider.GetRequiredService<ComprehensiveHubContext<THub, THubMethods>>());
|
||||
services.AddSingleton<IHubConnectionMapper<THub, THubMethods>>(provider => provider.GetRequiredService<ComprehensiveHubContext<THub, THubMethods>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the modifiable services to their default types.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows manually triggering jobs hub updates.
|
||||
/// </summary>
|
||||
interface IJobsHubUpdater
|
||||
{
|
||||
/// <summary>
|
||||
/// Queue a message to be sent to all clients with the current state of active jobs.
|
||||
/// </summary>
|
||||
void QueueActiveJobUpdates();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
using Tgstation.Server.Host.Components;
|
||||
@@ -14,12 +19,23 @@ using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
using Tgstation.Server.Host.Utils.SignalR;
|
||||
|
||||
namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
/// <inheritdoc cref="IJobService" />
|
||||
sealed class JobService : IJobService, IDisposable
|
||||
sealed class JobService : IJobService, IJobsHubUpdater, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum rate at which hub clients can receive updates.
|
||||
/// </summary>
|
||||
const int MaxHubUpdatesPerSecond = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IHubContext"/> for the <see cref="JobsHub"/>.
|
||||
/// </summary>
|
||||
readonly IConnectionMappedHubContext<JobsHub, IJobsHub> hub;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IServiceProvider"/> for the <see cref="JobService"/>.
|
||||
/// </summary>
|
||||
@@ -40,6 +56,11 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// </summary>
|
||||
readonly Dictionary<long, JobHandler> jobs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Dictionary{TKey, TValue}"/> of running <see cref="Job"/> <see cref="Api.Models.EntityId.Id"/>s to <see cref="Action"/>s that will push immediate <see cref="hub"/> updates.
|
||||
/// </summary>
|
||||
readonly Dictionary<long, Action> hubUpdateActions;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TaskCompletionSource{TResult}"/> to delay starting jobs until the server is ready.
|
||||
/// </summary>
|
||||
@@ -63,18 +84,23 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hub">The value of <see cref="hub"/>.</param>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public JobService(
|
||||
IConnectionMappedHubContext<JobsHub, IJobsHub> hub,
|
||||
IDatabaseContextFactory databaseContextFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<JobService> logger)
|
||||
{
|
||||
this.hub = hub ?? throw new ArgumentNullException(nameof(hub));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
jobs = new Dictionary<long, JobHandler>();
|
||||
hubUpdateActions = new Dictionary<long, Action>();
|
||||
activationTcs = new TaskCompletionSource<IInstanceCoreProvider>();
|
||||
synchronizationLock = new object();
|
||||
addCancelLock = new object();
|
||||
@@ -100,7 +126,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
|
||||
job.Instance = new Models.Instance
|
||||
{
|
||||
Id = job.Instance.Id,
|
||||
Id = job.Instance.Id.Value,
|
||||
};
|
||||
databaseContext.Instances.Attach(job.Instance);
|
||||
|
||||
@@ -111,7 +137,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
else
|
||||
job.StartedBy = new User
|
||||
{
|
||||
Id = job.StartedBy.Id,
|
||||
Id = job.StartedBy.Id ?? throw new InvalidOperationException("StartedBy User associated with job does not have an Id!"),
|
||||
};
|
||||
databaseContext.Users.Attach(job.StartedBy);
|
||||
|
||||
@@ -152,14 +178,14 @@ namespace Tgstation.Server.Host.Jobs
|
||||
.Jobs
|
||||
.AsQueryable()
|
||||
.Where(y => !y.StoppedAt.HasValue)
|
||||
.Select(y => y.Id)
|
||||
.Select(y => y.Id.Value)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (badJobIds.Count > 0)
|
||||
{
|
||||
logger.LogTrace("Cleaning {unfinishedJobCount} unfinished jobs...", badJobIds.Count);
|
||||
foreach (var badJobId in badJobIds)
|
||||
{
|
||||
var job = new Job { Id = badJobId };
|
||||
var job = new Job(badJobId);
|
||||
databaseContext.Jobs.Attach(job);
|
||||
job.Cancelled = true;
|
||||
job.StoppedAt = DateTimeOffset.UtcNow;
|
||||
@@ -181,10 +207,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
noMoreJobsShouldStart = true;
|
||||
joinTasks = jobs.Select(x => CancelJob(
|
||||
new Job
|
||||
{
|
||||
Id = x.Key,
|
||||
},
|
||||
new Job(x.Key),
|
||||
null,
|
||||
true,
|
||||
cancellationToken))
|
||||
@@ -214,7 +237,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
user ??= await databaseContext.Users.GetTgsUser(cancellationToken);
|
||||
|
||||
var updatedJob = new Job { Id = job.Id };
|
||||
var updatedJob = new Job(job.Id.Value);
|
||||
databaseContext.Jobs.Attach(updatedJob);
|
||||
var attachedUser = new User { Id = user.Id };
|
||||
databaseContext.Users.Attach(attachedUser);
|
||||
@@ -269,13 +292,12 @@ namespace Tgstation.Server.Host.Jobs
|
||||
if (noMoreJobsShouldStart && !handler.Started)
|
||||
await Extensions.TaskExtensions.InfiniteTask.WaitAsync(cancellationToken);
|
||||
|
||||
ValueTask<Job>? cancelTask = null;
|
||||
var cancelTask = ValueTask.FromResult<Job>(null);
|
||||
bool result;
|
||||
using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
|
||||
result = await handler.Wait(cancellationToken);
|
||||
|
||||
if (cancelTask.HasValue)
|
||||
await cancelTask.Value;
|
||||
await cancelTask;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -289,26 +311,83 @@ namespace Tgstation.Server.Host.Jobs
|
||||
activationTcs.SetResult(instanceCoreProvider);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void QueueActiveJobUpdates()
|
||||
{
|
||||
lock (hubUpdateActions)
|
||||
foreach (var action in hubUpdateActions.Values)
|
||||
action();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runner for <see cref="JobHandler"/>s.
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="Job"/> being run.</param>
|
||||
/// <param name="job">The <see cref="Job"/> being run. Must be fully populated.</param>
|
||||
/// <param name="operation">The <see cref="JobEntrypoint"/> for the <paramref name="job"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
async Task<bool> RunJob(Job job, JobEntrypoint operation, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1506
|
||||
{
|
||||
using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, job.Id))
|
||||
try
|
||||
{
|
||||
void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", job.Id);
|
||||
|
||||
var hubUpdatesTask = Task.CompletedTask;
|
||||
var result = false;
|
||||
|
||||
Stopwatch stopwatch = null;
|
||||
void QueueHubUpdate(JobResponse update, bool final)
|
||||
{
|
||||
void NextUpdate(bool bypassRate)
|
||||
{
|
||||
var currentUpdatesTask = hubUpdatesTask;
|
||||
async Task ChainHubUpdate()
|
||||
{
|
||||
await currentUpdatesTask;
|
||||
|
||||
// DCT: Cancellation token is for job, operation should always run
|
||||
await hub
|
||||
.Clients
|
||||
.Group(JobsHub.HubGroupName(job))
|
||||
.ReceiveJobUpdate(update, CancellationToken.None);
|
||||
}
|
||||
|
||||
Stopwatch enteredLock = null;
|
||||
try
|
||||
{
|
||||
if (!bypassRate && stopwatch != null)
|
||||
{
|
||||
Monitor.Enter(stopwatch);
|
||||
enteredLock = stopwatch;
|
||||
if (stopwatch.ElapsedMilliseconds * MaxHubUpdatesPerSecond < 1)
|
||||
return; // don't spam client
|
||||
}
|
||||
|
||||
hubUpdatesTask = ChainHubUpdate();
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (enteredLock != null)
|
||||
Monitor.Exit(enteredLock);
|
||||
}
|
||||
}
|
||||
|
||||
var jobId = update.Id.Value;
|
||||
lock (hubUpdateActions)
|
||||
if (final)
|
||||
hubUpdateActions.Remove(jobId);
|
||||
else
|
||||
hubUpdateActions[jobId] = () => NextUpdate(true);
|
||||
|
||||
NextUpdate(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldJob = job;
|
||||
job = new Job { Id = oldJob.Id };
|
||||
|
||||
void UpdateProgress(string stage, double? progress)
|
||||
{
|
||||
if (progress.HasValue
|
||||
@@ -319,19 +398,26 @@ namespace Tgstation.Server.Host.Jobs
|
||||
return;
|
||||
}
|
||||
|
||||
int? newProgress = progress.HasValue ? (int)Math.Floor(progress.Value * 100) : null;
|
||||
lock (synchronizationLock)
|
||||
if (jobs.TryGetValue(oldJob.Id.Value, out var handler))
|
||||
if (jobs.TryGetValue(job.Id.Value, out var handler))
|
||||
{
|
||||
handler.Stage = stage;
|
||||
handler.Progress = progress.HasValue ? (int)Math.Floor(progress.Value * 100) : null;
|
||||
handler.Progress = newProgress;
|
||||
|
||||
var updatedJob = job.ToApi();
|
||||
updatedJob.Stage = stage;
|
||||
updatedJob.Progress = newProgress;
|
||||
QueueHubUpdate(updatedJob, false);
|
||||
}
|
||||
}
|
||||
|
||||
var instanceCoreProvider = await activationTcs.Task.WaitAsync(cancellationToken);
|
||||
QueueHubUpdate(job.ToApi(), false);
|
||||
|
||||
logger.LogTrace("Starting job...");
|
||||
await operation(
|
||||
instanceCoreProvider.GetInstance(oldJob.Instance),
|
||||
instanceCoreProvider.GetInstance(job.Instance),
|
||||
databaseContextFactory,
|
||||
job,
|
||||
new JobProgressReporter(
|
||||
@@ -360,22 +446,54 @@ namespace Tgstation.Server.Host.Jobs
|
||||
LogException(e);
|
||||
}
|
||||
|
||||
await databaseContextFactory.UseContext(async databaseContext =>
|
||||
try
|
||||
{
|
||||
var attachedJob = new Job
|
||||
await databaseContextFactory.UseContext(async databaseContext =>
|
||||
{
|
||||
Id = job.Id,
|
||||
};
|
||||
var attachedJob = new Job(job.Id.Value);
|
||||
|
||||
databaseContext.Jobs.Attach(attachedJob);
|
||||
attachedJob.StoppedAt = DateTimeOffset.UtcNow;
|
||||
attachedJob.ExceptionDetails = job.ExceptionDetails;
|
||||
attachedJob.ErrorCode = job.ErrorCode;
|
||||
attachedJob.Cancelled = job.Cancelled;
|
||||
databaseContext.Jobs.Attach(attachedJob);
|
||||
attachedJob.StoppedAt = DateTimeOffset.UtcNow;
|
||||
attachedJob.ExceptionDetails = job.ExceptionDetails;
|
||||
attachedJob.ErrorCode = job.ErrorCode;
|
||||
attachedJob.Cancelled = job.Cancelled;
|
||||
|
||||
// DCT: Cancellation token is for job, operation should always run
|
||||
await databaseContext.Save(CancellationToken.None);
|
||||
});
|
||||
// DCT: Cancellation token is for job, operation should always run
|
||||
await databaseContext.Save(CancellationToken.None);
|
||||
});
|
||||
|
||||
// Resetting the context here because I CBA to worry if the cache is being used
|
||||
await databaseContextFactory.UseContext(async databaseContext =>
|
||||
{
|
||||
// Cancellation might be set in another async context, forced to reload here for the final hub update
|
||||
// DCT: Cancellation token is for job, operation should always run
|
||||
var finalJob = await databaseContext
|
||||
.Jobs
|
||||
.AsQueryable()
|
||||
.Include(x => x.Instance)
|
||||
.Include(x => x.StartedBy)
|
||||
.Include(x => x.CancelledBy)
|
||||
.Where(dbJob => dbJob.Id == job.Id.Value)
|
||||
.FirstAsync(CancellationToken.None);
|
||||
QueueHubUpdate(finalJob.ToApi(), true);
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (hubUpdateActions)
|
||||
hubUpdateActions.Remove(job.Id.Value);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await hubUpdatesTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in hub updates chain task!");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils.SignalR;
|
||||
|
||||
namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// A SignalR <see cref="Hub"/> for pushing job updates.
|
||||
/// </summary>
|
||||
sealed class JobsHub : ConnectionMappingHub<JobsHub, IJobsHub>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the group name for a given <paramref name="instanceId"/>.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">The <see cref="Instance"/> <see cref="Api.Models.EntityId.Id"/>.</param>
|
||||
/// <returns>The name of the group for the <paramref name="instanceId"/>.</returns>
|
||||
public static string HubGroupName(long instanceId)
|
||||
=> $"instance-{instanceId}";
|
||||
|
||||
/// <summary>
|
||||
/// Get the group name for a given <paramref name="job"/>.
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="Job"/>.</param>
|
||||
/// <returns>The name of the group for the <paramref name="job"/>.</returns>
|
||||
public static string HubGroupName(Job job)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
|
||||
if (job.Instance == null)
|
||||
throw new InvalidOperationException("job.Instance was null!");
|
||||
|
||||
return HubGroupName(job.Instance.Id.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobsHub"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connectionMapper">The <see cref="IHubConnectionMapper{THub, THubMethods}"/> for the <see cref="ConnectionMappingHub{TChildHub, THubMethods}"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ConnectionMappingHub{TChildHub, THubMethods}"/>.</param>
|
||||
public JobsHub(
|
||||
IHubConnectionMapper<JobsHub, IJobsHub> connectionMapper,
|
||||
IAuthenticationContext authenticationContext)
|
||||
: base(connectionMapper, authenticationContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils.SignalR;
|
||||
|
||||
namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles mapping groups for the <see cref="JobsHub"/>.
|
||||
/// </summary>
|
||||
sealed class JobsHubGroupMapper : IPermissionsUpdateNotifyee
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IHubContext"/> for the <see cref="JobsHub"/>.
|
||||
/// </summary>
|
||||
readonly IConnectionMappedHubContext<JobsHub, IJobsHub> hub;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="JobService"/>.
|
||||
/// </summary>
|
||||
readonly IDatabaseContextFactory databaseContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IJobsHubUpdater"/> for the <see cref="JobService"/>.
|
||||
/// </summary>
|
||||
readonly IJobsHubUpdater jobsHubUpdater;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="JobService"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<JobsHubGroupMapper> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobsHubGroupMapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hub">The value of <see cref="hub"/>.</param>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/>.</param>
|
||||
/// <param name="jobsHubUpdater">The value of <see cref="jobsHubUpdater"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public JobsHubGroupMapper(
|
||||
IConnectionMappedHubContext<JobsHub, IJobsHub> hub,
|
||||
IDatabaseContextFactory databaseContextFactory,
|
||||
IJobsHubUpdater jobsHubUpdater,
|
||||
ILogger<JobsHubGroupMapper> logger)
|
||||
{
|
||||
this.hub = hub ?? throw new ArgumentNullException(nameof(hub));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.jobsHubUpdater = jobsHubUpdater ?? throw new ArgumentNullException(nameof(jobsHubUpdater));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
hub.OnConnectionMapGroups += MapConnectionGroups;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask InstancePermissionSetCreated(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(instancePermissionSet);
|
||||
var permissionSetId = instancePermissionSet.PermissionSet.Id ?? instancePermissionSet.PermissionSetId;
|
||||
|
||||
logger.LogTrace("InstancePermissionSetCreated");
|
||||
return RefreshHubGroups(
|
||||
permissionSetId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask UserDisabled(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
if (!user.Id.HasValue)
|
||||
throw new InvalidOperationException("user.Id was null!");
|
||||
|
||||
logger.LogTrace("UserDisabled");
|
||||
hub.AbortUnauthedConnections(user);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask InstancePermissionSetDeleted(PermissionSet permissionSet, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(permissionSet);
|
||||
logger.LogTrace("InstancePermissionSetDeleted");
|
||||
return RefreshHubGroups(
|
||||
permissionSet.Id ?? throw new InvalidOperationException("permissionSet?.Id was null!"),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IConnectionMappedHubContext{THub, THubMethods}.OnConnectionMapGroups"/>.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to map the groups for.</param>
|
||||
/// <param name="mappingFunc">The <see cref="Func{T, TResult}"/> taking the mapped group names as an <see cref="IEnumerable{T}"/> of <see cref="string"/> resulting in a <see cref="ValueTask"/> to be <see langword="await"/>ed.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="IEnumerable{T}"/> of the <see cref="JobsHub"/> group names the user belongs in.</returns>
|
||||
async ValueTask MapConnectionGroups(
|
||||
IAuthenticationContext authenticationContext,
|
||||
Func<IEnumerable<string>, Task> mappingFunc,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authenticationContext);
|
||||
|
||||
List<long> permedInstanceIds = null;
|
||||
await databaseContextFactory.UseContext(
|
||||
async databaseContext =>
|
||||
permedInstanceIds = await databaseContext
|
||||
.InstancePermissionSets
|
||||
.AsQueryable()
|
||||
.Where(ips => ips.PermissionSetId == authenticationContext.PermissionSet.Id.Value)
|
||||
.Select(ips => ips.Id)
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
await mappingFunc(
|
||||
permedInstanceIds.Select(
|
||||
JobsHub.HubGroupName));
|
||||
|
||||
jobsHubUpdater.QueueActiveJobUpdates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the <see cref="hub"/> <see cref="Hub.Groups"/> for clients associated with a given <paramref name="permissionSetId"/>.
|
||||
/// </summary>
|
||||
/// <param name="permissionSetId">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="PermissionSet"/> who's users need updating.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask RefreshHubGroups(long permissionSetId, CancellationToken cancellationToken)
|
||||
=> databaseContextFactory.UseContext(
|
||||
async databaseContext =>
|
||||
{
|
||||
logger.LogTrace("RefreshHubGroups");
|
||||
var permissionSetUsers = await databaseContext
|
||||
.Users
|
||||
.Where(x => x.PermissionSet.Id == permissionSetId)
|
||||
.ToListAsync(cancellationToken);
|
||||
var allInstanceIds = await databaseContext
|
||||
.Instances
|
||||
.Select(
|
||||
instance => instance.Id.Value)
|
||||
.ToListAsync(cancellationToken);
|
||||
var permissionSetAccessibleInstanceIds = await databaseContext
|
||||
.InstancePermissionSets
|
||||
.AsQueryable()
|
||||
.Where(ips => ips.PermissionSetId == permissionSetId)
|
||||
.Select(ips => ips.InstanceId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var groupsToRemove = allInstanceIds
|
||||
.Except(permissionSetAccessibleInstanceIds)
|
||||
.Select(JobsHub.HubGroupName);
|
||||
|
||||
var groupsToAdd = permissionSetAccessibleInstanceIds
|
||||
.Select(JobsHub.HubGroupName);
|
||||
|
||||
var connectionIds = permissionSetUsers
|
||||
.SelectMany(user => hub.UserConnectionIds(user))
|
||||
.ToList();
|
||||
|
||||
logger.LogTrace(
|
||||
"Updating groups for the {connectionCount} hub connections of permission set {permissionSetId}. They may access {allowed}/{total} instances.",
|
||||
connectionIds.Count,
|
||||
permissionSetId,
|
||||
permissionSetAccessibleInstanceIds.Count,
|
||||
allInstanceIds.Count);
|
||||
|
||||
var removeTasks = connectionIds
|
||||
.SelectMany(connectionId => groupsToRemove
|
||||
.Select(groupName => hub
|
||||
.Groups
|
||||
.RemoveFromGroupAsync(connectionId, groupName, cancellationToken)));
|
||||
|
||||
var addTasks = connectionIds
|
||||
.SelectMany(connectionId => groupsToAdd
|
||||
.Select(groupName => hub
|
||||
.Groups
|
||||
.AddToGroupAsync(connectionId, groupName, cancellationToken)));
|
||||
|
||||
// Checked internally, the default implementations for these tasks complete synchronously
|
||||
// https://github.com/dotnet/aspnetcore/blob/ce330d9d12f7676ff35c2223bd8a3b1e252a4e86/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs#L34-L70
|
||||
await Task.WhenAll(removeTasks.Concat(addTasks));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ namespace Tgstation.Server.Host.Models
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CompileJobResponse ToApi() => new CompileJobResponse
|
||||
public CompileJobResponse ToApi() => new ()
|
||||
{
|
||||
DirectoryName = DirectoryName,
|
||||
DmeName = DmeName,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
@@ -26,10 +31,89 @@ namespace Tgstation.Server.Host.Models
|
||||
[Required]
|
||||
public Instance Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new job for registering in the <see cref="Jobs.IJobService"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRight">The <see cref="RightsType"/> of <paramref name="cancelRight"/>.</typeparam>
|
||||
/// <param name="code">The value of <see cref="Api.Models.Internal.Job.JobCode"/>. <see cref="Api.Models.Internal.Job.Description"/> will be derived from this.</param>
|
||||
/// <param name="startedBy">The value of <see cref="StartedBy"/>. If <see langword="null"/>, the <see cref="User.TgsSystemUserName"/> user will be used.</param>
|
||||
/// <param name="instance">The <see cref="Api.Models.Instance"/> used to generate the value of <see cref="Instance"/>.</param>
|
||||
/// <param name="cancelRight">The value of <see cref="Api.Models.Internal.Job.CancelRight"/>. <see cref="Api.Models.Internal.Job.CancelRightsType"/> will be derived from this.</param>
|
||||
/// <returns>A new <see cref="Job"/> ready to be registered with the <see cref="Jobs.IJobService"/>.</returns>
|
||||
public static Job Create<TRight>(JobCode code, User startedBy, Api.Models.Instance instance, TRight cancelRight)
|
||||
where TRight : Enum
|
||||
=> new (
|
||||
code,
|
||||
startedBy,
|
||||
instance,
|
||||
RightsHelper.TypeToRight<TRight>(),
|
||||
(ulong)(object)cancelRight);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new job for registering in the <see cref="Jobs.IJobService"/>.
|
||||
/// </summary>
|
||||
/// <param name="code">The value of <see cref="Api.Models.Internal.Job.JobCode"/>. <see cref="Api.Models.Internal.Job.Description"/> will be derived from this.</param>
|
||||
/// <param name="startedBy">The value of <see cref="StartedBy"/>. If <see langword="null"/>, the <see cref="User.TgsSystemUserName"/> user will be used.</param>
|
||||
/// <param name="instance">The <see cref="Api.Models.Instance"/> used to generate the value of <see cref="Instance"/>.</param>
|
||||
/// <returns>A new <see cref="Job"/> ready to be registered with the <see cref="Jobs.IJobService"/>.</returns>
|
||||
public static Job Create(JobCode code, User startedBy, Api.Models.Instance instance)
|
||||
=> new (
|
||||
code,
|
||||
startedBy,
|
||||
instance,
|
||||
null,
|
||||
null);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Job"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("For use by EFCore only", true)]
|
||||
public Job()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Job"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The value of <see cref="EntityId.Id"/>.</param>
|
||||
public Job(long id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Job"/> class.
|
||||
/// </summary>
|
||||
/// <param name="code">The value of <see cref="Api.Models.Internal.Job.JobCode"/>.</param>
|
||||
/// <param name="startedBy">The value of <see cref="StartedBy"/>.</param>
|
||||
/// <param name="instance">The value of <see cref="Instance"/>.</param>
|
||||
/// <param name="cancelRightsType">The value of <see cref="Api.Models.Internal.Job.CancelRightsType"/>.</param>
|
||||
/// <param name="cancelRight">The value of <see cref="Api.Models.Internal.Job.CancelRight"/>.</param>
|
||||
Job(JobCode code, User startedBy, Api.Models.Instance instance, RightsType? cancelRightsType, ulong? cancelRight)
|
||||
{
|
||||
StartedBy = startedBy;
|
||||
ArgumentNullException.ThrowIfNull(instance);
|
||||
Instance = new Instance
|
||||
{
|
||||
Id = instance.Id ?? throw new InvalidOperationException("Instance associated with job does not have an Id!"),
|
||||
};
|
||||
Description = typeof(JobCode)
|
||||
.GetField(code.ToString())
|
||||
.GetCustomAttributes(false)
|
||||
.OfType<DescriptionAttribute>()
|
||||
.First()
|
||||
.Description;
|
||||
JobCode = code;
|
||||
CancelRight = cancelRight;
|
||||
CancelRightsType = cancelRightsType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public JobResponse ToApi() => new ()
|
||||
{
|
||||
Id = Id,
|
||||
JobCode = JobCode.Value,
|
||||
InstanceId = Instance.Id.Value,
|
||||
StartedAt = StartedAt,
|
||||
StoppedAt = StoppedAt,
|
||||
Cancelled = Cancelled,
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Models
|
||||
/// </summary>
|
||||
/// <param name="showUsers">If <see cref="UserGroupResponse.Users"/> should be populated.</param>
|
||||
/// <returns>A new <see cref="UserGroupResponse"/>.</returns>
|
||||
public UserGroupResponse ToApi(bool showUsers) => new UserGroupResponse
|
||||
public UserGroupResponse ToApi(bool showUsers) => new ()
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name,
|
||||
|
||||
@@ -7,19 +7,22 @@ using Tgstation.Server.Host.Models;
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AuthenticationContext : IAuthenticationContext
|
||||
sealed class AuthenticationContext : IAuthenticationContext, IDisposable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public User User { get; }
|
||||
public bool Valid { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public PermissionSet PermissionSet { get; }
|
||||
public User User { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public InstancePermissionSet InstancePermissionSet { get; }
|
||||
public PermissionSet PermissionSet { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISystemIdentity SystemIdentity { get; }
|
||||
public InstancePermissionSet InstancePermissionSet { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISystemIdentity SystemIdentity { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationContext"/> class.
|
||||
@@ -28,13 +31,16 @@ namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => SystemIdentity?.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationContext"/> class.
|
||||
/// Initializes the <see cref="AuthenticationContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="systemIdentity">The value of <see cref="SystemIdentity"/>.</param>
|
||||
/// <param name="user">The value of <see cref="User"/>.</param>
|
||||
/// <param name="instanceUser">The value of <see cref="InstancePermissionSet"/>.</param>
|
||||
public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstancePermissionSet instanceUser)
|
||||
public void Initialize(ISystemIdentity systemIdentity, User user, InstancePermissionSet instanceUser)
|
||||
{
|
||||
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||
if (systemIdentity == null && User.SystemIdentifier != null)
|
||||
@@ -44,10 +50,9 @@ namespace Tgstation.Server.Host.Security
|
||||
?? throw new ArgumentException("No PermissionSet provider", nameof(user));
|
||||
InstancePermissionSet = instanceUser;
|
||||
SystemIdentity = systemIdentity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => SystemIdentity?.Dispose();
|
||||
Valid = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong GetRight(RightsType rightsType)
|
||||
@@ -69,10 +74,15 @@ namespace Tgstation.Server.Host.Security
|
||||
|
||||
var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType).First();
|
||||
|
||||
var right = prop.GetMethod.Invoke(isInstance ? InstancePermissionSet : PermissionSet, Array.Empty<object>());
|
||||
var right = prop.GetMethod.Invoke(
|
||||
isInstance
|
||||
? InstancePermissionSet
|
||||
: PermissionSet,
|
||||
Array.Empty<object>());
|
||||
|
||||
if (right == null)
|
||||
throw new InvalidOperationException("A user right was null!");
|
||||
|
||||
return (ulong)right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IAuthorizationFilter"/> that maps <see cref="Claim"/>s using an <see cref="IAuthenticationContext"/>.
|
||||
/// </summary>
|
||||
sealed class AuthenticationContextAuthorizationFilter : IAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthenticationContext"/> for the <see cref="AuthenticationContextAuthorizationFilter"/>.
|
||||
/// </summary>
|
||||
readonly IAuthenticationContext authenticationContext;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="AuthenticationContextAuthorizationFilter"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<AuthenticationContextAuthorizationFilter> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationContextAuthorizationFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The value of <see cref="authenticationContext"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public AuthenticationContextAuthorizationFilter(IAuthenticationContext authenticationContext, ILogger<AuthenticationContextAuthorizationFilter> logger)
|
||||
{
|
||||
this.authenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
if (!authenticationContext.Valid)
|
||||
{
|
||||
logger.LogTrace("authenticationContext is invalid!");
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (authenticationContext.User.Enabled.Value)
|
||||
return;
|
||||
|
||||
logger.LogTrace("authenticationContext is for a disabled user!");
|
||||
context.Result = new ForbidResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IClaimsTransformation"/> that maps <see cref="Claim"/>s using an <see cref="IAuthenticationContext"/>.
|
||||
/// </summary>
|
||||
sealed class AuthenticationContextClaimsTransformation : IClaimsTransformation
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthenticationContextFactory"/> for the <see cref="AuthenticationContextClaimsTransformation"/>.
|
||||
/// </summary>
|
||||
readonly IAuthenticationContextFactory authenticationContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ApiHeaders"/> for the <see cref="AuthenticationContextClaimsTransformation"/>.
|
||||
/// </summary>
|
||||
readonly ApiHeaders apiHeaders;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationContextClaimsTransformation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContextFactory">The value of <see cref="authenticationContextFactory"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> containing the value of <see cref="apiHeaders"/>.</param>
|
||||
public AuthenticationContextClaimsTransformation(IAuthenticationContextFactory authenticationContextFactory, IApiHeadersProvider apiHeadersProvider)
|
||||
{
|
||||
this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory));
|
||||
ArgumentNullException.ThrowIfNull(apiHeadersProvider);
|
||||
apiHeaders = apiHeadersProvider.ApiHeaders;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(principal);
|
||||
|
||||
var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub);
|
||||
if (userIdClaim == default)
|
||||
throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
|
||||
|
||||
long userId;
|
||||
try
|
||||
{
|
||||
userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse user ID!", e);
|
||||
}
|
||||
|
||||
var nbfClaim = principal.FindFirst(JwtRegisteredClaimNames.Nbf);
|
||||
if (nbfClaim == default)
|
||||
throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Nbf}' claim!");
|
||||
|
||||
DateTimeOffset nbf;
|
||||
try
|
||||
{
|
||||
nbf = new DateTimeOffset(
|
||||
EpochTime.DateTime(
|
||||
Int64.Parse(nbfClaim.Value, CultureInfo.InvariantCulture)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse nbf!", ex);
|
||||
}
|
||||
|
||||
var authenticationContext = await authenticationContextFactory.CreateAuthenticationContext(
|
||||
userId,
|
||||
apiHeaders?.InstanceId,
|
||||
nbf,
|
||||
CancellationToken.None); // DCT: None available
|
||||
|
||||
if (authenticationContext.Valid)
|
||||
{
|
||||
var enumerator = Enum.GetValues(typeof(RightsType));
|
||||
var claims = new List<Claim>();
|
||||
foreach (RightsType rightType in enumerator)
|
||||
{
|
||||
// if there's no instance user, do a weird thing and add all the instance roles
|
||||
// we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
|
||||
// if user is null that means they got the token with an expired password
|
||||
var rightAsULong = authenticationContext.User == null
|
||||
|| (RightsHelper.IsInstanceRight(rightType) && authenticationContext.InstancePermissionSet == null)
|
||||
? ~0UL
|
||||
: authenticationContext.GetRight(rightType);
|
||||
var rightEnum = RightsHelper.RightToType(rightType);
|
||||
var right = (Enum)Enum.ToObject(rightEnum, rightAsULong);
|
||||
foreach (Enum enumeratedRight in Enum.GetValues(rightEnum))
|
||||
if (right.HasFlag(enumeratedRight))
|
||||
claims.Add(
|
||||
new Claim(
|
||||
ClaimTypes.Role,
|
||||
RightsHelper.RoleName(rightType, enumeratedRight)));
|
||||
}
|
||||
|
||||
principal.AddIdentity(new ClaimsIdentity(claims));
|
||||
}
|
||||
|
||||
return principal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,13 @@ using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <inheritdoc cref="IAuthenticationContextFactory" />
|
||||
/// <inheritdoc cref="IAuthenticationContext" />
|
||||
sealed class AuthenticationContextFactory : IAuthenticationContextFactory, IDisposable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthenticationContext"/> the <see cref="AuthenticationContextFactory"/> created.
|
||||
/// </summary>
|
||||
public IAuthenticationContext CurrentAuthenticationContext => currentAuthenticationContext;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>.
|
||||
@@ -39,6 +41,16 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
readonly SwarmConfiguration swarmConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="CurrentAuthenticationContext"/>.
|
||||
/// </summary>
|
||||
readonly AuthenticationContext currentAuthenticationContext;
|
||||
|
||||
/// <summary>
|
||||
/// 1 if <see cref="currentAuthenticationContext"/> was initialized, 0 otherwise.
|
||||
/// </summary>
|
||||
int initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationContextFactory"/> class.
|
||||
/// </summary>
|
||||
@@ -56,15 +68,17 @@ namespace Tgstation.Server.Host.Security
|
||||
this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
|
||||
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
currentAuthenticationContext = new AuthenticationContext();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => CurrentAuthenticationContext?.Dispose();
|
||||
public void Dispose() => currentAuthenticationContext.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validAfter, CancellationToken cancellationToken)
|
||||
public async ValueTask<IAuthenticationContext> CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset notBefore, CancellationToken cancellationToken)
|
||||
{
|
||||
if (CurrentAuthenticationContext != null)
|
||||
if (Interlocked.Exchange(ref initialized, 1) != 0)
|
||||
throw new InvalidOperationException("Authentication context has already been loaded");
|
||||
|
||||
var user = await databaseContext
|
||||
@@ -79,9 +93,8 @@ namespace Tgstation.Server.Host.Security
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (user == default)
|
||||
{
|
||||
logger.LogWarning("Unable to find user with ID {0}!", userId);
|
||||
CurrentAuthenticationContext = new AuthenticationContext();
|
||||
return;
|
||||
logger.LogWarning("Unable to find user with ID {userId}!", userId);
|
||||
return currentAuthenticationContext;
|
||||
}
|
||||
|
||||
ISystemIdentity systemIdentity;
|
||||
@@ -89,11 +102,10 @@ namespace Tgstation.Server.Host.Security
|
||||
systemIdentity = identityCache.LoadCachedIdentity(user);
|
||||
else
|
||||
{
|
||||
if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter)
|
||||
if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > notBefore)
|
||||
{
|
||||
logger.LogDebug("Rejecting token for user {0} created before last password update: {1}", userId, user.LastPasswordUpdate.Value);
|
||||
CurrentAuthenticationContext = new AuthenticationContext();
|
||||
return;
|
||||
logger.LogDebug("Rejecting token for user {userId} created before last password update: {lastPasswordUpdate}", userId, user.LastPasswordUpdate.Value);
|
||||
return currentAuthenticationContext;
|
||||
}
|
||||
|
||||
systemIdentity = null;
|
||||
@@ -112,13 +124,14 @@ namespace Tgstation.Server.Host.Security
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (instancePermissionSet == null)
|
||||
logger.LogDebug("User {0} does not have permissions on instance {1}!", userId, instanceId.Value);
|
||||
logger.LogDebug("User {userId} does not have permissions on instance {instanceId}!", userId, instanceId.Value);
|
||||
}
|
||||
|
||||
CurrentAuthenticationContext = new AuthenticationContext(
|
||||
currentAuthenticationContext.Initialize(
|
||||
systemIdentity,
|
||||
user,
|
||||
instancePermissionSet);
|
||||
return currentAuthenticationContext;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user