diff --git a/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs index f160d48af6..e5eacacafa 100644 --- a/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs @@ -85,7 +85,28 @@ namespace Tgstation.Server.Host.Authority.Core if (result == null) return default; - return result.ToApi(); + var transformedResult = new TTransformer().CompiledExpression(result); + return transformedResult; + } + + /// + async ValueTask IGraphQLAuthorityInvoker.InvokeTransformableAllowMissing( + Func>> authorityInvoker, + QueryContext? queryContext) + where TApiModel : default + { + ArgumentNullException.ThrowIfNull(authorityInvoker); + + var requirementsGate = authorityInvoker(Authority); + var projectable = await ExecuteIfRequirementsSatisfied(requirementsGate); + var authorityResponse = await projectable.Resolve( + queryable => queryable + .Select(new TTransformer().ProjectedExpression) + .With(queryContext)); + ThrowGraphQLErrorIfNecessary(authorityResponse, false); + var result = authorityResponse.Result; + + return result; } /// @@ -102,7 +123,10 @@ namespace Tgstation.Server.Host.Authority.Core 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 + queryable = queryable + .Cast() + .OrderBy(item => item.Id!.Value) // order by ID to fix an EFCore warning + .Cast(); var expression = new TTransformer().Expression; return queryable @@ -148,6 +172,13 @@ namespace Tgstation.Server.Host.Authority.Core => await ((IGraphQLAuthorityInvoker)this).InvokeTransformableAllowMissing(authorityInvoker) ?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!"); + /// + async ValueTask IGraphQLAuthorityInvoker.InvokeTransformable( + Func>> authorityInvoker, + QueryContext? queryContext) + => await ((IGraphQLAuthorityInvoker)this).InvokeTransformable(authorityInvoker, queryContext) + ?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!"); + /// protected override void OnRequirementsFailure(AuthorizationFailure authFailure) => throw authFailure.ForbiddenGraphQLException(); diff --git a/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs index f0ac777801..70c52deede 100644 --- a/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs @@ -129,5 +129,31 @@ namespace Tgstation.Server.Host.Authority.Core return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse!); } + + /// + async ValueTask IRestAuthorityInvoker.InvokeTransformable( + ApiController controller, + Func>> authorityInvoker) + { + ArgumentNullException.ThrowIfNull(controller); + ArgumentNullException.ThrowIfNull(authorityInvoker); + + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); + var erroredResult = CreateErroredActionResult(controller, authorityResponse); + if (erroredResult != null) + return erroredResult; + + return CreateSuccessfulActionResult(controller, result => new TTransformer().CompiledExpression(result), authorityResponse!); + } + + /// + async ValueTask IRestAuthorityInvoker.InvokeTransformable( + ApiController controller, + Func>> authorityInvoker) + { + await Task.Yield(); + throw new NotImplementedException(); + } } } diff --git a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs index 12d044844a..d3cc6e9146 100644 --- a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs @@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Authority /// The . /// The resulting of the return value. /// The resulting in the . - /// A resulting in the generated for the resulting . + /// A resulting in the generated for the resulting if any. ValueTask InvokeAllowMissing(Func>> authorityInvoker) where TResult : TApiModel where TApiModel : notnull; @@ -45,9 +45,25 @@ namespace Tgstation.Server.Host.Authority /// The resulting of the return value. /// The for converting s to s. /// The resulting in the . - /// A resulting in the generated for the resulting . + /// A resulting in the generated for the resulting if any. ValueTask InvokeTransformableAllowMissing(Func>> authorityInvoker) - where TResult : notnull, IApiTransformable + where TResult : notnull + where TApiModel : notnull + where TTransformer : ITransformer, new(); + + /// + /// Invoke a method and get the non-nullable result. + /// + /// The . + /// The resulting of the return value. + /// The for converting s to s. + /// The resulting in the . + /// The active . + /// A resulting in the generated for the resulting if any. + ValueTask InvokeTransformableAllowMissing( + Func>> authorityInvoker, + QueryContext? queryContext) + where TResult : EntityId where TApiModel : notnull where TTransformer : ITransformer, new(); @@ -71,7 +87,23 @@ namespace Tgstation.Server.Host.Authority /// The resulting in the . /// A resulting in the generated for the resulting . ValueTask InvokeTransformable(Func>> authorityInvoker) - where TResult : notnull, IApiTransformable + where TResult : notnull + where TApiModel : notnull + where TTransformer : ITransformer, new(); + + /// + /// Invoke a method and get the non-nullable result. + /// + /// The . + /// The resulting of the return value. + /// The for converting s to s. + /// The resulting in the . + /// The active . + /// A resulting in the generated for the resulting . + ValueTask InvokeTransformable( + Func>> authorityInvoker, + QueryContext? queryContext) + where TResult : EntityId where TApiModel : notnull where TTransformer : ITransformer, new(); @@ -87,7 +119,6 @@ namespace Tgstation.Server.Host.Authority ValueTask> InvokeTransformableQueryable( Func>> authorityInvoker, Func, IQueryable>? preTransformer = null) - where TResult : IApiTransformable where TApiModel : notnull where TTransformer : ITransformer, new(); @@ -105,7 +136,7 @@ namespace Tgstation.Server.Host.Authority Func>> authorityInvoker, IReadOnlyList ids, QueryContext>? queryContext) - where TResult : EntityId, IApiTransformable + where TResult : EntityId where TApiModel : Entity where TTransformer : ITransformer, new(); } diff --git a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs index 111d83271f..527cf41b85 100644 --- a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs @@ -15,8 +15,8 @@ namespace Tgstation.Server.Host.Authority /// Attempt to login to the server with the current Basic or OAuth credentials. /// /// The for the operation. - /// A resulting in a . - RequirementsGated> AttemptLogin(CancellationToken cancellationToken); + /// A resulting in an authenticated token . + RequirementsGated> AttemptLogin(CancellationToken cancellationToken); /// /// Attempt to login to an OAuth service with the current OAuth credentials. diff --git a/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs index 0e3b6f3b14..3b993ded6c 100644 --- a/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Models; @@ -47,5 +48,33 @@ namespace Tgstation.Server.Host.Authority ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) where TResult : notnull, ILegacyApiTransformable where TApiModel : notnull; + + /// + /// Invoke a method and get the result. + /// + /// The . + /// The returned REST . + /// The for converting s to s. + /// The invoking the . + /// The resulting in the . + /// A resulting in the generated for the resulting . + ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) + where TResult : notnull + where TApiModel : notnull + where TTransformer : ITransformer, new(); + + /// + /// Invoke a method and get the result. + /// + /// The . + /// The returned REST . + /// The for converting s to s. + /// The invoking the . + /// The resulting in the . + /// A resulting in the generated for the resulting . + ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) + where TResult : EntityId + where TApiModel : notnull + where TTransformer : ITransformer, new(); } } diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs index 4bb1fa17eb..395727833d 100644 --- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs @@ -13,33 +13,16 @@ namespace Tgstation.Server.Host.Authority /// public interface IUserAuthority : IAuthority { - /// - /// Gets the currently authenticated user. - /// - /// The for the operation. - /// A . - RequirementsGated> Read(CancellationToken cancellationToken); - - /// - /// Gets the with a given . - /// - /// The of the . - /// If related entities should be loaded. - /// If the may be returned. - /// The for the operation. - /// A . - RequirementsGated> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken); - /// /// Gets the with a given . /// /// The result type after projection. /// The of the . - /// If the may be returned. + /// If the may be returned or will result in a response. /// The for the operation. /// A for . RequirementsGated> GetId(long id, bool allowSystemUser, CancellationToken cancellationToken) - where TResult : class; + where TResult : notnull; /// /// Gets the s for the with a given . diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs index 1d1ba896db..f540ce6a4b 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -15,7 +15,6 @@ using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.GraphQL.Mutations.Payloads; -using Tgstation.Server.Host.GraphQL.Transformers; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; @@ -140,7 +139,7 @@ namespace Tgstation.Server.Host.Authority } /// - public RequirementsGated> AttemptLogin(CancellationToken cancellationToken) + public RequirementsGated> AttemptLogin(CancellationToken cancellationToken) => new( () => null, () => AttemptLoginImpl(cancellationToken), @@ -177,19 +176,19 @@ namespace Tgstation.Server.Host.Authority /// Login process. /// /// The for the operation. - /// A resulting in the for the . - private async ValueTask> AttemptLoginImpl(CancellationToken cancellationToken) + /// A resulting in the containing the authenticated bearer token. + private async ValueTask> AttemptLoginImpl(CancellationToken cancellationToken) { // password and oauth logins disabled if (securityConfigurationOptions.Value.OidcStrictMode) - return Unauthorized(); + return Unauthorized(); var headers = apiHeadersProvider.ApiHeaders; if (headers == null) - return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); + return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); if (headers.IsTokenAuthentication) - return BadRequest(ErrorCode.TokenWithToken); + return BadRequest(ErrorCode.TokenWithToken); var oAuthLogin = headers.OAuthProvider.HasValue; @@ -212,7 +211,7 @@ namespace Tgstation.Server.Host.Authority if (oAuthLogin) { var oAuthProvider = headers.OAuthProvider!.Value; - var (errorResponse, oauthResult) = await TryOAuthenticate(headers, oAuthProvider, true, cancellationToken); + var (errorResponse, oauthResult) = await TryOAuthenticate(headers, oAuthProvider, true, cancellationToken); if (errorResponse != null) return errorResponse; @@ -225,7 +224,7 @@ namespace Tgstation.Server.Host.Authority { var canonicalUserName = User.CanonicalizeName(headers.Username!); if (canonicalUserName == User.CanonicalizeName(User.TgsSystemUserName)) - return Unauthorized(); + return Unauthorized(); if (systemIdentity == null) query = query.Where(x => x.CanonicalName == canonicalUserName); @@ -237,7 +236,7 @@ namespace Tgstation.Server.Host.Authority // No user? You're not allowed if (user == null) - return Unauthorized(); + return Unauthorized(); // A system user may have had their name AND password changed to one in our DB... // Or a DB user was created that had the same user/pass as a system user @@ -252,7 +251,7 @@ namespace Tgstation.Server.Host.Authority { // DB User password check and update if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, headers.Password!)) - return Unauthorized(); + return Unauthorized(); if (user.PasswordHash != originalHash) { Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id); @@ -295,22 +294,17 @@ namespace Tgstation.Server.Host.Authority if (!user.Enabled!.Value) { Logger.LogTrace("Not logging in disabled user {userId}.", user.Id); - return Forbid(); + return Forbid(); } - var token = tokenFactory.CreateToken(user, oAuthLogin); - var payload = new LoginResult - { - Bearer = token, - User = ((IApiTransformable)user).ToApi(), - }; + var (token, expiresAt) = tokenFactory.CreateToken(user, oAuthLogin); if (usingSystemIdentity) - await CacheSystemIdentity(systemIdentity!, user, payload); + await CacheSystemIdentity(systemIdentity!, user, expiresAt); Logger.LogDebug("Successfully logged in user {userId}!", user.Id); - return new AuthorityResponse(payload); + return new AuthorityResponse(token); } } @@ -319,12 +313,12 @@ namespace Tgstation.Server.Host.Authority /// /// The to cache. /// The the was generated for. - /// The for the successful login. + /// When the user's session exipres. /// A representing the running operation. - private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, LoginResult loginPayload) + private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, DateTimeOffset validTo) { // expire the identity slightly after the auth token in case of lag - var identExpiry = loginPayload.ToApi().ParseJwt().ValidTo; + var identExpiry = validTo; identExpiry += tokenFactory.ValidationParameters.ClockSkew; identExpiry += TimeSpan.FromSeconds(15); await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs index 4d404c7a3c..5784dbd07b 100644 --- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs @@ -24,7 +24,6 @@ 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.GraphQL.Transformers; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.RightsEvaluation; @@ -229,27 +228,6 @@ namespace Tgstation.Server.Host.Authority return failResponse != null; } - /// - public RequirementsGated> Read(CancellationToken cancellationToken) - => new( - () => Enumerable.Empty(), - () => GetIdImpl(claimsPrincipalAccessor.User.RequireTgsUserId(), true, false, cancellationToken)); - - /// - 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( @@ -565,7 +543,7 @@ namespace Tgstation.Server.Host.Authority /// public RequirementsGated> GetId(long id, bool allowSystemUser, CancellationToken cancellationToken) - where TResult : class + where TResult : notnull => new( () => { @@ -600,37 +578,6 @@ namespace Tgstation.Server.Host.Authority }, cancellationToken))); - /// - /// Implementation of retrieving a by ID. - /// - /// The of the user to retrieve. - /// If related entities should be loaded. - /// If the may be returned. - /// The for the operation. - /// A . - async ValueTask> GetIdImpl(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken) - { - User? user; - if (includeJoins) - { - var queryable = Queryable(true, true); - - user = await queryable.FirstOrDefaultAsync( - dbModel => dbModel.Id == id, - cancellationToken); - } - else - user = await usersDataLoader.LoadAsync(id, cancellationToken); - - if (user == default) - return NotFound(); - - if (!allowSystemUser && user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) - return Forbid(); - - return new AuthorityResponse(user); - } - /// /// Create the for an . /// @@ -667,7 +614,7 @@ namespace Tgstation.Server.Host.Authority user.Require(x => x.Id)) .Select(topic => topicEventSender.SendAsync( topic, - ((IApiTransformable)user).ToApi(), + ((IApiTransformable)user).ToApi(), CancellationToken.None))); // DCT: Operation should always run /// diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 1b9b20f198..b56022edac 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -208,7 +208,6 @@ namespace Tgstation.Server.Host.Controllers })); } }, - null, page, pageSize, cancellationToken); diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 771972a0f7..dd61412501 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -243,19 +243,45 @@ namespace Tgstation.Server.Host.Controllers /// /// The of model being generated and returned. /// A resulting in a resulting in the generated . - /// Optional to transform the s after being queried. /// The requested page from the query. /// The requested page size from the query. /// The for the operation. /// A resulting in the for the operation. protected ValueTask Paginated( Func?>> queryGenerator, - Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) => PaginatedImpl( queryGenerator, - resultTransformer, + model => model, + null, + pageQuery, + pageSizeQuery, + cancellationToken); + + /// + /// Generates a paginated response. + /// + /// The of model being generated. + /// The of model being returned. + /// The of the for /. + /// A resulting in a resulting in the generated . + /// The requested page from the query. + /// The requested page size from the query. + /// The for the operation. + /// A resulting in the for the operation. + protected ValueTask Paginated( + Func?>> queryGenerator, + int? pageQuery, + int? pageSizeQuery, + CancellationToken cancellationToken) + where TModel : IApiTransformable + where TApiModel : notnull + where TTransformer : ITransformer, new() + => PaginatedImpl( + queryGenerator, + model => model.ToApi(), + null, pageQuery, pageSizeQuery, cancellationToken); @@ -266,21 +292,22 @@ namespace Tgstation.Server.Host.Controllers /// The of model being generated. /// The of model being returned. /// A resulting in a resulting in the generated . - /// A to transform the s after being queried. + /// A to mutate the s after being queried. /// The requested page from the query. /// The requested page size from the query. /// The for the operation. /// A resulting in the for the operation. protected ValueTask Paginated( Func?>> queryGenerator, - Func? resultTransformer, + Func? resultMutator, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) where TModel : ILegacyApiTransformable => PaginatedImpl( queryGenerator, - resultTransformer, + model => model.ToApi(), + resultMutator, pageQuery, pageSizeQuery, cancellationToken); @@ -291,14 +318,16 @@ namespace Tgstation.Server.Host.Controllers /// The of model being generated. If different from , must implement for . /// The of model being returned. /// A resulting in a resulting in the generated or if an authorization requirment failed. - /// A to transform the s after being queried. + /// The conversion from to . + /// A to mutate the s after being queried. /// The requested page from the query. /// The requested page size from the query. /// The for the operation. /// A resulting in the for the operation. async ValueTask PaginatedImpl( Func?>> queryGenerator, - Func? resultTransformer, + Func resultTransformer, + Func? resultMutator, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) @@ -343,18 +372,13 @@ namespace Tgstation.Server.Host.Controllers pagedResults = [.. queriedResults]; } - ICollection finalResults; - if (typeof(TResultModel).IsAssignableFrom(typeof(TModel))) - finalResults = pagedResults.Cast().ToList(); // clearly a safe cast - else - finalResults = pagedResults - .Cast>() - .Select(x => x.ToApi()) - .ToList(); + var finalResults = pagedResults + .Select(resultTransformer) + .ToList(); - if (resultTransformer != null) + if (resultMutator != null) foreach (var finalResult in finalResults) - await resultTransformer(finalResult); + await resultMutator(finalResult); var carryTheOne = totalResults % pageSize != 0 ? 1 diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index 148f5bbc7b..0163af7bc3 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -16,6 +16,7 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Controllers.Transformers; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.GraphQL.Mutations.Payloads; @@ -194,7 +195,7 @@ namespace Tgstation.Server.Host.Controllers return ValueTask.FromResult(HeadersIssue(ApiHeadersProvider.HeadersException!)); } - return loginAuthority.InvokeTransformable(this, authority => authority.AttemptLogin(cancellationToken)); + return loginAuthority.InvokeTransformable(this, authority => authority.AttemptLogin(cancellationToken)); } /// diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index f060946784..5af21ca792 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -204,7 +204,6 @@ namespace Tgstation.Server.Host.Controllers })); } }, - null, page, pageSize, cancellationToken)); diff --git a/src/Tgstation.Server.Host/Controllers/EngineController.cs b/src/Tgstation.Server.Host/Controllers/EngineController.cs index 89a1ba8abe..eedd54bf2b 100644 --- a/src/Tgstation.Server.Host/Controllers/EngineController.cs +++ b/src/Tgstation.Server.Host/Controllers/EngineController.cs @@ -121,7 +121,6 @@ namespace Tgstation.Server.Host.Controllers }) .AsQueryable() .OrderBy(x => x.EngineVersion!.ToString()))), - null, page, pageSize, cancellationToken)); diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/PermissionSetTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/PermissionSetTransformer.cs new file mode 100644 index 0000000000..b6ab842c45 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Transformers/PermissionSetTransformer.cs @@ -0,0 +1,24 @@ +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; + +namespace Tgstation.Server.Host.Controllers.Transformers +{ + /// + /// for s. + /// + sealed class PermissionSetTransformer : Models.TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public PermissionSetTransformer() + : base( + model => new PermissionSet + { + AdministrationRights = model.AdministrationRights ?? NotNullFallback(), + InstanceManagerRights = model.InstanceManagerRights ?? NotNullFallback(), + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs new file mode 100644 index 0000000000..7b7851100f --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs @@ -0,0 +1,23 @@ +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Controllers.Transformers +{ + /// + /// for s. + /// + sealed class TokenResponseTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public TokenResponseTransformer() + : base( + token => new TokenResponse + { + Bearer = token, + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/UpdatedUserResponseTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/UpdatedUserResponseTransformer.cs new file mode 100644 index 0000000000..f569b8c9e7 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Transformers/UpdatedUserResponseTransformer.cs @@ -0,0 +1,25 @@ +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Controllers.Transformers +{ + /// + /// for s to s. + /// + sealed class UpdatedUserResponseTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public UpdatedUserResponseTransformer() + : base( + BuildSubProjection( + (model, fullUser) => fullUser ?? new UserResponse + { + Id = model.Id, + }, + model => model.User)) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/UserGroupTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/UserGroupTransformer.cs new file mode 100644 index 0000000000..285c7039de --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Transformers/UserGroupTransformer.cs @@ -0,0 +1,27 @@ +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Host.Controllers.Transformers +{ + /// + /// for s. + /// + sealed class UserGroupTransformer : Models.TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public UserGroupTransformer() + : base( + BuildSubProjection( + (model, permissionSet) => new UserGroup + { + Id = model.Id, + Name = model.Name, + PermissionSet = permissionSet, + }, + model => model.PermissionSet)) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/UserNameTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/UserNameTransformer.cs new file mode 100644 index 0000000000..7023079ad0 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Transformers/UserNameTransformer.cs @@ -0,0 +1,24 @@ +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Controllers.Transformers +{ + /// + /// for s. + /// + sealed class UserNameTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public UserNameTransformer() + : base( + model => new UserName + { + Id = model.Id, + Name = model.Name, + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/UserResponseTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/UserResponseTransformer.cs new file mode 100644 index 0000000000..866d2e561b --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Transformers/UserResponseTransformer.cs @@ -0,0 +1,64 @@ +using System.Linq; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Models.Response; + +namespace Tgstation.Server.Host.Controllers.Transformers +{ + /// + /// for s. + /// + sealed class UserResponseTransformer : Models.TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public UserResponseTransformer() + : base( + BuildSubProjection< + Models.UserGroup, + Models.PermissionSet, + Models.User, + UserGroup, + PermissionSet, + UserName, + UserGroupTransformer, + PermissionSetTransformer, + UserNameTransformer>( + (model, group, permissionSet, createdBy) => new UserResponse + { + Id = model.Id, + CreatedAt = model.CreatedAt, + OAuthConnections = model.OAuthConnections != null + ? model.OAuthConnections.Select( + oAuthConnection => new OAuthConnection + { + ExternalUserId = oAuthConnection.ExternalUserId, + Provider = oAuthConnection.Provider, + }) + .ToList() + : null, + CreatedBy = createdBy, + Enabled = model.Enabled, + Group = group, + Name = model.Name, + OidcConnections = model.OidcConnections != null + ? model.OidcConnections.Select( + oAuthConnection => new OidcConnection + { + ExternalUserId = oAuthConnection.ExternalUserId, + SchemeKey = oAuthConnection.SchemeKey, + }) + .ToList() + : null, + PermissionSet = permissionSet, + SystemIdentifier = model.SystemIdentifier, + }, + model => model.Group, + model => model.PermissionSet, + model => model.CreatedBy)) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index fc858c41bd..73537ba942 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -14,7 +14,9 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Controllers.Results; +using Tgstation.Server.Host.Controllers.Transformers; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; @@ -33,18 +35,25 @@ namespace Tgstation.Server.Host.Controllers /// readonly IRestAuthorityInvoker userAuthority; + /// + /// The for the request. + /// + readonly IClaimsPrincipalAccessor claimsPrincipalAccessor; + /// /// Initializes a new instance of the class. /// /// The for the . /// The for the . /// The value of . + /// The value of . /// The for the . /// The for the . public UserController( IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, IRestAuthorityInvoker userAuthority, + IClaimsPrincipalAccessor claimsPrincipalAccessor, ILogger logger, IApiHeadersProvider apiHeaders) : base( @@ -55,6 +64,7 @@ namespace Tgstation.Server.Host.Controllers true) { this.userAuthority = userAuthority ?? throw new ArgumentNullException(nameof(userAuthority)); + this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor)); } /// @@ -68,7 +78,7 @@ namespace Tgstation.Server.Host.Controllers [HttpPut] [ProducesResponseType(typeof(UserResponse), 201)] public ValueTask Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken) - => userAuthority.InvokeTransformable(this, authority => authority.Create(model, null, cancellationToken)); + => userAuthority.InvokeTransformable(this, authority => authority.Create(model, null, cancellationToken)); /// /// Update a . @@ -86,7 +96,7 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessageResponse), 404)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public ValueTask Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken) - => userAuthority.InvokeTransformable(this, authority => authority.Update(model, cancellationToken)); + => userAuthority.InvokeTransformable(this, authority => authority.Update(model, cancellationToken)); /// /// Get information about the current . @@ -98,7 +108,10 @@ namespace Tgstation.Server.Host.Controllers [Authorize] [ProducesResponseType(typeof(UserResponse), 200)] public ValueTask Read(CancellationToken cancellationToken) - => userAuthority.InvokeTransformable(this, authority => authority.Read(cancellationToken)); + => userAuthority.InvokeTransformable(this, authority => authority.GetId( + claimsPrincipalAccessor.User.RequireTgsUserId(), + false, + cancellationToken)); /// /// List all s in the server. @@ -111,7 +124,7 @@ namespace Tgstation.Server.Host.Controllers [HttpGet(Routes.List)] [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) - => Paginated( + => Paginated( async () => { var queryable = await userAuthority.InvokeQueryable( @@ -121,7 +134,6 @@ namespace Tgstation.Server.Host.Controllers return new PaginatableResult(queryable.OrderBy(x => x.Id)); }, - null, page, pageSize, cancellationToken); @@ -145,9 +157,9 @@ namespace Tgstation.Server.Host.Controllers if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) return Forbid(); - return await userAuthority.InvokeTransformable( + return await userAuthority.InvokeTransformable( this, - authority => authority.GetId(id, true, false, cancellationToken)); + authority => authority.GetId(id, false, cancellationToken)); } } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 8b73470640..65d8d4a2c8 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -861,7 +861,7 @@ namespace Tgstation.Server.Host.Core var authenticationContext = services .GetRequiredService(); context.HandleResponse(); - context.HttpContext.Response.Redirect($"{config.ReturnPath}?code={HttpUtility.UrlEncode(tokenFactory.CreateToken(authenticationContext.User, true))}&state=oidc.{HttpUtility.UrlEncode(configName)}"); + context.HttpContext.Response.Redirect($"{config.ReturnPath}?code={HttpUtility.UrlEncode(tokenFactory.CreateToken(authenticationContext.User, true).Token)}&state=oidc.{HttpUtility.UrlEncode(configName)}"); return Task.CompletedTask; }, }; diff --git a/src/Tgstation.Server.Host/GraphQL/Mutation.cs b/src/Tgstation.Server.Host/GraphQL/Mutation.cs index 2bb7231021..cee0635398 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs @@ -8,6 +8,7 @@ using HotChocolate.Types; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Mutations.Payloads; +using Tgstation.Server.Host.GraphQL.Transformers; namespace Tgstation.Server.Host.GraphQL { @@ -38,7 +39,7 @@ namespace Tgstation.Server.Host.GraphQL { ArgumentNullException.ThrowIfNull(loginAuthority); - return loginAuthority.Invoke( + return loginAuthority.InvokeTransformable( authority => authority.AttemptLogin(cancellationToken)); } diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs index a28589c21d..8a75d61018 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs @@ -1,14 +1,12 @@ using HotChocolate; -using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.GraphQL.Scalars; -using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads { /// /// Success response for a login attempt. /// - public sealed class LoginResult : ILegacyApiTransformable + public sealed class LoginResult { /// /// The JSON Web Token (JWT) to use as a Bearer token for accessing the server at non-login endpoints. Contains an expiry time. @@ -16,18 +14,5 @@ namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads [GraphQLType] [GraphQLNonNullType] public required string Bearer { get; init; } - - /// - /// The that was logged in. - /// - public required Types.User User { get; init; } - - /// - [GraphQLIgnore] - public TokenResponse ToApi() - => new() - { - Bearer = Bearer, - }; } } diff --git a/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs b/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs new file mode 100644 index 0000000000..14fd2b32d5 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs @@ -0,0 +1,23 @@ +using Tgstation.Server.Host.GraphQL.Mutations.Payloads; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.GraphQL.Transformers +{ + /// + /// for s. + /// + sealed class LoginResultTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public LoginResultTransformer() + : base( + token => new LoginResult + { + Bearer = token, + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs index 933e047b00..63ca95ffa6 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs @@ -32,17 +32,5 @@ namespace Tgstation.Server.Host.GraphQL.Types { Name = copy.Name; } - - /// - /// Initializes a new instance of the class. - /// - /// The ID for the . - /// The value of . - [SetsRequiredMembers] - protected NamedEntity(long id, string name) - : base(id) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index 8df9ef3c82..c372a4a813 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// A user registered in the server. /// [Node] - public sealed class User : NamedEntity, IUserName + public sealed class User : UserName { /// [IsProjected(true)] @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// Implements the . /// - /// The of s to load. + /// The of s to load paired with if the system user should be allowed. /// The for the . /// The for mapped to an . /// The for the operation. @@ -108,31 +108,43 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The for the operation. /// The for the operation. /// A resulting in the queried , if present. - public static ValueTask GetUser( + public static async ValueTask GetUser( long id, [Service] IUsersDataLoader usersDataLoader, QueryContext? queryContext, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(usersDataLoader); - return usersDataLoader.LoadAuthorityResponse(queryContext, id, cancellationToken); + var user = await usersDataLoader.LoadAuthorityResponse(queryContext, id, cancellationToken); + if (user?.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + throw new NotImplementedException(); + + return user; } /// /// The who created this . /// /// The for the . + /// The for the operation. /// The for the operation. /// The that created this , if any. public async ValueTask CreatedBy( [Service] IGraphQLAuthorityInvoker userAuthority, + QueryContext? queryContext, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(userAuthority); if (!CreatedById.HasValue) return null; - var user = await userAuthority.InvokeTransformable(authority => authority.GetId(CreatedById.Value, false, true, cancellationToken)); + // This one is particular and cannot be data-loaded due to necessitating a different parameter + var user = await userAuthority.InvokeTransformable( + authority => authority.GetId(CreatedById.Value, true, cancellationToken), + queryContext); + if (user == null) + throw new InvalidOperationException($"Query for created by of user ID {CreatedById.Value} returned null!"); + if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return new UserName(user); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs index c3d9117241..1a72927851 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs @@ -1,47 +1,21 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; -using HotChocolate; -using HotChocolate.Types.Relay; - -using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Interfaces; -using Tgstation.Server.Host.GraphQL.Transformers; namespace Tgstation.Server.Host.GraphQL.Types { /// /// A with limited fields. /// - [Node] - public sealed class UserName : NamedEntity, IUserName + public class UserName : NamedEntity, IUserName { - /// - /// Node resolver for s. - /// - /// The to lookup. - /// The for the . - /// The for the operation. - /// A resulting in the queried , if present. - public static ValueTask GetUserName( - long id, - [Service] IGraphQLAuthorityInvoker userAuthority, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformableAllowMissing( - authority => authority.GetId(id, false, true, cancellationToken)); - } - /// /// Initializes a new instance of the class. /// - /// The to copy. + /// The to copy. [SetsRequiredMembers] - public UserName(NamedEntity copy) - : base(copy) + public UserName(User user) + : base(user) { } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs b/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs index 7b0d4bbc4e..fcb1b6faba 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs @@ -14,7 +14,9 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.GraphQL.Transformers; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { @@ -38,15 +40,27 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// Gets the current . /// - /// The for the . + /// The for getting the current user ID. + /// The to use. + /// The active . /// The for the operation. /// A resulting in the current . - public ValueTask Current( - [Service] IGraphQLAuthorityInvoker userAuthority, + [Error(typeof(ErrorMessageException))] + public async ValueTask Current( + [Service] IClaimsPrincipalAccessor claimsPrincipalAccessor, + [Service] IUsersDataLoader usersDataLoader, + QueryContext? queryContext, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable(authority => authority.Read(cancellationToken)); + var user = await ById( + (claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor))).User.RequireTgsUserId(), + usersDataLoader, + queryContext, + cancellationToken); + if (user == null) + throw new InvalidOperationException("Reading the current user returned null!"); + + return user; } /// diff --git a/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel,TTransformer}.cs b/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel}.cs similarity index 54% rename from src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel,TTransformer}.cs rename to src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel}.cs index 0fff10e229..8b75aa418b 100644 --- a/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel,TTransformer}.cs +++ b/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel}.cs @@ -1,7 +1,5 @@ using System; -#pragma warning disable CA1005 - namespace Tgstation.Server.Host.Models { /// @@ -9,17 +7,17 @@ namespace Tgstation.Server.Host.Models /// /// The internal model . /// The API model . - /// The . - public interface IApiTransformable + public interface IApiTransformable where TApiModel : notnull - where TModel : IApiTransformable - where TTransformer : ITransformer, new() + where TModel : IApiTransformable { /// - /// Convert the to it's . + /// Convert the to it's . /// - /// A new based on the . - TApiModel ToApi() + /// The . + /// A new based on the . + TApiModel ToApi() + where TTransformer : ITransformer, new() => new TTransformer() .CompiledExpression((TModel)this); } diff --git a/src/Tgstation.Server.Host/Models/OAuthConnection.cs b/src/Tgstation.Server.Host/Models/OAuthConnection.cs index 1b652f79be..e04a20e442 100644 --- a/src/Tgstation.Server.Host/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Host/Models/OAuthConnection.cs @@ -1,13 +1,11 @@ using System.ComponentModel.DataAnnotations; -using Tgstation.Server.Host.GraphQL.Transformers; - namespace Tgstation.Server.Host.Models { /// public sealed class OAuthConnection : Api.Models.OAuthConnection, ILegacyApiTransformable, - IApiTransformable + IApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/OidcConnection.cs b/src/Tgstation.Server.Host/Models/OidcConnection.cs index c80daceb87..7eeff69a26 100644 --- a/src/Tgstation.Server.Host/Models/OidcConnection.cs +++ b/src/Tgstation.Server.Host/Models/OidcConnection.cs @@ -1,13 +1,11 @@ using System.ComponentModel.DataAnnotations; -using Tgstation.Server.Host.GraphQL.Transformers; - namespace Tgstation.Server.Host.Models { /// public sealed class OidcConnection : Api.Models.OidcConnection, ILegacyApiTransformable, - IApiTransformable + IApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/PermissionSet.cs b/src/Tgstation.Server.Host/Models/PermissionSet.cs index 3a5bbdaede..3530c2f407 100644 --- a/src/Tgstation.Server.Host/Models/PermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/PermissionSet.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; -using Tgstation.Server.Host.GraphQL.Transformers; - namespace Tgstation.Server.Host.Models { /// - public sealed class PermissionSet : Api.Models.PermissionSet, IApiTransformable + public sealed class PermissionSet : Api.Models.PermissionSet, IApiTransformable { /// /// The of . diff --git a/src/Tgstation.Server.Host/Models/TransformerBase{TInput,TOutput}.cs b/src/Tgstation.Server.Host/Models/TransformerBase{TInput,TOutput}.cs index 893f352067..7eb4f1506b 100644 --- a/src/Tgstation.Server.Host/Models/TransformerBase{TInput,TOutput}.cs +++ b/src/Tgstation.Server.Host/Models/TransformerBase{TInput,TOutput}.cs @@ -137,6 +137,47 @@ namespace Tgstation.Server.Host.Models return global::System.Linq.Expressions.Expression.Lambda>(outputExpression, primaryInput); } + /// + /// Build an for to when contains three sub-inputs with their own s. + /// + /// The first field in that needs transforming. + /// The second field in that needs transforming. + /// The third field in that needs transforming. + /// The first transformed of . + /// The second transformed of . + /// The third transformed of . + /// The for /. + /// The for /. + /// The for /. + /// The to take a , , and and produce a . + /// The to select from . + /// The to select from . + /// The to select from . + /// An expression converting into based on with its other arguments generated from the transformation result of , , . + protected static Expression> BuildSubProjection< + TSubInput1, + TSubInput2, + TSubInput3, + TSubOutput1, + TSubOutput2, + TSubOutput3, + TTransformer1, + TTransformer2, + TTransformer3>( + Expression> transformerExpression, + Expression> subInput1SelectionExpression, + Expression> subInput2SelectionExpression, + Expression> subInput3SelectionExpression) + where TSubOutput1 : class + where TSubOutput2 : class + where TSubOutput3 : class + where TTransformer1 : ITransformer, new() + where TTransformer2 : ITransformer, new() + where TTransformer3 : ITransformer, new() + { + throw new NotImplementedException(); + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Models/UpdatedUser.cs b/src/Tgstation.Server.Host/Models/UpdatedUser.cs index 837a5146df..99d7384982 100644 --- a/src/Tgstation.Server.Host/Models/UpdatedUser.cs +++ b/src/Tgstation.Server.Host/Models/UpdatedUser.cs @@ -1,7 +1,6 @@ using System; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Host.GraphQL.Transformers; namespace Tgstation.Server.Host.Models { @@ -9,8 +8,8 @@ namespace Tgstation.Server.Host.Models /// Represents a that has been updated. /// public sealed class UpdatedUser : - ILegacyApiTransformable, - IApiTransformable + IApiTransformable, + IApiTransformable { /// /// The 's . @@ -40,12 +39,5 @@ namespace Tgstation.Server.Host.Models { Id = id; } - - /// - public UserResponse ToApi() - => User?.ToApi() ?? new UserResponse - { - Id = Id, - }; } } diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index dba5616b6d..fcf92eebdb 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -1,19 +1,17 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Host.GraphQL.Transformers; namespace Tgstation.Server.Host.Models { /// public sealed class User : Api.Models.Internal.UserModelBase, - ILegacyApiTransformable, - IApiTransformable, - IApiTransformable + IApiTransformable, + IApiTransformable, + IApiTransformable { /// /// Username used when creating jobs automatically. @@ -88,33 +86,5 @@ namespace Tgstation.Server.Host.Models /// The . /// The . public static string CanonicalizeName(string name) => name?.ToUpperInvariant() ?? throw new ArgumentNullException(nameof(name)); - - /// - public UserResponse ToApi() => CreateUserResponse(true); - - /// - /// Generate a from . - /// - /// If we should recurse on . - /// A new . - UserResponse CreateUserResponse(bool recursive) - { - var result = CreateUserName(); - if (recursive) - result.CreatedBy = CreatedBy?.CreateUserName(); - - result.CreatedAt = CreatedAt; - result.Enabled = Enabled; - result.SystemIdentifier = SystemIdentifier; - result.OAuthConnections = OAuthConnections - ?.Select(x => x.ToApi()) - .ToList(); - result.OidcConnections = OidcConnections - ?.Select(x => x.ToApi()) - .ToList(); - result.Group = Group?.ToApi(false); - result.PermissionSet = PermissionSet?.ToApi(); - return result; - } } } diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index 23907d9968..7b0bbd6882 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -5,14 +5,13 @@ using System.Linq; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Host.GraphQL.Transformers; namespace Tgstation.Server.Host.Models { /// /// Represents a group of s. /// - public sealed class UserGroup : NamedEntity, ILegacyApiTransformable, IApiTransformable + public sealed class UserGroup : NamedEntity, ILegacyApiTransformable, IApiTransformable { /// /// The the has. diff --git a/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs b/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs index 9ac3da2494..1ff2177c88 100644 --- a/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs +++ b/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Security /// /// Interface for accessing the current request's . /// - interface IClaimsPrincipalAccessor + public interface IClaimsPrincipalAccessor { /// /// Get the current . diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs index bfa7bb360f..1baa29e6cc 100644 --- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs @@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Security /// /// The to create the token for. Must have the field available. /// Whether or not this is an external service login. - /// A new token . - string CreateToken(Models.User user, bool serviceLogin); + /// A new token and the that it expires. + (string Token, DateTimeOffset Expiry) CreateToken(Models.User user, bool serviceLogin); } } diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 9e2b18e668..d41803ef4d 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security } /// - public string CreateToken(User user, bool serviceLogin) + public (string Token, DateTimeOffset Expiry) CreateToken(User user, bool serviceLogin) { ArgumentNullException.ThrowIfNull(user); @@ -141,7 +141,7 @@ namespace Tgstation.Server.Host.Security var tokenResponse = tokenHandler.WriteToken(securityToken); - return tokenResponse; + return (Token: tokenResponse, Expiry: expiry); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs index 5f5c9aec76..60e465c342 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs @@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Swarm.Tests public TokenValidationParameters ValidationParameters => throw new NotSupportedException(); - public string CreateToken(User user, bool serviceLogin) + public (string, DateTimeOffset) CreateToken(User user, bool serviceLogin) { throw new NotSupportedException(); }