diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 7f9a1e7835..e7b1cb91b3 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -1409,7 +1409,7 @@ jobs:
name: Build .deb Package # Can't do i386 due to https://github.com/dotnet/core/issues/4595
needs: build-releasenotes
runs-on: ubuntu-latest
- timeout-minutes: 15
+ timeout-minutes: 30
env:
TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt
steps:
diff --git a/.github/workflows/regenerate-nix-deps.yml b/.github/workflows/regenerate-nix-deps.yml
index b779024714..2410792275 100644
--- a/.github/workflows/regenerate-nix-deps.yml
+++ b/.github/workflows/regenerate-nix-deps.yml
@@ -2,6 +2,9 @@ name: Regenerate Nix Nuget Dependencies
on:
pull_request_target:
+ paths:
+ - '**.csproj'
+ - '**.props'
branches:
- dev
- master
diff --git a/README.md b/README.md
index 527ed213e5..364919b42c 100644
--- a/README.md
+++ b/README.md
@@ -169,6 +169,7 @@ The following dependencies are required.
- aspnetcore-runtime-10.0 (See Prerequisites under the `Ubuntu/Debian Package` section)
- libc6-i386
- libstdc++6:i386
+- libcurl4:i386
- gcc-multilib (Only on 64-bit systems)
- gdb (for using gcore to create core dumps)
diff --git a/build/Dockerfile b/build/Dockerfile
index f826539df8..2bed6cf1e2 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -7,11 +7,11 @@ ARG TGS_TELEMETRY_KEY_FILE=
# replace shell with bash so we can source files
RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.1/install.sh | sh
-ENV NODE_VERSION 20.5.1
+ENV NODE_VERSION=20.5.1
-ENV NVM_DIR /root/.nvm
-ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
-ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
+ENV NVM_DIR=/root/.nvm
+ENV NODE_PATH=$NVM_DIR/v$NODE_VERSION/lib/node_modules
+ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
RUN . $NVM_DIR/nvm.sh \
&& nvm install $NODE_VERSION \
@@ -70,9 +70,9 @@ RUN dpkg --add-architecture i386 \
EXPOSE 5000
-ENV General__ValidInstancePaths__0 /tgs_instances
-ENV FileLogging__Directory /tgs_logs
-ENV Internal__UsingDocker true
+ENV General__ValidInstancePaths__0=/tgs_instances
+ENV FileLogging__Directory=/tgs_logs
+ENV Internal__UsingDocker=true
WORKDIR /app
diff --git a/build/TestCommon.props b/build/TestCommon.props
index 0e6e0f5cdc..3f87378f49 100644
--- a/build/TestCommon.props
+++ b/build/TestCommon.props
@@ -18,8 +18,8 @@
-
+
-
+
diff --git a/build/Version.props b/build/Version.props
index 7b944755a1..a444d4c393 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -5,7 +5,7 @@
7.0.0
6.0.0
- 10.13.0
+ 10.14.0
0.6.0
7.0.0
19.0.0
diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs
index ea2c9ebd67..b7673ff4e9 100644
--- a/src/Tgstation.Server.Api/Models/ErrorCode.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs
@@ -493,7 +493,7 @@ namespace Tgstation.Server.Api.Models
///
/// Attempted to restart a stopped watchdog.
///
- [Description("Cannot restart the watchdog as it is not running!")]
+ [Description("Cannot perform watchdog operation as it is not running!")]
WatchdogNotRunning,
///
diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs
index 116f27425d..d97e8c3102 100644
--- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs
@@ -14,6 +14,13 @@ namespace Tgstation.Server.Api.Models.Internal
[ResponseOptions]
public long? SessionId { get; set; }
+ ///
+ /// A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc/Reboot). Only unique within the current . Only tracked in game sessions with the DMAPI enabled.
+ ///
+ /// 1
+ [ResponseOptions]
+ public long? WorldIteration { get; set; }
+
///
/// When the current server execution was started.
///
diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
index 424d4608d5..ec1d0fad08 100644
--- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs
+++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
@@ -33,6 +33,12 @@ namespace Tgstation.Server.Api.Rights
/// The of the given .
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
+ ///
+ /// Iterate the of each right.
+ ///
+ /// An of each of right.
+ public static IEnumerable AllRightTypes() => TypeMap.Values;
+
///
/// Map a given to its respective .
///
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql
index 3ee0b9c6bc..23ad75d65a 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql
@@ -9,9 +9,11 @@ mutation CreateSystemUserWithPermissionSet($systemIdentifier: String!) {
message
}
}
- user {
+ updatedUser {
id
- name
+ user {
+ name
+ }
}
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql
index dc69b9ef29..50140af490 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql
@@ -7,7 +7,7 @@ mutation CreateUserFromOAuthConnection($name: String!, $oAuthConnections: [OAuth
message
}
}
- user {
+ updatedUser {
id
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql
index 8f330b0364..69e3cbe5d3 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql
@@ -7,7 +7,7 @@ mutation CreateUserWithPassword($name: String!, $password: String!) {
message
}
}
- user {
+ updatedUser {
id
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql
index 2d6b335889..86874a4834 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql
@@ -1,10 +1,12 @@
mutation CreateUserWithPasswordSelectOAuthConnections($name: String!, $password: String!) {
createUserByPasswordAndPermissionSet(input: { name: $name, password: $password }) {
- user {
+ updatedUser {
id
- oAuthConnections {
- externalUserId
- provider
+ user {
+ oAuthConnections {
+ externalUserId
+ provider
+ }
}
}
errors {
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql
index 4299d5b537..db77e0c601 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql
@@ -7,35 +7,37 @@ mutation SetUserGroup($id: ID!, $newGroupId: ID!) {
message
}
}
- user {
- ownedPermissionSet {
- instanceManagerRights {
- canCreate
- canDelete
- canGrantPermissions
- canList
- canRead
- canRelocate
- canRename
- canSetAutoUpdate
- canSetChatBotLimit
- canSetConfiguration
- canSetOnline
+ updatedUser {
+ user {
+ ownedPermissionSet {
+ instanceManagerRights {
+ canCreate
+ canDelete
+ canGrantPermissions
+ canList
+ canRead
+ canRelocate
+ canRename
+ canSetAutoUpdate
+ canSetChatBotLimit
+ canSetConfiguration
+ canSetOnline
+ }
+ administrationRights {
+ canChangeVersion
+ canDownloadLogs
+ canEditOwnServiceConnections
+ canEditOwnPassword
+ canReadUsers
+ canRestartHost
+ canUploadVersion
+ canWriteUsers
+ }
}
- administrationRights {
- canChangeVersion
- canDownloadLogs
- canEditOwnServiceConnections
- canEditOwnPassword
- canReadUsers
- canRestartHost
- canUploadVersion
- canWriteUsers
+ group {
+ id
}
}
- group {
- id
- }
}
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql
index 81746cf0b4..34f74f64bf 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql
@@ -2,17 +2,19 @@ mutation SetUserOAuthConnections($id: ID!, $newOAuthConnections: [OAuthConnectio
updateUser(
input: { id: $id, newOAuthConnections: $newOAuthConnections }
) {
- user {
- canonicalName
- createdAt
- enabled
- id
- name
- systemIdentifier
- oAuthConnections {
- externalUserId
- provider
- }
+ updatedUser {
+ user {
+ canonicalName
+ createdAt
+ enabled
+ id
+ name
+ systemIdentifier
+ oAuthConnections {
+ externalUserId
+ provider
+ }
+ }
}
errors {
... on ErrorMessageError {
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql
index fa2fa558fe..ae1a58b5fc 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql
@@ -7,60 +7,62 @@ mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) {
message
}
}
- user {
- effectivePermissionSet {
- administrationRights {
- canChangeVersion
- canDownloadLogs
- canEditOwnServiceConnections
- canEditOwnPassword
- canReadUsers
- canRestartHost
- canUploadVersion
- canWriteUsers
+ updatedUser {
+ user {
+ effectivePermissionSet {
+ administrationRights {
+ canChangeVersion
+ canDownloadLogs
+ canEditOwnServiceConnections
+ canEditOwnPassword
+ canReadUsers
+ canRestartHost
+ canUploadVersion
+ canWriteUsers
+ }
+ instanceManagerRights {
+ canCreate
+ canDelete
+ canGrantPermissions
+ canList
+ canRead
+ canRelocate
+ canRename
+ canSetAutoUpdate
+ canSetChatBotLimit
+ canSetConfiguration
+ canSetOnline
+ }
+ }
+ ownedPermissionSet {
+ administrationRights {
+ canChangeVersion
+ canDownloadLogs
+ canEditOwnServiceConnections
+ canEditOwnPassword
+ canReadUsers
+ canRestartHost
+ canUploadVersion
+ canWriteUsers
+ }
+ instanceManagerRights {
+ canCreate
+ canDelete
+ canGrantPermissions
+ canList
+ canRead
+ canRelocate
+ canRename
+ canSetAutoUpdate
+ canSetChatBotLimit
+ canSetConfiguration
+ canSetOnline
+ }
+ }
+ group {
+ id
+ }
}
- instanceManagerRights {
- canCreate
- canDelete
- canGrantPermissions
- canList
- canRead
- canRelocate
- canRename
- canSetAutoUpdate
- canSetChatBotLimit
- canSetConfiguration
- canSetOnline
- }
- }
- ownedPermissionSet {
- administrationRights {
- canChangeVersion
- canDownloadLogs
- canEditOwnServiceConnections
- canEditOwnPassword
- canReadUsers
- canRestartHost
- canUploadVersion
- canWriteUsers
- }
- instanceManagerRights {
- canCreate
- canDelete
- canGrantPermissions
- canList
- canRead
- canRelocate
- canRename
- canSetAutoUpdate
- canSetChatBotLimit
- canSetConfiguration
- canSetOnline
- }
- }
- group {
- id
- }
}
}
}
diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql
index 8cd18d2dc5..ad0fdfdbad 100644
--- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql
+++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql
@@ -1,10 +1,12 @@
mutation UpdateUserOAuthConnections($id: ID!, $newOAuthConnections: [OAuthConnectionInput!]) {
updateUser(input: { id: $id, newOAuthConnections: $newOAuthConnections }) {
- user {
+ updatedUser {
id
- oAuthConnections {
- externalUserId
- provider
+ user {
+ oAuthConnections {
+ externalUserId
+ provider
+ }
}
}
errors {
diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs
index 0a7214e3cd..ffa5731f35 100644
--- a/src/Tgstation.Server.Client/ApiClient.cs
+++ b/src/Tgstation.Server.Client/ApiClient.cs
@@ -69,9 +69,9 @@ namespace Tgstation.Server.Client
};
///
- /// The for the .
+ /// The for the .
///
- readonly IHttpClient httpClient;
+ readonly HttpClient httpClient;
///
/// The s used by the .
@@ -166,7 +166,7 @@ namespace Tgstation.Server.Client
/// The value of .
/// The value of .
public ApiClient(
- IHttpClient httpClient,
+ HttpClient httpClient,
Uri url,
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
diff --git a/src/Tgstation.Server.Client/ApiClientFactory.cs b/src/Tgstation.Server.Client/ApiClientFactory.cs
index 0429f7d649..469010c153 100644
--- a/src/Tgstation.Server.Client/ApiClientFactory.cs
+++ b/src/Tgstation.Server.Client/ApiClientFactory.cs
@@ -1,7 +1,7 @@
using System;
+using System.Net.Http;
using Tgstation.Server.Api;
-using Tgstation.Server.Common.Http;
namespace Tgstation.Server.Client
{
@@ -19,5 +19,19 @@ namespace Tgstation.Server.Client
apiHeaders,
tokenRefreshHeaders,
authless);
+
+ ///
+ public IApiClient CreateApiClient(
+ Uri url,
+ ApiHeaders apiHeaders,
+ ApiHeaders? tokenRefreshHeaders,
+ HttpMessageHandler handler,
+ bool disposeHandler,
+ bool authless) => new ApiClient(
+ new HttpClient(handler, disposeHandler),
+ url,
+ apiHeaders,
+ tokenRefreshHeaders,
+ authless);
}
}
diff --git a/src/Tgstation.Server.Client/IApiClientFactory.cs b/src/Tgstation.Server.Client/IApiClientFactory.cs
index e49385dcf9..f0fa49a42a 100644
--- a/src/Tgstation.Server.Client/IApiClientFactory.cs
+++ b/src/Tgstation.Server.Client/IApiClientFactory.cs
@@ -1,4 +1,5 @@
using System;
+using System.Net.Http;
using Tgstation.Server.Api;
@@ -22,5 +23,23 @@ namespace Tgstation.Server.Client
ApiHeaders apiHeaders,
ApiHeaders? tokenRefreshHeaders,
bool authless);
+
+ ///
+ /// Create an .
+ ///
+ /// The base .
+ /// The for the .
+ /// The to use to generate a new .
+ /// The to use with the internal .
+ /// If should be disposed with the created .
+ /// If there should be no authentication performed.
+ /// A new .
+ public IApiClient CreateApiClient(
+ Uri url,
+ ApiHeaders apiHeaders,
+ ApiHeaders? tokenRefreshHeaders,
+ HttpMessageHandler handler,
+ bool disposeHandler,
+ bool authless);
}
}
diff --git a/src/Tgstation.Server.Common/Http/CachedResponseStream.cs b/src/Tgstation.Server.Common/Http/CachedResponseStream.cs
index eb007b54a5..14361b6ef8 100644
--- a/src/Tgstation.Server.Common/Http/CachedResponseStream.cs
+++ b/src/Tgstation.Server.Common/Http/CachedResponseStream.cs
@@ -36,7 +36,7 @@ namespace Tgstation.Server.Common.Http
response.Content = null;
try
{
- // don't cry about the missing CancellationToken overload: https://github.com/dotnet/runtime/issues/916
+ // don't cry about the missing CancellationToken overload: https://github.com/dotnet/corefx/issues/32615#issuecomment-562083237
var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
return new CachedResponseStream(content, responseStream);
}
diff --git a/src/Tgstation.Server.Common/Http/HttpClient.cs b/src/Tgstation.Server.Common/Http/HttpClient.cs
deleted file mode 100644
index 692c10a304..0000000000
--- a/src/Tgstation.Server.Common/Http/HttpClient.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Tgstation.Server.Common.Http
-{
- ///
- public sealed class HttpClient : IHttpClient
- {
- ///
- public TimeSpan Timeout
- {
- get => httpClient.Timeout;
- set => httpClient.Timeout = value;
- }
-
- ///
- public HttpRequestHeaders DefaultRequestHeaders => httpClient.DefaultRequestHeaders;
-
- ///
- /// The real .
- ///
- readonly System.Net.Http.HttpClient httpClient;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The to wrap.
- public HttpClient(System.Net.Http.HttpClient implementation)
- {
- httpClient = implementation ?? throw new ArgumentNullException(nameof(implementation));
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- public HttpClient()
- : this(new System.Net.Http.HttpClient())
- {
- }
-
- ///
- public void Dispose() => httpClient.Dispose();
-
- ///
- public Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
- => httpClient.SendAsync(request, completionOption, cancellationToken);
- }
-}
diff --git a/src/Tgstation.Server.Common/Http/HttpClientFactory.cs b/src/Tgstation.Server.Common/Http/HttpClientFactory.cs
deleted file mode 100644
index 93659471b5..0000000000
--- a/src/Tgstation.Server.Common/Http/HttpClientFactory.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-using System.Net.Http.Headers;
-
-namespace Tgstation.Server.Common.Http
-{
- ///
- /// that creates s.
- ///
- public sealed class HttpClientFactory : IAbstractHttpClientFactory
- {
- ///
- public IHttpClient CreateClient()
- {
- var client = new HttpClient();
- try
- {
- client.DefaultRequestHeaders.UserAgent.Add(userAgent);
- return client;
- }
- catch
- {
- client.Dispose();
- throw;
- }
- }
-
- ///
- /// The used as created client's User-Agent header on request.
- ///
- readonly ProductInfoHeaderValue userAgent;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The value of .
- public HttpClientFactory(ProductInfoHeaderValue userAgent)
- {
- this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent));
- }
- }
-}
diff --git a/src/Tgstation.Server.Common/Http/IAbstractHttpClientFactory.cs b/src/Tgstation.Server.Common/Http/IAbstractHttpClientFactory.cs
deleted file mode 100644
index 120712f2bb..0000000000
--- a/src/Tgstation.Server.Common/Http/IAbstractHttpClientFactory.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Tgstation.Server.Common.Http
-{
- ///
- /// Creates s.
- ///
- public interface IAbstractHttpClientFactory
- {
- ///
- /// Create a .
- ///
- /// A new .
- IHttpClient CreateClient();
- }
-}
diff --git a/src/Tgstation.Server.Common/Http/IHttpClient.cs b/src/Tgstation.Server.Common/Http/IHttpClient.cs
deleted file mode 100644
index d0ade7050d..0000000000
--- a/src/Tgstation.Server.Common/Http/IHttpClient.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Tgstation.Server.Common.Http
-{
- ///
- /// For sending HTTP requests.
- ///
- public interface IHttpClient : IDisposable
- {
- ///
- /// The request timeout.
- ///
- TimeSpan Timeout { get; set; }
-
- ///
- /// The used on every request.
- ///
- HttpRequestHeaders DefaultRequestHeaders { get; }
-
- ///
- /// Send an HTTP request.
- ///
- /// The .
- /// The .
- /// The for the operation.
- /// A resulting in the of the request.
- Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);
- }
-}
diff --git a/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs b/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs
index 76c7fc937b..ca369c70dc 100644
--- a/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs
@@ -1,9 +1,12 @@
using System;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using System.Web;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Octokit;
@@ -11,9 +14,11 @@ using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Authority.Core;
+using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
-using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.IO;
+using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Transfer;
using Tgstation.Server.Host.Utils.GitHub;
@@ -57,10 +62,29 @@ namespace Tgstation.Server.Host.Authority
///
readonly IMemoryCache cacheService;
+ ///
+ /// The for the .
+ ///
+ readonly IAssemblyInformationProvider assemblyInformationProvider;
+
+ ///
+ /// The for the .
+ ///
+ readonly IPlatformIdentifier platformIdentifier;
+
+ ///
+ /// The for the .
+ ///
+ readonly IIOManager ioManager;
+
+ ///
+ /// The for the .
+ ///
+ readonly IOptionsSnapshot fileLoggingConfigurationOptions;
+
///
/// Initializes a new instance of the class.
///
- /// The to use.
/// The to use.
/// The to use.
/// The value of .
@@ -68,17 +92,23 @@ namespace Tgstation.Server.Host.Authority
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
public AdministrationAuthority(
- IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger logger,
IGitHubServiceFactory gitHubServiceFactory,
IServerControl serverControl,
IServerUpdateInitiator serverUpdateInitiator,
IFileTransferTicketProvider fileTransferService,
- IMemoryCache cacheService)
+ IMemoryCache cacheService,
+ IAssemblyInformationProvider assemblyInformationProvider,
+ IPlatformIdentifier platformIdentifier,
+ IIOManager ioManager,
+ IOptionsSnapshot fileLoggingConfigurationOptions)
: base(
- authenticationContext,
databaseContext,
logger)
{
@@ -87,157 +117,212 @@ namespace Tgstation.Server.Host.Authority
this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
+ this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
+ this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ this.fileLoggingConfigurationOptions = fileLoggingConfigurationOptions ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
}
///
- public async ValueTask> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken)
- {
- try
- {
- async Task CacheFactory()
+ public RequirementsGated> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken)
+ => new(
+ () => Flag(AdministrationRights.ChangeVersion),
+ async () =>
{
- Version? greatestVersion = null;
- Uri? repoUrl = null;
- var scopeCancellationToken = CancellationToken.None; // DCT: None available
try
{
- var gitHubService = await gitHubServiceFactory.CreateService(scopeCancellationToken);
- var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(scopeCancellationToken);
- var releases = await gitHubService.GetTgsReleases(scopeCancellationToken);
-
- foreach (var kvp in releases)
+ async Task CacheFactory()
{
- var version = kvp.Key;
- var release = kvp.Value;
- if (version.Major > 3 // Forward/backward compatible but not before TGS4
- && (greatestVersion == null || version > greatestVersion))
- greatestVersion = version;
+ Version? greatestVersion = null;
+ Uri? repoUrl = null;
+ var scopeCancellationToken = CancellationToken.None; // DCT: None available
+ try
+ {
+ var gitHubService = await gitHubServiceFactory.CreateService(scopeCancellationToken);
+ var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(scopeCancellationToken);
+ var releases = await gitHubService.GetTgsReleases(scopeCancellationToken);
+
+ foreach (var kvp in releases)
+ {
+ var version = kvp.Key;
+ var release = kvp.Value;
+ if (version.Major > 3 // Forward/backward compatible but not before TGS4
+ && (greatestVersion == null || version > greatestVersion))
+ greatestVersion = version;
+ }
+
+ repoUrl = await repositoryUrlTask;
+ }
+ catch (NotFoundException e)
+ {
+ Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
+ }
+
+ return new AdministrationResponse
+ {
+ LatestVersion = greatestVersion,
+ TrackedRepositoryUrl = repoUrl,
+ GeneratedAt = DateTimeOffset.UtcNow,
+ };
}
- repoUrl = await repositoryUrlTask;
+ var ttl = TimeSpan.FromMinutes(30);
+ Task task;
+ if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
+ {
+ using var entry = cacheService.CreateEntry(ReadCacheKey);
+ entry.AbsoluteExpirationRelativeToNow = ttl;
+ entry.Value = task = CacheFactory();
+ }
+ else
+ task = (Task)rawCacheObject!;
+
+ var result = await task.WaitAsync(cancellationToken);
+ return new AuthorityResponse(result);
}
- catch (NotFoundException e)
+ catch (RateLimitExceededException e)
{
- Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
+ return RateLimit(e);
+ }
+ catch (ApiException e)
+ {
+ Logger.LogWarning(e, OctokitException);
+ return new AuthorityResponse(
+ new ErrorMessageResponse(ErrorCode.RemoteApiError)
+ {
+ AdditionalData = e.Message,
+ },
+ HttpFailureResponse.FailedDependency);
+ }
+ });
+
+ ///
+ public RequirementsGated> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(targetVersion);
+
+ return new(
+ () =>
+ {
+ if (uploadZip)
+ return Flag(AdministrationRights.UploadVersion);
+
+ return Flag(AdministrationRights.ChangeVersion);
+ },
+ async () =>
+ {
+ if (targetVersion.Major < 4)
+ return BadRequest(ErrorCode.CannotChangeServerSuite);
+
+ if (!serverControl.WatchdogPresent)
+ return new AuthorityResponse(
+ new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
+ HttpFailureResponse.UnprocessableEntity);
+
+ IFileUploadTicket? uploadTicket = uploadZip
+ ? fileTransferService.CreateUpload(FileUploadStreamKind.None)
+ : null;
+
+ ServerUpdateResult updateResult;
+ try
+ {
+ try
+ {
+ updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken);
+ }
+ catch
+ {
+ if (uploadZip)
+ await uploadTicket!.DisposeAsync();
+
+ throw;
+ }
+ }
+ catch (RateLimitExceededException ex)
+ {
+ return RateLimit(ex);
+ }
+ catch (ApiException e)
+ {
+ Logger.LogWarning(e, OctokitException);
+ return new AuthorityResponse(
+ new ErrorMessageResponse(ErrorCode.RemoteApiError)
+ {
+ AdditionalData = e.Message,
+ },
+ HttpFailureResponse.FailedDependency);
}
- return new AdministrationResponse
+ return updateResult switch
{
- LatestVersion = greatestVersion,
- TrackedRepositoryUrl = repoUrl,
- GeneratedAt = DateTimeOffset.UtcNow,
+ ServerUpdateResult.Started => new AuthorityResponse(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted),
+ ServerUpdateResult.ReleaseMissing => Gone(),
+ ServerUpdateResult.UpdateInProgress => BadRequest(ErrorCode.ServerUpdateInProgress),
+ ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse(
+ new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed),
+ HttpFailureResponse.FailedDependency),
+ _ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
};
- }
-
- var ttl = TimeSpan.FromMinutes(30);
- Task task;
- if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
- {
- using var entry = cacheService.CreateEntry(ReadCacheKey);
- entry.AbsoluteExpirationRelativeToNow = ttl;
- entry.Value = task = CacheFactory();
- }
- else
- task = (Task)rawCacheObject!;
-
- var result = await task.WaitAsync(cancellationToken);
- return new AuthorityResponse(result);
- }
- catch (RateLimitExceededException e)
- {
- return RateLimit(e);
- }
- catch (ApiException e)
- {
- Logger.LogWarning(e, OctokitException);
- return new AuthorityResponse(
- new ErrorMessageResponse(ErrorCode.RemoteApiError)
- {
- AdditionalData = e.Message,
- },
- HttpFailureResponse.FailedDependency);
- }
+ });
}
///
- public async ValueTask> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken)
- {
- var attemptingUpload = uploadZip == true;
- if (attemptingUpload)
- {
- if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion))
- return Forbid();
- }
- else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion))
- return Forbid();
-
- if (targetVersion.Major < 4)
- return BadRequest(ErrorCode.CannotChangeServerSuite);
-
- if (!serverControl.WatchdogPresent)
- return new AuthorityResponse(
- new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
- HttpFailureResponse.UnprocessableEntity);
-
- IFileUploadTicket? uploadTicket = attemptingUpload
- ? fileTransferService.CreateUpload(FileUploadStreamKind.None)
- : null;
-
- ServerUpdateResult updateResult;
- try
- {
- try
+ public RequirementsGated TriggerServerRestart()
+ => new(
+ () => Flag(AdministrationRights.RestartHost),
+ async () =>
{
- updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken);
- }
- catch
- {
- if (attemptingUpload)
- await uploadTicket!.DisposeAsync();
-
- throw;
- }
- }
- catch (RateLimitExceededException ex)
- {
- return RateLimit(ex);
- }
- catch (ApiException e)
- {
- Logger.LogWarning(e, OctokitException);
- return new AuthorityResponse(
- new ErrorMessageResponse(ErrorCode.RemoteApiError)
+ if (!serverControl.WatchdogPresent)
{
- AdditionalData = e.Message,
- },
- HttpFailureResponse.FailedDependency);
- }
+ Logger.LogDebug("Restart request failed due to lack of host watchdog!");
+ return new AuthorityResponse(
+ new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
+ HttpFailureResponse.UnprocessableEntity);
+ }
- return updateResult switch
- {
- ServerUpdateResult.Started => new AuthorityResponse(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted),
- ServerUpdateResult.ReleaseMissing => Gone(),
- ServerUpdateResult.UpdateInProgress => BadRequest(ErrorCode.ServerUpdateInProgress),
- ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse(
- new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed),
- HttpFailureResponse.FailedDependency),
- _ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
- };
- }
+ await serverControl.Restart();
+ return new AuthorityResponse();
+ });
///
- public async ValueTask TriggerServerRestart()
+ public RequirementsGated> GetLog(string path, CancellationToken cancellationToken)
{
- if (!serverControl.WatchdogPresent)
- {
- Logger.LogDebug("Restart request failed due to lack of host watchdog!");
- return new AuthorityResponse(
- new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
- HttpFailureResponse.UnprocessableEntity);
- }
+ ArgumentNullException.ThrowIfNull(path);
+ return new(
+ () => Flag(AdministrationRights.DownloadLogs),
+ async () =>
+ {
+ path = HttpUtility.UrlDecode(path);
- await serverControl.Restart();
- return new AuthorityResponse();
+ // guard against directory navigation
+ var sanitizedPath = ioManager.GetFileName(path);
+ if (path != sanitizedPath)
+ return Forbid();
+
+ var fullPath = ioManager.ConcatPath(
+ fileLoggingConfigurationOptions.Value.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier),
+ path);
+ try
+ {
+ var fileTransferTicket = fileTransferService.CreateDownload(
+ new FileDownloadProvider(
+ () => null,
+ null,
+ fullPath,
+ true));
+
+ return new AuthorityResponse(new LogFileResponse
+ {
+ Name = path,
+ LastModified = await ioManager.GetLastModified(fullPath, cancellationToken),
+ FileTicket = fileTransferTicket.FileTicket,
+ });
+ }
+ catch (IOException ex)
+ {
+ return Conflict(ErrorCode.IOError, ex.ToString());
+ }
+ });
}
}
}
diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs
index 0ea01a89da..e7701c903b 100644
--- a/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs
+++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs
@@ -8,7 +8,7 @@ using Octokit;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Database;
-using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Security.RightsEvaluation;
namespace Tgstation.Server.Host.Authority.Core
{
@@ -17,11 +17,6 @@ namespace Tgstation.Server.Host.Authority.Core
///
abstract class AuthorityBase : IAuthority
{
- ///
- /// Gets the for the .
- ///
- protected IAuthenticationContext AuthenticationContext { get; }
-
///
/// Gets the for the .
///
@@ -88,24 +83,57 @@ namespace Tgstation.Server.Host.Authority.Core
///
/// The of the .
/// The .
+ /// for the error message.
/// A new, errored .
- protected static AuthorityResponse Conflict(ErrorCode errorCode)
+ protected static AuthorityResponse Conflict(ErrorCode errorCode, string? additionalData = null)
=> new(
- new ErrorMessageResponse(errorCode),
+ new ErrorMessageResponse(errorCode)
+ {
+ AdditionalData = additionalData,
+ },
HttpFailureResponse.Conflict);
+ ///
+ /// Helper to quickly construct a .
+ ///
+ /// The to evaluate.
+ /// The single bit flag of the .
+ /// A new .
+ protected static FlagRightsConditional Flag(TRights flag)
+ where TRights : Enum
+ => new(flag);
+
+ ///
+ /// Helper to quickly construct an .
+ ///
+ /// The to evaluate.
+ /// The left hand side operand.
+ /// The right hand side operand.
+ /// A new .
+ protected static OrRightsConditional Or(RightsConditional lhs, RightsConditional rhs)
+ where TRights : Enum
+ => new(lhs, rhs);
+
+ ///
+ /// Helper to quickly construct an .
+ ///
+ /// The to evaluate.
+ /// The left hand side operand.
+ /// The right hand side operand.
+ /// A new .
+ protected static AndRightsConditional And(RightsConditional lhs, RightsConditional rhs)
+ where TRights : Enum
+ => new(lhs, rhs);
+
///
/// Initializes a new instance of the class.
///
- /// The value of .
/// The value of .
/// The value of .
protected AuthorityBase(
- IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger logger)
{
- AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs
index f04293aecc..0a509ef39b 100644
--- a/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs
+++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs
@@ -1,7 +1,8 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
-using Tgstation.Server.Api.Models;
+using Microsoft.AspNetCore.Authorization;
namespace Tgstation.Server.Host.Authority.Core
{
@@ -14,35 +15,60 @@ namespace Tgstation.Server.Host.Authority.Core
///
protected TAuthority Authority { get; }
+ ///
+ /// The authorization service for the .
+ ///
+ readonly Security.IAuthorizationService authorizationService;
+
///
/// Initializes a new instance of the class.
///
/// The value of .
- public AuthorityInvokerBase(TAuthority authority)
+ /// The value of .
+ public AuthorityInvokerBase(
+ TAuthority authority,
+ Security.IAuthorizationService authorizationService)
{
Authority = authority ?? throw new ArgumentNullException(nameof(authority));
+ this.authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService));
}
///
- IQueryable IAuthorityInvoker.InvokeQueryable(Func> authorityInvoker)
+ async ValueTask?> IAuthorityInvoker.InvokeQueryable(Func>> authorityInvoker)
{
ArgumentNullException.ThrowIfNull(authorityInvoker);
- return authorityInvoker(Authority);
+
+ var requirementsGate = authorityInvoker(Authority);
+ return await ExecuteIfRequirementsSatisfied(requirementsGate);
}
- ///
- IQueryable IAuthorityInvoker.InvokeTransformableQueryable(Func> authorityInvoker)
+ ///
+ /// Unwrap a result, returning if the requirements weren't satisfied.
+ ///
+ /// The contained by the .
+ /// The result.
+ /// A resulting in the if the requirements were met, if the requirments weren't met.
+ protected async ValueTask ExecuteIfRequirementsSatisfied(RequirementsGated requirementsGate)
+ where TResult : class
{
- ArgumentNullException.ThrowIfNull(authorityInvoker);
+ var requirements = await requirementsGate.GetRequirements();
+ var authorizationResult = await authorizationService.AuthorizeAsync(requirements);
- var queryable = authorityInvoker(Authority);
+ if (!authorizationResult.Succeeded)
+ {
+ OnRequirementsFailure(authorizationResult.Failure);
+ return null;
+ }
- if (typeof(EntityId).IsAssignableFrom(typeof(TResult)))
- queryable = queryable.OrderBy(item => ((EntityId)(object)item).Id!.Value); // order by ID to fix an EFCore warning
+ return await requirementsGate.Execute(authorizationService);
+ }
- var expression = new TTransformer().Expression;
- return queryable
- .Select(expression);
+ ///
+ /// Called to handle generic behavior when requirements evaluation fails.
+ ///
+ /// The .
+ protected virtual void OnRequirementsFailure(AuthorizationFailure authFailure)
+ {
}
}
}
diff --git a/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs
index a25a04e278..7d75355f57 100644
--- a/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs
+++ b/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs
@@ -1,6 +1,12 @@
using System;
+using System.Linq;
using System.Threading.Tasks;
+using HotChocolate;
+
+using Microsoft.AspNetCore.Authorization;
+
+using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.GraphQL;
namespace Tgstation.Server.Host.Authority.Core
@@ -12,14 +18,17 @@ namespace Tgstation.Server.Host.Authority.Core
///
/// Throws a for errored s.
///
- /// The potentially errored .
+ /// The being checked.
+ /// The potentially errored or if requirements evaluation failed.
/// If an error should be raised for and failures.
- static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse, bool errorOnMissing)
+ /// if an wasn't thrown.
+ static TAuthorityResponse ThrowGraphQLErrorIfNecessary(TAuthorityResponse authorityResponse, bool errorOnMissing)
+ where TAuthorityResponse : AuthorityResponse
{
if (authorityResponse.Success
|| ((authorityResponse.FailureResponse.Value == HttpFailureResponse.NotFound
|| authorityResponse.FailureResponse.Value == HttpFailureResponse.Gone) && !errorOnMissing))
- return;
+ return authorityResponse;
var fallbackString = authorityResponse.FailureResponse.ToString()!;
throw new ErrorMessageException(authorityResponse.ErrorMessage, fallbackString);
@@ -29,38 +38,43 @@ namespace Tgstation.Server.Host.Authority.Core
/// Initializes a new instance of the class.
///
/// The .
- public GraphQLAuthorityInvoker(TAuthority authority)
- : base(authority)
+ /// the authorization service to use.
+ public GraphQLAuthorityInvoker(TAuthority authority, Security.IAuthorizationService authorizationService)
+ : base(authority, authorizationService)
{
}
///
- async ValueTask IGraphQLAuthorityInvoker.Invoke(Func> authorityInvoker)
+ async ValueTask IGraphQLAuthorityInvoker.Invoke(Func> authorityInvoker)
{
ArgumentNullException.ThrowIfNull(authorityInvoker);
- var authorityResponse = await authorityInvoker(Authority);
+ var requirementsGate = authorityInvoker(Authority);
+ var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
ThrowGraphQLErrorIfNecessary(authorityResponse, true);
}
///
- async ValueTask IGraphQLAuthorityInvoker.InvokeAllowMissing(Func>> authorityInvoker)
+ async ValueTask IGraphQLAuthorityInvoker.InvokeAllowMissing(Func>> authorityInvoker)
where TApiModel : default
{
ArgumentNullException.ThrowIfNull(authorityInvoker);
- var authorityResponse = await authorityInvoker(Authority);
+ var requirementsGate = authorityInvoker(Authority);
+ var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
ThrowGraphQLErrorIfNecessary(authorityResponse, false);
+
return authorityResponse.Result;
}
///
- async ValueTask IGraphQLAuthorityInvoker.InvokeTransformableAllowMissing(Func>> authorityInvoker)
+ async ValueTask IGraphQLAuthorityInvoker.InvokeTransformableAllowMissing(Func>> authorityInvoker)
where TApiModel : default
{
ArgumentNullException.ThrowIfNull(authorityInvoker);
- var authorityResponse = await authorityInvoker(Authority);
+ var requirementsGate = authorityInvoker(Authority);
+ var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
ThrowGraphQLErrorIfNecessary(authorityResponse, false);
var result = authorityResponse.Result;
if (result == null)
@@ -70,11 +84,49 @@ namespace Tgstation.Server.Host.Authority.Core
}
///
- ValueTask IGraphQLAuthorityInvoker.Invoke(Func>> authorityInvoker)
- => ((IGraphQLAuthorityInvoker)this).InvokeAllowMissing(authorityInvoker)!;
+ async ValueTask> IGraphQLAuthorityInvoker.InvokeTransformableQueryable(
+ Func>> authorityInvoker,
+ Func, IQueryable>? preTransformer)
+ {
+ ArgumentNullException.ThrowIfNull(authorityInvoker);
+
+ var requirementsGate = authorityInvoker(Authority);
+ var queryable = await ExecuteIfRequirementsSatisfied(requirementsGate);
+
+ if (preTransformer != null)
+ queryable = preTransformer(queryable);
+
+ if (typeof(EntityId).IsAssignableFrom(typeof(TResult)))
+ queryable = queryable.OrderBy(item => ((EntityId)(object)item).Id!.Value); // order by ID to fix an EFCore warning
+
+ var expression = new TTransformer().Expression;
+ return queryable
+ .Select(expression);
+ }
///
- ValueTask IGraphQLAuthorityInvoker.InvokeTransformable(Func>> authorityInvoker)
- => ((IGraphQLAuthorityInvoker)this).InvokeTransformableAllowMissing(authorityInvoker)!;
+ async ValueTask IGraphQLAuthorityInvoker.Invoke(Func>> authorityInvoker)
+ => await ((IGraphQLAuthorityInvoker)this).InvokeAllowMissing(authorityInvoker)
+ ?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!");
+
+ ///
+ async ValueTask IGraphQLAuthorityInvoker.InvokeTransformable(Func>> authorityInvoker)
+ => await ((IGraphQLAuthorityInvoker)this).InvokeTransformableAllowMissing(authorityInvoker)
+ ?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!");
+
+ ///
+ protected override void OnRequirementsFailure(AuthorizationFailure authFailure)
+ => throw authFailure.ForbiddenGraphQLException();
+
+ ///
+ /// Unwrap a result, throwing a if they weren't met.
+ ///
+ /// The contained by the .
+ /// The result.
+ /// A resulting in the if the requirements were met.
+ /// Throw when requirements were not met.
+ new async ValueTask ExecuteIfRequirementsSatisfied(RequirementsGated requirementsGate)
+ where TResult : class
+ => (await base.ExecuteIfRequirementsSatisfied(requirementsGate))!; // base class throws if requirements evaluation fails
}
}
diff --git a/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs
index 9b49bd9dda..98270fcc96 100644
--- a/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs
+++ b/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs
@@ -1,7 +1,6 @@
using System;
using System.Linq;
-
-using Tgstation.Server.Host.Models;
+using System.Threading.Tasks;
namespace Tgstation.Server.Host.Authority.Core
{
@@ -16,21 +15,8 @@ namespace Tgstation.Server.Host.Authority.Core
/// Invoke a method and get the result.
///
/// The returned .
- /// The returning a .
- /// A returned.
- IQueryable InvokeQueryable(Func> authorityInvoker);
-
- ///
- /// Invoke a method and get the transformed result.
- ///
- /// The returned by the .
- /// The returned .
- /// The for converting s to s.
- /// The returning a .
- /// A returned.
- IQueryable InvokeTransformableQueryable(Func> authorityInvoker)
- where TResult : IApiTransformable
- where TApiModel : notnull
- where TTransformer : ITransformer, new();
+ /// The authority invocation returning a .
+ /// A resulting in the returned on success or if the requirements weren't satisfied.
+ ValueTask?> InvokeQueryable(Func>> authorityInvoker);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs
new file mode 100644
index 0000000000..cf724d4605
--- /dev/null
+++ b/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Microsoft.AspNetCore.Authorization;
+
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Authority.Core
+{
+ ///
+ /// Evaluates a set of s to be checked before executing a response.
+ ///
+ /// The of object the response generates.
+ public sealed class RequirementsGated
+ {
+ ///
+ /// The retrieval function. is included automatically.
+ ///
+ readonly Func>> getRequirements;
+
+ ///
+ /// The response generation function.
+ ///
+ readonly Func> getResponse;
+
+ ///
+ /// If the should not be added.
+ ///
+ readonly bool doNotAddUserSessionValidRequirement;
+
+ ///
+ /// Convert a given into a .
+ ///
+ /// The to convert.
+ /// A new based on .
+#pragma warning disable CA1000 // Do not declare static members on generic types
+ public static RequirementsGated FromResult(TResult result)
+#pragma warning restore CA1000 // Do not declare static members on generic types
+ => new(
+ () => (IAuthorizationRequirement?)null,
+ () => ValueTask.FromResult(result));
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of . Resulting in a value is eqivalent to returning an empty of s.
+ /// The value of .
+ public RequirementsGated(
+ Func> getRequirement,
+ Func> getResponse)
+ {
+ ArgumentNullException.ThrowIfNull(getRequirement);
+ ArgumentNullException.ThrowIfNull(getResponse);
+ getRequirements = async () =>
+ {
+ var requirement = await getRequirement();
+ if (requirement == null)
+ return Enumerable.Empty();
+
+ return new List
+ {
+ requirement,
+ };
+ };
+ this.getResponse = _ => getResponse();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The value of .
+ public RequirementsGated(
+ Func> getRequirements,
+ Func> getResponse)
+ {
+ ArgumentNullException.ThrowIfNull(getRequirements);
+ ArgumentNullException.ThrowIfNull(getResponse);
+ this.getRequirements = () => ValueTask.FromResult(getRequirements());
+ this.getResponse = _ => getResponse();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of . Resulting in a value is eqivalent to returning an empty of s.
+ /// The value of .
+ /// The value of .
+ public RequirementsGated(
+ Func getRequirement,
+ Func> getResponse,
+ bool doNotAddUserSessionValidRequirement = false)
+ {
+ ArgumentNullException.ThrowIfNull(getRequirement);
+ ArgumentNullException.ThrowIfNull(getResponse);
+ getRequirements = () =>
+ {
+ var requirement = getRequirement();
+ if (requirement == null)
+ return ValueTask.FromResult(Enumerable.Empty());
+
+ return ValueTask.FromResult>(
+ new List
+ {
+ requirement,
+ });
+ };
+
+ this.getResponse = _ => getResponse();
+
+ this.doNotAddUserSessionValidRequirement = doNotAddUserSessionValidRequirement;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of . Resulting in a value is eqivalent to returning an empty of s.
+ /// The value of .
+ public RequirementsGated(
+ Func getRequirement,
+ Func> getResponse)
+ {
+ ArgumentNullException.ThrowIfNull(getRequirement);
+ getRequirements = () =>
+ {
+ var requirement = getRequirement();
+ if (requirement == null)
+ return ValueTask.FromResult(Enumerable.Empty());
+
+ return ValueTask.FromResult>(
+ new List
+ {
+ requirement,
+ });
+ };
+
+ this.getResponse = getResponse ?? throw new ArgumentNullException(nameof(getResponse));
+ }
+
+ ///
+ /// Evaluates the s of the request.
+ ///
+ /// A resulting in the s for the request.
+ public async ValueTask> GetRequirements()
+ {
+ var requirements = await getRequirements();
+ if (!doNotAddUserSessionValidRequirement)
+ requirements = UserSessionValidRequirement.InstanceAsEnumerable.Concat(requirements);
+
+ return requirements;
+ }
+
+ ///
+ /// Executes the request.
+ ///
+ /// The authorization service to use.
+ /// A resulting in the request .
+ public ValueTask Execute(Security.IAuthorizationService authorizationService)
+ => getResponse(authorizationService);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs
index 52532c2f0e..f0ac777801 100644
--- a/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs
+++ b/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
using Tgstation.Server.Host.Controllers;
using Tgstation.Server.Host.Extensions;
+using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority.Core
{
@@ -22,7 +23,10 @@ namespace Tgstation.Server.Host.Authority.Core
/// An for the .
/// The result returned in the .
/// The REST API result model built from .
- static IActionResult CreateSuccessfulActionResult(ApiController controller, Func resultTransformer, AuthorityResponse authorityResponse)
+ static IActionResult CreateSuccessfulActionResult(
+ ApiController controller,
+ Func resultTransformer,
+ AuthorityResponse authorityResponse)
where TApiModel : notnull
{
if (authorityResponse.IsNoContent!.Value)
@@ -44,9 +48,14 @@ namespace Tgstation.Server.Host.Authority.Core
///
/// The to use.
/// The .
- /// An if the is not successful, otherwise.
- static IActionResult? CreateErroredActionResult(ApiController controller, AuthorityResponse authorityResponse)
+ /// An if the is not successful, otherwise. If is returned, is not .
+ static IActionResult? CreateErroredActionResult(
+ ApiController controller,
+ AuthorityResponse? authorityResponse)
{
+ if (authorityResponse == null)
+ return controller.Forbid();
+
if (authorityResponse.Success)
return null;
@@ -74,47 +83,51 @@ namespace Tgstation.Server.Host.Authority.Core
/// Initializes a new instance of the class.
///
/// The .
- public RestAuthorityInvoker(TAuthority authority)
- : base(authority)
+ /// The to use.
+ public RestAuthorityInvoker(TAuthority authority, IAuthorizationService authorizationService)
+ : base(authority, authorizationService)
{
}
///
- async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func> authorityInvoker)
+ async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func> authorityInvoker)
{
ArgumentNullException.ThrowIfNull(controller);
ArgumentNullException.ThrowIfNull(authorityInvoker);
- var authorityResponse = await authorityInvoker(Authority);
+ var requirementsGate = authorityInvoker(Authority);
+ var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
return CreateErroredActionResult(controller, authorityResponse) ?? controller.NoContent();
}
///
- async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func>> authorityInvoker)
+ async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func>> authorityInvoker)
{
ArgumentNullException.ThrowIfNull(controller);
ArgumentNullException.ThrowIfNull(authorityInvoker);
- var authorityResponse = await authorityInvoker(Authority);
+ var requirementsGate = authorityInvoker(Authority);
+ var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
var erroredResult = CreateErroredActionResult(controller, authorityResponse);
if (erroredResult != null)
return erroredResult;
- return CreateSuccessfulActionResult(controller, result => result, authorityResponse);
+ return CreateSuccessfulActionResult(controller, result => result, authorityResponse!);
}
///
- async ValueTask IRestAuthorityInvoker.InvokeTransformable(ApiController controller, Func>> authorityInvoker)
+ async ValueTask IRestAuthorityInvoker.InvokeTransformable(ApiController controller, Func>> authorityInvoker)
{
ArgumentNullException.ThrowIfNull(controller);
ArgumentNullException.ThrowIfNull(authorityInvoker);
- var authorityResponse = await authorityInvoker(Authority);
+ var requirementsGate = authorityInvoker(Authority);
+ var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
var erroredResult = CreateErroredActionResult(controller, authorityResponse);
if (erroredResult != null)
return erroredResult;
- return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse);
+ return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse!);
}
}
}
diff --git a/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs b/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs
index 7fb28925b5..6853c89ca8 100644
--- a/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs
@@ -1,11 +1,8 @@
using System;
using System.Threading;
-using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Response;
-using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Authority.Core;
-using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority
{
@@ -19,9 +16,8 @@ namespace Tgstation.Server.Host.Authority
///
/// Bypass the caching that the authority performs for this request, forcing it to contact GitHub.
/// The for the operation.
- /// A resulting in the .
- [TgsAuthorize(AdministrationRights.ChangeVersion)]
- ValueTask> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken);
+ /// A .
+ RequirementsGated> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken);
///
/// Triggers a restart of tgstation-server without terminating running game instances, setting its version to a given .
@@ -29,15 +25,21 @@ namespace Tgstation.Server.Host.Authority
/// The TGS will switch to upon reboot.
/// If a will be returned and the call must provide an uploaded zip file containing the update data to the file transfer service.
/// The for the operation.
- /// A resulting in the .
- [TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)]
- ValueTask> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken);
+ /// A .
+ RequirementsGated> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken);
///
/// Triggers a restart of tgstation-server without terminating running game instances.
///
- /// A resulting in the .
- [TgsAuthorize(AdministrationRights.RestartHost)]
- ValueTask TriggerServerRestart();
+ /// A .
+ RequirementsGated TriggerServerRestart();
+
+ ///
+ /// Get a ticket for downloading a log file at a given .
+ ///
+ /// The relative path to the log file in the directory.
+ /// The for the operation.
+ /// A .
+ RequirementsGated> GetLog(string path, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs
index edface219a..341b78a6cd 100644
--- a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs
+++ b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Threading.Tasks;
using Tgstation.Server.Host.Authority.Core;
@@ -10,24 +11,25 @@ namespace Tgstation.Server.Host.Authority
/// Invokes s from GraphQL endpoints.
///
/// The invoked.
+ /// We take the approach that fields should be non-nullable if that is the case under ideal circumstances. Authorization issues should throw.
public interface IGraphQLAuthorityInvoker : IAuthorityInvoker
where TAuthority : IAuthority
{
///
/// Invoke a method with no success result.
///
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A representing the running operation.
- ValueTask Invoke(Func> authorityInvoker);
+ ValueTask Invoke(Func> authorityInvoker);
///
/// Invoke a method and get the result.
///
/// The .
/// The resulting of the return value.
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask InvokeAllowMissing(Func>> authorityInvoker)
+ ValueTask InvokeAllowMissing(Func>> authorityInvoker)
where TResult : TApiModel
where TApiModel : notnull;
@@ -37,9 +39,9 @@ namespace Tgstation.Server.Host.Authority
/// The .
/// The resulting of the return value.
/// The for converting s to s.
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask InvokeTransformableAllowMissing(Func>> authorityInvoker)
+ ValueTask InvokeTransformableAllowMissing(Func>> authorityInvoker)
where TResult : notnull, IApiTransformable
where TApiModel : notnull
where TTransformer : ITransformer, new();
@@ -49,9 +51,9 @@ namespace Tgstation.Server.Host.Authority
///
/// The .
/// The resulting of the return value.
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask Invoke(Func>> authorityInvoker)
+ ValueTask Invoke(Func>> authorityInvoker)
where TResult : TApiModel
where TApiModel : notnull;
@@ -61,11 +63,27 @@ namespace Tgstation.Server.Host.Authority
/// The .
/// The resulting of the return value.
/// The for converting s to s.
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask InvokeTransformable(Func>> authorityInvoker)
+ ValueTask InvokeTransformable(Func>> authorityInvoker)
where TResult : notnull, IApiTransformable
where TApiModel : notnull
where TTransformer : ITransformer, new();
+
+ ///
+ /// Invoke a method and get the transformed result.
+ ///
+ /// The returned by the .
+ /// The returned .
+ /// The for converting s to s.
+ /// The returning a .
+ /// Optional transformer for the run once it has been acquired.
+ /// A resulting in the returned on success or if the requirements weren't satisfied.
+ ValueTask> InvokeTransformableQueryable(
+ Func>> authorityInvoker,
+ Func, IQueryable>? preTransformer = null)
+ where TResult : IApiTransformable
+ where TApiModel : notnull
+ where TTransformer : ITransformer, new();
}
}
diff --git a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs
index 0558bbfaa0..111d83271f 100644
--- a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs
@@ -16,13 +16,13 @@ namespace Tgstation.Server.Host.Authority
///
/// The for the operation.
/// A resulting in a .
- ValueTask> AttemptLogin(CancellationToken cancellationToken);
+ RequirementsGated> AttemptLogin(CancellationToken cancellationToken);
///
/// Attempt to login to an OAuth service with the current OAuth credentials.
///
/// The for the operation.
/// A resulting in an .
- ValueTask> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
+ RequirementsGated> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs b/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs
index bb10fed383..8e4a370087 100644
--- a/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs
@@ -1,10 +1,8 @@
using System.Threading;
using System.Threading.Tasks;
-using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Authority.Core;
using Tgstation.Server.Host.Models;
-using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority
{
@@ -20,7 +18,6 @@ namespace Tgstation.Server.Host.Authority
/// The of .
/// The for the operation.
/// A resulting in a .
- [TgsAuthorize(AdministrationRights.ReadUsers)]
- ValueTask> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken);
+ RequirementsGated> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs
index 2cb05a2b9a..0e3b6f3b14 100644
--- a/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs
+++ b/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs
@@ -20,9 +20,9 @@ namespace Tgstation.Server.Host.Authority
/// Invoke a method with no success result.
///
/// The invoking the .
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask Invoke(ApiController controller, Func> authorityInvoker);
+ ValueTask Invoke(ApiController controller, Func> authorityInvoker);
///
/// Invoke a method and get the result.
@@ -30,9 +30,9 @@ namespace Tgstation.Server.Host.Authority
/// The .
/// The resulting of the .
/// The invoking the .
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask Invoke(ApiController controller, Func>> authorityInvoker)
+ ValueTask Invoke(ApiController controller, Func>> authorityInvoker)
where TResult : TApiModel
where TApiModel : notnull;
@@ -42,9 +42,9 @@ namespace Tgstation.Server.Host.Authority
/// The .
/// The returned REST .
/// The invoking the .
- /// The returning a resulting in the .
+ /// The resulting in the .
/// A resulting in the generated for the resulting .
- ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker)
+ ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker)
where TResult : notnull, ILegacyApiTransformable
where TApiModel : notnull;
}
diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
index f60275aceb..2aba4ddaef 100644
--- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
@@ -1,13 +1,10 @@
using System.Linq;
using System.Threading;
-using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Request;
-using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Authority.Core;
using Tgstation.Server.Host.Models;
-using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority
{
@@ -20,9 +17,8 @@ namespace Tgstation.Server.Host.Authority
/// Gets the currently authenticated user.
///
/// The for the operation.
- /// A resulting in a .
- [TgsAuthorize]
- ValueTask> Read(CancellationToken cancellationToken);
+ /// A .
+ RequirementsGated> Read(CancellationToken cancellationToken);
///
/// Gets the with a given .
@@ -31,33 +27,31 @@ namespace Tgstation.Server.Host.Authority
/// If related entities should be loaded.
/// If the may be returned.
/// The for the operation.
- /// A resulting in a .
- [TgsAuthorize(AdministrationRights.ReadUsers)]
- ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
+ /// A .
+ RequirementsGated> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
///
/// Gets the s for the with a given .
///
/// The of the .
/// The for the operation.
- /// A resulting in an of .
- ValueTask> OAuthConnections(long userId, CancellationToken cancellationToken);
+ /// A of .
+ RequirementsGated> OAuthConnections(long userId, CancellationToken cancellationToken);
///
/// Gets the s for the with a given .
///
/// The of the .
/// The for the operation.
- /// A resulting in an of .
- ValueTask> OidcConnections(long userId, CancellationToken cancellationToken);
+ /// A of .
+ RequirementsGated> OidcConnections(long userId, CancellationToken cancellationToken);
///
/// Gets all registered s.
///
/// If related entities should be loaded.
- /// A of s.
- [TgsAuthorize(AdministrationRights.ReadUsers)]
- IQueryable Queryable(bool includeJoins);
+ /// A of s.
+ RequirementsGated> Queryable(bool includeJoins);
///
/// Creates a .
@@ -65,9 +59,8 @@ namespace Tgstation.Server.Host.Authority
/// The .
/// If a zero-length indicates and OAuth only user.
/// The for the operation.
- /// A resulting in am for the created .
- [TgsAuthorize(AdministrationRights.WriteUsers)]
- ValueTask> Create(
+ /// A for the created .
+ RequirementsGated> Create(
UserCreateRequest createRequest,
bool? needZeroLengthPasswordWithOAuthConnections,
CancellationToken cancellationToken);
@@ -77,8 +70,7 @@ namespace Tgstation.Server.Host.Authority
///
/// The .
/// The for the operation.
- /// A resulting in am for the created .
- [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnServiceConnections)]
- ValueTask> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
+ /// A for the created .
+ RequirementsGated> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs
index 28113b95f5..c63c417ab1 100644
--- a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs
@@ -17,8 +17,9 @@ namespace Tgstation.Server.Host.Authority
///
/// Gets the current .
///
+ /// The for the operation.
/// A resulting in a .
- ValueTask> Read();
+ RequirementsGated> Read(CancellationToken cancellationToken);
///
/// Gets the with a given .
@@ -26,17 +27,17 @@ namespace Tgstation.Server.Host.Authority
/// The of the .
/// If related entities should be loaded.
/// The for the operation.
- /// A resulting in a .
+ /// A .
[TgsAuthorize(AdministrationRights.ReadUsers)]
- ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
+ RequirementsGated> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
///
/// Gets all registered s.
///
/// If related entities should be loaded.
- /// A of s.
+ /// A of s.
[TgsAuthorize(AdministrationRights.ReadUsers)]
- IQueryable Queryable(bool includeJoins);
+ RequirementsGated> Queryable(bool includeJoins);
///
/// Create a .
@@ -44,9 +45,9 @@ namespace Tgstation.Server.Host.Authority
/// The created 's .
/// The created 's .
/// The for the operation.
- /// A resulting in a .
+ /// A .
[TgsAuthorize(AdministrationRights.WriteUsers)]
- ValueTask> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken);
+ RequirementsGated> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken);
///
/// Updates a .
@@ -55,17 +56,17 @@ namespace Tgstation.Server.Host.Authority
/// The optional new for the .
/// The optional new for the .
/// The for the operation.
- /// A resulting in a .
+ /// A resulting in a .
[TgsAuthorize(AdministrationRights.WriteUsers)]
- ValueTask> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken);
+ RequirementsGated> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken);
///
/// Deletes an empty .
///
/// The of the to delete.
/// The for the operation.
- /// A representing the running operation.
+ /// A representing the running operation.
[TgsAuthorize(AdministrationRights.WriteUsers)]
- ValueTask DeleteEmpty(long id, CancellationToken cancellationToken);
+ RequirementsGated DeleteEmpty(long id, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs
index ab8af50f91..f1b2b8b47c 100644
--- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -103,7 +104,6 @@ namespace Tgstation.Server.Host.Authority
///
/// Initializes a new instance of the class.
///
- /// The to use.
/// The to use.
/// The to use.
/// The value of .
@@ -115,7 +115,6 @@ namespace Tgstation.Server.Host.Authority
/// The value of .
/// The containing the value of .
public LoginAuthority(
- IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger logger,
IApiHeadersProvider apiHeadersProvider,
@@ -127,7 +126,6 @@ namespace Tgstation.Server.Host.Authority
ISessionInvalidationTracker sessionInvalidationTracker,
IOptions securityConfigurationOptions)
: base(
- authenticationContext,
databaseContext,
logger)
{
@@ -142,7 +140,45 @@ namespace Tgstation.Server.Host.Authority
}
///
- public async ValueTask> AttemptLogin(CancellationToken cancellationToken)
+ public RequirementsGated> AttemptLogin(CancellationToken cancellationToken)
+ => new(
+ () => null,
+ () => AttemptLoginImpl(cancellationToken),
+ true);
+
+ ///
+ public RequirementsGated> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
+ => new(
+ () => (IAuthorizationRequirement?)null,
+ async () =>
+ {
+ var headers = apiHeadersProvider.ApiHeaders;
+ if (headers == null)
+ return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!);
+
+ var oAuthProvider = headers.OAuthProvider;
+ if (!oAuthProvider.HasValue)
+ return BadRequest(ErrorCode.BadHeaders);
+
+ var (errorResponse, oAuthResult) = await TryOAuthenticate(headers, oAuthProvider.Value, false, cancellationToken);
+ if (errorResponse != null)
+ return errorResponse;
+
+ Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
+
+ return new(
+ new OAuthGatewayLoginResult
+ {
+ AccessCode = oAuthResult!.Value.AccessCode,
+ });
+ });
+
+ ///
+ /// Login process.
+ ///
+ /// The for the operation.
+ /// A resulting in the for the .
+ private async ValueTask> AttemptLoginImpl(CancellationToken cancellationToken)
{
// password and oauth logins disabled
if (securityConfiguration.OidcStrictMode)
@@ -278,30 +314,6 @@ namespace Tgstation.Server.Host.Authority
}
}
- ///
- public async ValueTask> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
- {
- var headers = apiHeadersProvider.ApiHeaders;
- if (headers == null)
- return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!);
-
- var oAuthProvider = headers.OAuthProvider;
- if (!oAuthProvider.HasValue)
- return BadRequest(ErrorCode.BadHeaders);
-
- var (errorResponse, oAuthResult) = await TryOAuthenticate(headers, oAuthProvider.Value, false, cancellationToken);
- if (errorResponse != null)
- return errorResponse;
-
- Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
-
- return new AuthorityResponse(
- new OAuthGatewayLoginResult
- {
- AccessCode = oAuthResult!.Value.AccessCode,
- });
- }
-
///
/// Add a given to the .
///
diff --git a/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs b/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs
index a252aae1cc..1fcfee647c 100644
--- a/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Authority.Core;
using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -25,6 +26,11 @@ namespace Tgstation.Server.Host.Authority
///
readonly IPermissionSetsDataLoader permissionSetsDataLoader;
+ ///
+ /// The for the .
+ ///
+ readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
+
///
/// Implements .
///
@@ -85,34 +91,60 @@ namespace Tgstation.Server.Host.Authority
///
/// Initializes a new instance of the class.
///
- /// The to use.
/// The to use.
/// The to use.
/// The value of .
+ /// The value of .
public PermissionSetAuthority(
- IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger logger,
- IPermissionSetsDataLoader permissionSetsDataLoader)
+ IPermissionSetsDataLoader permissionSetsDataLoader,
+ IClaimsPrincipalAccessor claimsPrincipalAccessor)
: base(
- authenticationContext,
databaseContext,
logger)
{
this.permissionSetsDataLoader = permissionSetsDataLoader ?? throw new ArgumentNullException(nameof(permissionSetsDataLoader));
+ this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor));
}
///
- public async ValueTask> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken)
+ public RequirementsGated> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken)
{
- if (id != AuthenticationContext.PermissionSet.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
- return Forbid();
+ var permissionSetTask = permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken);
+ return new(
+ async () =>
+ {
+ var userId = claimsPrincipalAccessor.User.GetTgsUserId();
- var permissionSet = await permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken);
- if (permissionSet == null)
- return NotFound();
+ var groupIdQuery = DatabaseContext
+ .Users
+ .AsQueryable()
+ .Where(user => user.Id == userId)
+ .Select(user => user.GroupId);
- return new AuthorityResponse(permissionSet);
+ var permissionSetId = await DatabaseContext
+ .PermissionSets
+ .AsQueryable()
+ .Where(permissionSet => permissionSet.UserId == userId
+ || groupIdQuery.Contains(permissionSet.GroupId))
+ .Select(permissionSet => permissionSet.Id!.Value)
+ .FirstAsync(cancellationToken);
+
+ if (permissionSetId == id)
+ return null;
+
+ return Flag(AdministrationRights.ReadUsers);
+ },
+ async () =>
+ {
+ var permissionSet = await permissionSetTask;
+
+ if (permissionSet == null)
+ return NotFound();
+
+ return new AuthorityResponse(permissionSet);
+ });
}
}
}
diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
index e9577dbcfd..8bee7fd0d0 100644
--- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
@@ -9,6 +9,7 @@ using GreenDonut;
using HotChocolate.Subscriptions;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -22,9 +23,11 @@ using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Authority.Core;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Models.Transformers;
using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Security.RightsEvaluation;
namespace Tgstation.Server.Host.Authority
{
@@ -71,6 +74,11 @@ namespace Tgstation.Server.Host.Authority
///
readonly ITopicEventSender topicEventSender;
+ ///
+ /// The for the .
+ ///
+ readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
+
///
/// The of for the .
///
@@ -164,22 +172,21 @@ namespace Tgstation.Server.Host.Authority
/// The to check.
/// If this is a new .
/// if is valid, an errored otherwise.
- static AuthorityResponse? CheckValidName(UserUpdateRequest model, bool newUser)
+ static AuthorityResponse? CheckValidName(UserUpdateRequest model, bool newUser)
{
var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null;
if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
- return BadRequest(ErrorCode.UserMissingName);
+ return BadRequest(ErrorCode.UserMissingName);
model.Name = model.Name?.Trim();
if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
- return BadRequest(ErrorCode.UserColonInName);
+ return BadRequest(ErrorCode.UserColonInName);
return null;
}
///
/// Initializes a new instance of the class.
///
- /// The to use.
/// The to use.
/// The to use.
/// The value of .
@@ -190,10 +197,10 @@ namespace Tgstation.Server.Host.Authority
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The value of .
/// The value of .
public UserAuthority(
- IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger logger,
IUsersDataLoader usersDataLoader,
@@ -204,10 +211,10 @@ namespace Tgstation.Server.Host.Authority
ICryptographySuite cryptographySuite,
ISessionInvalidationTracker sessionInvalidationTracker,
ITopicEventSender topicEventSender,
+ IClaimsPrincipalAccessor claimsPrincipalAccessor,
IOptionsSnapshot generalConfigurationOptions,
IOptions securityConfigurationOptions)
: base(
- authenticationContext,
databaseContext,
logger)
{
@@ -219,6 +226,7 @@ namespace Tgstation.Server.Host.Authority
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker));
this.topicEventSender = topicEventSender ?? throw new ArgumentNullException(nameof(topicEventSender));
+ this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor));
this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
}
@@ -233,11 +241,11 @@ namespace Tgstation.Server.Host.Authority
static bool BadCreateRequestChecks(
UserCreateRequest createRequest,
bool? needZeroLengthPasswordWithOAuthConnections,
- [NotNullWhen(true)] out AuthorityResponse? failResponse)
+ [NotNullWhen(true)] out AuthorityResponse? failResponse)
{
if (createRequest.OAuthConnections?.Any(x => x == null) == true)
{
- failResponse = BadRequest(ErrorCode.ModelValidationFailure);
+ failResponse = BadRequest(ErrorCode.ModelValidationFailure);
return true;
}
@@ -247,7 +255,7 @@ namespace Tgstation.Server.Host.Authority
if ((hasNonNullPassword && hasNonNullSystemIdentifier)
|| (!hasNonNullPassword && !hasNonNullSystemIdentifier && !hasOAuthConnections))
{
- failResponse = BadRequest(ErrorCode.UserMismatchPasswordSid);
+ failResponse = BadRequest(ErrorCode.UserMismatchPasswordSid);
return true;
}
@@ -261,20 +269,20 @@ namespace Tgstation.Server.Host.Authority
if (createRequest.OAuthConnections.Count == 0)
{
- failResponse = BadRequest(ErrorCode.ModelValidationFailure);
+ failResponse = BadRequest(ErrorCode.ModelValidationFailure);
return true;
}
}
else if (hasZeroLengthPassword)
{
- failResponse = BadRequest(ErrorCode.ModelValidationFailure);
+ failResponse = BadRequest(ErrorCode.ModelValidationFailure);
return true;
}
}
if (createRequest.Group != null && createRequest.PermissionSet != null)
{
- failResponse = BadRequest(ErrorCode.UserGroupAndPermissionSet);
+ failResponse = BadRequest(ErrorCode.UserGroupAndPermissionSet);
return true;
}
@@ -284,7 +292,7 @@ namespace Tgstation.Server.Host.Authority
if (!(createRequest.Name == null ^ createRequest.SystemIdentifier == null))
{
- failResponse = BadRequest(ErrorCode.UserMismatchNameSid);
+ failResponse = BadRequest(ErrorCode.UserMismatchNameSid);
return true;
}
@@ -293,15 +301,350 @@ namespace Tgstation.Server.Host.Authority
}
///
- public ValueTask> Read(CancellationToken cancellationToken)
- => ValueTask.FromResult(new AuthorityResponse(AuthenticationContext.User));
+ public RequirementsGated> Read(CancellationToken cancellationToken)
+ => new(
+ () => Enumerable.Empty(),
+ () => GetIdImpl(claimsPrincipalAccessor.User.RequireTgsUserId(), true, false, cancellationToken));
///
- public async ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
- {
- if (id != AuthenticationContext.User.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
- return Forbid();
+ public RequirementsGated> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
+ => new(
+ () =>
+ {
+ if (id != claimsPrincipalAccessor.User.GetTgsUserId())
+ return Enumerable.Empty();
+ return new List
+ {
+ Flag(AdministrationRights.ReadUsers),
+ };
+ },
+ () => GetIdImpl(id, includeJoins, allowSystemUser, cancellationToken));
+
+ ///
+ public RequirementsGated> Queryable(bool includeJoins)
+ => new(
+ () => Flag(AdministrationRights.ReadUsers),
+ () => ValueTask.FromResult(Queryable(includeJoins, false)));
+
+ ///
+ public RequirementsGated> OAuthConnections(long userId, CancellationToken cancellationToken)
+ => new(
+ () => claimsPrincipalAccessor.User.GetTgsUserId() != userId
+ ? Flag(AdministrationRights.ReadUsers)
+ : null,
+ async () => new AuthorityResponse(
+ await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)));
+
+ ///
+ public RequirementsGated> OidcConnections(long userId, CancellationToken cancellationToken)
+ => new(
+ () => claimsPrincipalAccessor.User.GetTgsUserId() != userId
+ ? Flag(AdministrationRights.ReadUsers)
+ : null,
+ async () => new AuthorityResponse(
+ await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)));
+
+ ///