From 537d8f07de3d6e7f63c9e0a6ce8a76c3ed4d2f57 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 24 Dec 2023 17:21:31 -0500 Subject: [PATCH 01/10] Add a log message about discord remaps --- .../Components/Chat/Providers/DiscordProvider.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 6f7ad59c5a..da46f6e678 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -687,9 +687,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers channelId, discordChannelResponse.LogFormat()); - remapRequired |= !(discordChannelResponse.Error is RestResultError restResultError + var remapConditional = !(discordChannelResponse.Error is RestResultError restResultError && (restResultError.Error?.Code == DiscordError.MissingAccess || restResultError.Error?.Code == DiscordError.UnknownChannel)); + + if (remapConditional) + { + Logger.Log( + remapRequired + ? LogLevel.Trace + : LogLevel.Debug, + "Error on channel {channelId} is not an access/thread issue. Will retry remap...", + channelId); + remapRequired = true; + } + return null; } @@ -721,7 +733,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers "Error retrieving discord guild {guildID}: {result}", guildId, guildsResponse.LogFormat()); - remapRequired |= true; + remapRequired = true; } return null; From b9562f17bdd1b9b979cb5aeb9e6f6decdb7b4c9a Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 24 Dec 2023 18:05:52 -0500 Subject: [PATCH 02/10] Fix a README typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2830e93d9..8b5432ccf0 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ On Linux, as long as OpenDream and TGS do not use the same .NET major version, y 1. Install `tgstation-server` using any of the above methods. 1. [Download the Linux SDK binaries](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) for your selected architecture. -1. Extract everything EXCEPT the `dotnet` executable, `LICENSE.txt``, and `ThirdPartyNotices.txt` in the `.tar.gz` on top of the existing installation directory `/usr/share/dotnet/` +1. Extract everything EXCEPT the `dotnet` executable, `LICENSE.txt`, and `ThirdPartyNotices.txt` in the `.tar.gz` on top of the existing installation directory `/usr/share/dotnet/` 1. Run `sudo chown -R root /usr/share/dotnet` You should now be able to run the `dotnet --list-sdks` command and see an entry for `7.0.XXX [/usr/share/dotnet/sdk]`. From 1f480ce28de4bc035cff5c485408d6ff6742fe92 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 08:35:50 -0500 Subject: [PATCH 03/10] Maybe fix issues with rooted API paths? --- .../Controllers/ControlPanelController.cs | 2 +- src/Tgstation.Server.Host/Controllers/RootController.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs index d69947d4ba..7e450874f5 100644 --- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs @@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The value of the route. /// The to use. - [Route("/{**appRoute}")] + [Route("{**appRoute}")] [HttpGet] public IActionResult Get([FromRoute] string appRoute) { diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs index 9ae6e7e942..e075aa8332 100644 --- a/src/Tgstation.Server.Host/Controllers/RootController.cs +++ b/src/Tgstation.Server.Host/Controllers/RootController.cs @@ -94,21 +94,21 @@ namespace Tgstation.Server.Host.Controllers [AllowAnonymous] public IActionResult Index() { - const string ApiDocumentationRoute = "/" + SwaggerConfiguration.DocumentationSiteRouteExtension; var panelEnabled = controlPanelConfiguration.Enable; var apiDocsEnabled = generalConfiguration.HostApiDocumentation; + var controlPanelRoute = ControlPanelController.ControlPanelRoute.TrimStart('/'); if (panelEnabled ^ apiDocsEnabled) if (panelEnabled) - return Redirect(ControlPanelController.ControlPanelRoute); + return Redirect(controlPanelRoute); else - return Redirect(ApiDocumentationRoute); + return Redirect(SwaggerConfiguration.DocumentationSiteRouteExtension); Dictionary? links; if (panelEnabled) links = new Dictionary() { - { "Web Control Panel", ControlPanelController.ControlPanelRoute.TrimStart('/') }, + { "Web Control Panel", controlPanelRoute }, { "API Documentation", SwaggerConfiguration.DocumentationSiteRouteExtension }, }; else From 80695b8f7306f4e494f3368f108c7c7655b08705 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 08:41:38 -0500 Subject: [PATCH 04/10] Version bump to 6.0.1 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index c27dc59c94..4d9c107cd1 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,7 +3,7 @@ - 6.0.0 + 6.0.1 5.0.0 10.0.0 7.0.0 From 256732618cb9d2e94b743287597ae5bac8beee8d Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 08:46:59 -0500 Subject: [PATCH 05/10] Adjust test server startup timeout to 60s --- tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs | 4 ++-- tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs index 8cc7d0b6fb..07e87c2d24 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs @@ -139,10 +139,10 @@ namespace Tgstation.Server.Tests.Live.Instance var updatedDD = await dreamDaemonClient.Update(new DreamDaemonRequest { - StartupTimeout = 30, + StartupTimeout = 60, Port = ddPort }, cancellationToken); - Assert.AreEqual(30U, updatedDD.StartupTimeout); + Assert.AreEqual(60U, updatedDD.StartupTimeout); Assert.AreEqual(ddPort, updatedDD.Port); async Task CompileAfterByondInstall() diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 678ef10fc7..6c32ac45b8 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -133,7 +133,7 @@ namespace Tgstation.Server.Tests.Live.Instance // Increase startup timeout, disable heartbeats, enable map threads because we've tested without for years instanceClient.DreamDaemon.Update(new DreamDaemonRequest { - StartupTimeout = 30, + StartupTimeout = 60, HealthCheckSeconds = 0, Port = ddPort, MapThreads = 2, From eea3ec71d4eee2a9020a0f0f522ed18582a4d5ea Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 08:49:38 -0500 Subject: [PATCH 06/10] Workaround for random-ass port allocation failures --- .../Live/Instance/InstanceTest.cs | 25 +++++++++++-- .../Live/Instance/WatchdogTest.cs | 37 ++++++++++++++----- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index bbe349ca5c..555760d91f 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -159,10 +159,27 @@ namespace Tgstation.Server.Tests.Live.Instance Origin = new Uri(Origin), }, cancellationToken).AsTask(); - var dmUpdateRequest = instanceClient.DreamMaker.Update(new DreamMakerRequest + async Task UpdateDMSettings() { - ApiValidationPort = dmPort, - }, cancellationToken); + for (var i = 0; i < 5; ++i) + try + { + await instanceClient.DreamMaker.Update(new DreamMakerRequest + { + ApiValidationPort = dmPort, + }, cancellationToken); + } + catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable) + { + if (i == 4) + throw; + + // I have no idea why this happens sometimes + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + } + } + + var dmUpdateRequest = UpdateDMSettings(); // need at least one chat bot to satisfy DMAPI test, // use discord as it allows multi-botting on on token unlike IRC @@ -230,7 +247,7 @@ namespace Tgstation.Server.Tests.Live.Instance await Task.WhenAll( jrt.WaitForJob(installJob2.InstallJob, EngineTest.EngineInstallationTimeout(compatVersion) + 30, false, null, cancellationToken), jrt.WaitForJob(cloneRequest.Result.ActiveJob, 60, false, null, cancellationToken), - dmUpdateRequest.AsTask(), + dmUpdateRequest, cloneRequest); if (compatVersion.Engine.Value == EngineType.OpenDream) diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 6c32ac45b8..87a04a297d 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -129,17 +129,34 @@ namespace Tgstation.Server.Tests.Live.Instance } } + async Task UpdateDDSettings() + { + for (var i = 0; i < 5; ++i) + try + { + // Increase startup timeout, disable heartbeats, enable map threads because we've tested without for years + await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + StartupTimeout = 60, + HealthCheckSeconds = 0, + Port = ddPort, + MapThreads = 2, + LogOutput = false, + AdditionalParameters = BaseAdditionalParameters + }, cancellationToken); + } + catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable) + { + if (i == 4) + throw; + + // I have no idea why this happens sometimes + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + } + } + await Task.WhenAll( - // Increase startup timeout, disable heartbeats, enable map threads because we've tested without for years - instanceClient.DreamDaemon.Update(new DreamDaemonRequest - { - StartupTimeout = 60, - HealthCheckSeconds = 0, - Port = ddPort, - MapThreads = 2, - LogOutput = false, - AdditionalParameters = BaseAdditionalParameters - }, cancellationToken).AsTask(), + UpdateDDSettings(), CheckByondVersions(), ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest { From 1974d67bf1bf3bed12c90d74c0179ea84e72fd22 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 21:38:07 -0500 Subject: [PATCH 07/10] Increase a deployment test timeout --- tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs index 07e87c2d24..0cd00f5330 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs @@ -123,7 +123,7 @@ namespace Tgstation.Server.Tests.Live.Instance var updatedDM = await dreamMakerClient.Update(new DreamMakerRequest { ProjectName = "tests/DMAPI/ApiFree/api_free", - ApiValidationPort = dmPort + ApiValidationPort = dmPort, }, cancellationToken); Assert.AreEqual(dmPort, updatedDM.ApiValidationPort); Assert.AreEqual("tests/DMAPI/ApiFree/api_free", updatedDM.ProjectName); @@ -153,7 +153,7 @@ namespace Tgstation.Server.Tests.Live.Instance var deployJobTask = CompileAfterByondInstall(); var deployJob = await deployJobTask; - var deploymentJobWaitTask = WaitForJob(deployJob, 40, true, ErrorCode.DeploymentNeverValidated, cancellationToken); + var deploymentJobWaitTask = WaitForJob(deployJob, 120, true, ErrorCode.DeploymentNeverValidated, cancellationToken); await CheckDreamDaemonPriority(deploymentJobWaitTask, cancellationToken); From ec59596dcd1113a50d87008951c9d77a8533f4ca Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 21:45:47 -0500 Subject: [PATCH 08/10] Add failing test for relog issue --- tests/Tgstation.Server.Tests/Live/TestLiveServer.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 9ac95ce865..d745eddbcb 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -1342,7 +1342,20 @@ namespace Tgstation.Server.Tests.Live { Api.Models.Instance instance; long initialStaged, initialActive, initialSessionId; + await using var firstAdminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); + await using (var tokenOnlyClient = clientFactory.CreateFromToken(server.RootUrl, firstAdminClient.Token)) + { + // regression test for password change issue + var currentUser = await tokenOnlyClient.Users.Read(cancellationToken); + var updatedUser = await tokenOnlyClient.Users.Update(new UserUpdateRequest + { + Id = currentUser.Id, + Password = DefaultCredentials.DefaultAdminUserPassword, + }, cancellationToken); + + await ApiAssert.ThrowsException(() => tokenOnlyClient.Users.Read(cancellationToken), null); + } async ValueTask CreateUserWithNoInstancePerms() { From fb9d0f6d27c0da096917b43c37ce46348ec4bc7f Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 22:28:20 -0500 Subject: [PATCH 09/10] Fix authorization filter issues - Remove `AuthenticationContextAuthorizationFilter` as it wasn't being called. Properly move functionality to `TgsAuthorizeAttribute` - Fix unauth/forbid decision being skipped for tokens with password updates. - Fix issues if a user used a token at the exact same second of a password update. - Update auth documentation. --- src/Tgstation.Server.Host/Core/Application.cs | 2 - ...uthenticationContextAuthorizationFilter.cs | 55 ------------------- ...thenticationContextClaimsTransformation.cs | 42 +++++++------- .../Security/AuthenticationContextFactory.cs | 2 +- src/Tgstation.Server.Host/Security/README.md | 2 +- .../Security/TgsAuthorizeAttribute.cs | 28 +++++++++- 6 files changed, 48 insertions(+), 83 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 3b80d9da38..7e16addfe8 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -16,7 +16,6 @@ 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; @@ -629,7 +628,6 @@ namespace Tgstation.Server.Host.Core .GetRequiredService() .CurrentAuthenticationContext); services.AddScoped(); - services.AddScoped(); services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs deleted file mode 100644 index b009f599ad..0000000000 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Security.Claims; - -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Logging; - -using Tgstation.Server.Host.Models; - -namespace Tgstation.Server.Host.Security -{ - /// - /// An that maps s using an . - /// - sealed class AuthenticationContextAuthorizationFilter : IAuthorizationFilter - { - /// - /// The for the . - /// - readonly IAuthenticationContext authenticationContext; - - /// - /// The for the . - /// - readonly ILogger logger; - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - public AuthenticationContextAuthorizationFilter(IAuthenticationContext authenticationContext, ILogger logger) - { - this.authenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!authenticationContext.Valid) - { - logger.LogTrace("authenticationContext is invalid!"); - context.Result = new UnauthorizedResult(); - return; - } - - if (authenticationContext.User.Require(x => x.Enabled)) - return; - - logger.LogTrace("authenticationContext is for a disabled user!"); - context.Result = new ForbidResult(); - } - } -} diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs index c5b1fe0ce2..d649e2f7ff 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs @@ -83,32 +83,28 @@ namespace Tgstation.Server.Host.Security nbf, CancellationToken.None); // DCT: None available - if (authenticationContext.Valid) + var enumerator = Enum.GetValues(typeof(RightsType)); + var claims = new List(); + foreach (RightsType rightType in enumerator) { - var enumerator = Enum.GetValues(typeof(RightsType)); - var claims = new List(); - 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)); + // if there's a bad condition, do a weird thing and add all the roles + // we need it so we can get to TgsAuthorizeAttribute where we can properly decide between BadRequest and Forbid + var rightAsULong = !authenticationContext.Valid + || (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; } } diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 80a9f1df92..00df5df54f 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -102,7 +102,7 @@ namespace Tgstation.Server.Host.Security systemIdentity = identityCache.LoadCachedIdentity(user); else { - if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > notBefore) + if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate >= notBefore) { logger.LogDebug("Rejecting token for user {userId} created before last password update: {lastPasswordUpdate}", userId, user.LastPasswordUpdate.Value); return currentAuthenticationContext; diff --git a/src/Tgstation.Server.Host/Security/README.md b/src/Tgstation.Server.Host/Security/README.md index ac6cbbc08f..2f7faf3923 100644 --- a/src/Tgstation.Server.Host/Security/README.md +++ b/src/Tgstation.Server.Host/Security/README.md @@ -64,7 +64,7 @@ 1. If a valid authentication context is returned from the [IAuthenticationContextFactory](./IAuthenticationContextFactory.cs), the [AuthenticationContextClaimsTransformation](./AuthenticationContextClaimsTransformation.cs) uses the context to add claims for each permission bit to the user's identity principal. - Internally, ASP.NET Core uses this to determine whether or not a request to an endpoint will 403 or not based on the parameters of its [TgsAuthorizeAttribute](./TgsAuthorizeAttribute.cs). 1. The authorization filter is invoked - - For non-SignalR hub requests, this is the [AuthenticationContextAuthorizationFilter](./AuthenticationContextAuthorizationFilter.cs). It does two simple things: + - For non-SignalR hub requests, this is the `IAuthorizationFilter` part of the [TgsAuthorizeAttribute](./TgsAuthorizeAttribute.cs). It does two simple things: 1. It checks the validity of the scope's [IAuthenticationContext](./IAuthenticationContext.cs). If it is invalid (indicating the user is not authorized either due to not existing (Only possible with a forged and signed JWT) or if their token was outdated compared to the last time their password or `Enabled` status was updated), HTTP 401 will be returned. 1. It checks the user's `Enabled` status. If the user is disabled, HTTP 403 will be returned. - For SignalR hub requests, this is the [AuthorizationContextHubFilter](./AuthorizationContextHubFilter.cs). diff --git a/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs index 0fc4c11c1a..0dda24b472 100644 --- a/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs +++ b/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs @@ -1,8 +1,13 @@ using System; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Security { @@ -11,7 +16,7 @@ namespace Tgstation.Server.Host.Security /// #pragma warning disable CA1019 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - sealed class TgsAuthorizeAttribute : AuthorizeAttribute + sealed class TgsAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter { /// /// Gets the associated with the if any. @@ -114,5 +119,26 @@ namespace Tgstation.Server.Host.Security Roles = RightsHelper.RoleNames(requiredRights); RightsType = Api.Rights.RightsType.InstancePermissionSet; } + + /// + public void OnAuthorization(AuthorizationFilterContext context) + { + var services = context.HttpContext.RequestServices; + var authenticationContext = services.GetRequiredService(); + var logger = services.GetRequiredService>(); + + if (!authenticationContext.Valid) + { + logger.LogTrace("authenticationContext is invalid!"); + context.Result = new UnauthorizedResult(); + return; + } + + if (authenticationContext.User.Require(x => x.Enabled)) + return; + + logger.LogTrace("authenticationContext is for a disabled user!"); + context.Result = new ForbidResult(); + } } } From 41762f7e6eaaa314ac65b91c8d2a827768bb2540 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 25 Dec 2023 22:29:12 -0500 Subject: [PATCH 10/10] Fix `ErrorMessageResponse` improperly deserializing from 401 responses --- build/Version.props | 4 ++-- .../Models/Response/ErrorMessageResponse.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build/Version.props b/build/Version.props index 4d9c107cd1..61ce004864 100644 --- a/build/Version.props +++ b/build/Version.props @@ -7,8 +7,8 @@ 5.0.0 10.0.0 7.0.0 - 13.0.0 - 15.0.0 + 13.0.1 + 15.0.1 7.0.0 5.7.0 1.4.1 diff --git a/src/Tgstation.Server.Api/Models/Response/ErrorMessageResponse.cs b/src/Tgstation.Server.Api/Models/Response/ErrorMessageResponse.cs index 914d9ecc3a..70cb64c530 100644 --- a/src/Tgstation.Server.Api/Models/Response/ErrorMessageResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/ErrorMessageResponse.cs @@ -1,6 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + namespace Tgstation.Server.Api.Models.Response { /// @@ -28,6 +30,7 @@ namespace Tgstation.Server.Api.Models.Response /// The of the . /// [EnumDataType(typeof(ErrorCode))] + [JsonProperty(Required = Required.Always)] public ErrorCode ErrorCode { get; set; } ///