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]`.
diff --git a/build/Version.props b/build/Version.props
index c27dc59c94..61ce004864 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,12 +3,12 @@
- 6.0.0
+ 6.0.1
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; }
///
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;
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
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();
+ }
}
}
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs
index 8cc7d0b6fb..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);
@@ -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()
@@ -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);
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 678ef10fc7..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 = 30,
- HealthCheckSeconds = 0,
- Port = ddPort,
- MapThreads = 2,
- LogOutput = false,
- AdditionalParameters = BaseAdditionalParameters
- }, cancellationToken).AsTask(),
+ UpdateDDSettings(),
CheckByondVersions(),
ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest
{
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()
{