mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-26 14:37:44 +01:00
Way too much stuff again, mostly relating to transformers
- Improve transformer implementations of authority invokers - LoginAuthority now just returns the token string. - Remove non-projectable GetId implementation from UserAuthority. - Improve Rest Paginated implementations and add transformer support. - Made GQL `User` derive from `UserName`. - Remove `ILegacyApiTransformable` from user models. TODO: - Implement `NotImplementedException`s. - Consider removing `IApiTransformable`. - Consider wrapping LoginResult in its own authority-based DTO. Maybe a record struct also containing the expiry.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
QueryContext<TApiModel>? 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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<EntityId>()
|
||||
.OrderBy(item => item.Id!.Value) // order by ID to fix an EFCore warning
|
||||
.Cast<TResult>();
|
||||
|
||||
var expression = new TTransformer().Expression;
|
||||
return queryable
|
||||
@@ -148,6 +172,13 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
=> await ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(authorityInvoker)
|
||||
?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!");
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
QueryContext<TApiModel>? queryContext)
|
||||
=> await ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeTransformable<TResult, TApiModel, TTransformer>(authorityInvoker, queryContext)
|
||||
?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnRequirementsFailure(AuthorizationFailure authFailure)
|
||||
=> throw authFailure.ForbiddenGraphQLException();
|
||||
|
||||
@@ -129,5 +129,31 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
|
||||
return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(
|
||||
ApiController controller,
|
||||
Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> 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!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(
|
||||
ApiController controller,
|
||||
Func<TAuthority, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker)
|
||||
{
|
||||
await Task.Yield();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/> if any.</returns>
|
||||
ValueTask<TApiModel?> InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
@@ -45,9 +45,25 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/> if any.</returns>
|
||||
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TResult : notnull
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the non-nullable result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/>.</param>
|
||||
/// <param name="queryContext">The active <see cref="QueryContext{TEntity}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/> if any.</returns>
|
||||
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
QueryContext<TApiModel>? queryContext)
|
||||
where TResult : EntityId
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
@@ -71,7 +87,23 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TResult : notnull
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the non-nullable result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/>.</param>
|
||||
/// <param name="queryContext">The active <see cref="QueryContext{TEntity}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
QueryContext<TApiModel>? queryContext)
|
||||
where TResult : EntityId
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
@@ -87,7 +119,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
ValueTask<IQueryable<TApiModel>> InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker,
|
||||
Func<IQueryable<TResult>, IQueryable<TResult>>? preTransformer = null)
|
||||
where TResult : IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
@@ -105,7 +136,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
Func<TAuthority, long, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
IReadOnlyList<long> ids,
|
||||
QueryContext<AuthorityResponse<TApiModel>>? queryContext)
|
||||
where TResult : EntityId, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TResult : EntityId
|
||||
where TApiModel : Entity
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Attempt to login to the server with the current Basic or OAuth credentials.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="LoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an authenticated token <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<string>> AttemptLogin(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to login to an OAuth service with the current OAuth credentials.
|
||||
|
||||
@@ -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<IActionResult> InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, ILegacyApiTransformable<TApiModel>
|
||||
where TApiModel : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned REST <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="controller">The <see cref="ApiController"/> invoking the <typeparamref name="TAuthority"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel, TTransformer>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned REST <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="controller">The <see cref="ApiController"/> invoking the <typeparamref name="TAuthority"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel, TTransformer>(ApiController controller, Func<TAuthority, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker)
|
||||
where TResult : EntityId
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,33 +13,16 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
public interface IUserAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the currently authenticated user.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The result type after projection.</typeparam>
|
||||
/// <param name="id">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned or will result in a <see cref="HttpFailureResponse.Forbidden"/> response.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/> <see cref="User"/> for <typeparamref name="TResult"/>.</returns>
|
||||
RequirementsGated<Projectable<User, TResult>> GetId<TResult>(long id, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
where TResult : class;
|
||||
where TResult : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Models.OAuthConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<string>> AttemptLogin(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => null,
|
||||
() => AttemptLoginImpl(cancellationToken),
|
||||
@@ -177,19 +176,19 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Login process.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/> for the <see cref="LoginResult"/>.</returns>
|
||||
private async ValueTask<AuthorityResponse<LoginResult>> AttemptLoginImpl(CancellationToken cancellationToken)
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/> containing the authenticated bearer token.</returns>
|
||||
private async ValueTask<AuthorityResponse<string>> AttemptLoginImpl(CancellationToken cancellationToken)
|
||||
{
|
||||
// password and oauth logins disabled
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return Unauthorized<LoginResult>();
|
||||
return Unauthorized<string>();
|
||||
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse<LoginResult>(apiHeadersProvider.HeadersException!);
|
||||
return GenerateHeadersExceptionResponse<string>(apiHeadersProvider.HeadersException!);
|
||||
|
||||
if (headers.IsTokenAuthentication)
|
||||
return BadRequest<LoginResult>(ErrorCode.TokenWithToken);
|
||||
return BadRequest<string>(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<LoginResult>(headers, oAuthProvider, true, cancellationToken);
|
||||
var (errorResponse, oauthResult) = await TryOAuthenticate<string>(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<LoginResult>();
|
||||
return Unauthorized<string>();
|
||||
|
||||
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<LoginResult>();
|
||||
return Unauthorized<string>();
|
||||
|
||||
// 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<LoginResult>();
|
||||
return Unauthorized<string>();
|
||||
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<LoginResult>();
|
||||
return Forbid<string>();
|
||||
}
|
||||
|
||||
var token = tokenFactory.CreateToken(user, oAuthLogin);
|
||||
var payload = new LoginResult
|
||||
{
|
||||
Bearer = token,
|
||||
User = ((IApiTransformable<User, GraphQL.Types.User, UserTransformer>)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<LoginResult>(payload);
|
||||
return new AuthorityResponse<string>(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,12 +313,12 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> to cache.</param>
|
||||
/// <param name="user">The <see cref="User"/> the <paramref name="systemIdentity"/> was generated for.</param>
|
||||
/// <param name="loginPayload">The <see cref="LoginResult"/> for the successful login.</param>
|
||||
/// <param name="validTo">When the user's session exipres.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<User>> Read(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => Enumerable.Empty<IAuthorizationRequirement>(),
|
||||
() => GetIdImpl(claimsPrincipalAccessor.User.RequireTgsUserId(), true, false, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
if (id != claimsPrincipalAccessor.User.GetTgsUserId())
|
||||
return Enumerable.Empty<IAuthorizationRequirement>();
|
||||
|
||||
return new List<IAuthorizationRequirement>
|
||||
{
|
||||
Flag(AdministrationRights.ReadUsers),
|
||||
};
|
||||
},
|
||||
() => GetIdImpl(id, includeJoins, allowSystemUser, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<IQueryable<User>> Queryable(bool includeJoins)
|
||||
=> new(
|
||||
@@ -565,7 +543,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<Projectable<User, TResult>> GetId<TResult>(long id, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
where TResult : class
|
||||
where TResult : notnull
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
@@ -600,37 +578,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
},
|
||||
cancellationToken)));
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of retrieving a <see cref="User"/> by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="EntityId.Id"/> of the user to retrieve.</param>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
async ValueTask<AuthorityResponse<User>> 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<User>();
|
||||
|
||||
if (!allowSystemUser && user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<User>();
|
||||
|
||||
return new AuthorityResponse<User>(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the <see cref="AuthorityResponse{TResult}"/> for an <see cref="UpdatedUser"/>.
|
||||
/// </summary>
|
||||
@@ -667,7 +614,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
user.Require(x => x.Id))
|
||||
.Select(topic => topicEventSender.SendAsync(
|
||||
topic,
|
||||
((IApiTransformable<User, GraphQL.Types.User, UserTransformer>)user).ToApi(),
|
||||
((IApiTransformable<User, GraphQL.Types.User>)user).ToApi<GraphQL.Transformers.UserTransformer>(),
|
||||
CancellationToken.None))); // DCT: Operation should always run
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -208,7 +208,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}));
|
||||
}
|
||||
},
|
||||
null,
|
||||
page,
|
||||
pageSize,
|
||||
cancellationToken);
|
||||
|
||||
@@ -243,19 +243,45 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated and returned.</typeparam>
|
||||
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="Task{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
|
||||
/// <param name="resultTransformer">Optional <see cref="Func{T, TResult}"/> to transform the <typeparamref name="TModel"/>s after being queried.</param>
|
||||
/// <param name="pageQuery">The requested page from the query.</param>
|
||||
/// <param name="pageSizeQuery">The requested page size from the query.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
protected ValueTask<IActionResult> Paginated<TModel>(
|
||||
Func<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
Func<TModel, ValueTask>? resultTransformer,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
CancellationToken cancellationToken) => PaginatedImpl(
|
||||
queryGenerator,
|
||||
resultTransformer,
|
||||
model => model,
|
||||
null,
|
||||
pageQuery,
|
||||
pageSizeQuery,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a paginated response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated.</typeparam>
|
||||
/// <typeparam name="TApiModel">The <see cref="Type"/> of model being returned.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="Type"/> of the <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TModel"/>/<typeparamref name="TApiModel"/>.</typeparam>
|
||||
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="Task{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
|
||||
/// <param name="pageQuery">The requested page from the query.</param>
|
||||
/// <param name="pageSizeQuery">The requested page size from the query.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
protected ValueTask<IActionResult> Paginated<TModel, TApiModel, TTransformer>(
|
||||
Func<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
CancellationToken cancellationToken)
|
||||
where TModel : IApiTransformable<TModel, TApiModel>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TModel, TApiModel>, new()
|
||||
=> PaginatedImpl(
|
||||
queryGenerator,
|
||||
model => model.ToApi<TTransformer>(),
|
||||
null,
|
||||
pageQuery,
|
||||
pageSizeQuery,
|
||||
cancellationToken);
|
||||
@@ -266,21 +292,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated.</typeparam>
|
||||
/// <typeparam name="TApiModel">The <see cref="Type"/> of model being returned.</typeparam>
|
||||
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="ValueTask{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
|
||||
/// <param name="resultTransformer">A <see cref="Func{T, TResult}"/> to transform the <typeparamref name="TApiModel"/>s after being queried.</param>
|
||||
/// <param name="resultMutator">A <see cref="Func{T, TResult}"/> to mutate the <typeparamref name="TApiModel"/>s after being queried.</param>
|
||||
/// <param name="pageQuery">The requested page from the query.</param>
|
||||
/// <param name="pageSizeQuery">The requested page size from the query.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
protected ValueTask<IActionResult> Paginated<TModel, TApiModel>(
|
||||
Func<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
Func<TApiModel, ValueTask>? resultTransformer,
|
||||
Func<TApiModel, ValueTask>? resultMutator,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
CancellationToken cancellationToken)
|
||||
where TModel : ILegacyApiTransformable<TApiModel>
|
||||
=> PaginatedImpl(
|
||||
queryGenerator,
|
||||
resultTransformer,
|
||||
model => model.ToApi(),
|
||||
resultMutator,
|
||||
pageQuery,
|
||||
pageSizeQuery,
|
||||
cancellationToken);
|
||||
@@ -291,14 +318,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated. If different from <typeparamref name="TResultModel"/>, must implement <see cref="ILegacyApiTransformable{TApiModel}"/> for <typeparamref name="TResultModel"/>.</typeparam>
|
||||
/// <typeparam name="TResultModel">The <see cref="Type"/> of model being returned.</typeparam>
|
||||
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="ValueTask{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/> or <see langword="null"/> if an authorization requirment failed.</param>
|
||||
/// <param name="resultTransformer">A <see cref="Func{T, TResult}"/> to transform the <typeparamref name="TResultModel"/>s after being queried.</param>
|
||||
/// <param name="resultTransformer">The conversion <see cref="Func{T, TResult}"/> from <typeparamref name="TModel"/> to <typeparamref name="TResultModel"/>.</param>
|
||||
/// <param name="resultMutator">A <see cref="Func{T, TResult}"/> to mutate the <typeparamref name="TResultModel"/>s after being queried.</param>
|
||||
/// <param name="pageQuery">The requested page from the query.</param>
|
||||
/// <param name="pageSizeQuery">The requested page size from the query.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
async ValueTask<IActionResult> PaginatedImpl<TModel, TResultModel>(
|
||||
Func<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
Func<TResultModel, ValueTask>? resultTransformer,
|
||||
Func<TModel, TResultModel> resultTransformer,
|
||||
Func<TResultModel, ValueTask>? resultMutator,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -343,18 +372,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
pagedResults = [.. queriedResults];
|
||||
}
|
||||
|
||||
ICollection<TResultModel> finalResults;
|
||||
if (typeof(TResultModel).IsAssignableFrom(typeof(TModel)))
|
||||
finalResults = pagedResults.Cast<TResultModel>().ToList(); // clearly a safe cast
|
||||
else
|
||||
finalResults = pagedResults
|
||||
.Cast<ILegacyApiTransformable<TResultModel>>()
|
||||
.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
|
||||
|
||||
@@ -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<LoginResult, TokenResponse>(this, authority => authority.AttemptLogin(cancellationToken));
|
||||
return loginAuthority.InvokeTransformable<string, TokenResponse, TokenResponseTransformer>(this, authority => authority.AttemptLogin(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -204,7 +204,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}));
|
||||
}
|
||||
},
|
||||
null,
|
||||
page,
|
||||
pageSize,
|
||||
cancellationToken));
|
||||
|
||||
@@ -121,7 +121,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
})
|
||||
.AsQueryable()
|
||||
.OrderBy(x => x.EngineVersion!.ToString()))),
|
||||
null,
|
||||
page,
|
||||
pageSize,
|
||||
cancellationToken));
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Models.ITransformer{TInput, TOutput}"/> for <see cref="PermissionSet"/>s.
|
||||
/// </summary>
|
||||
sealed class PermissionSetTransformer : Models.TransformerBase<Models.PermissionSet, PermissionSet>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PermissionSetTransformer"/> class.
|
||||
/// </summary>
|
||||
public PermissionSetTransformer()
|
||||
: base(
|
||||
model => new PermissionSet
|
||||
{
|
||||
AdministrationRights = model.AdministrationRights ?? NotNullFallback<AdministrationRights>(),
|
||||
InstanceManagerRights = model.InstanceManagerRights ?? NotNullFallback<InstanceManagerRights>(),
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="TokenResponse"/>s.
|
||||
/// </summary>
|
||||
sealed class TokenResponseTransformer : TransformerBase<string, TokenResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TokenResponseTransformer"/> class.
|
||||
/// </summary>
|
||||
public TokenResponseTransformer()
|
||||
: base(
|
||||
token => new TokenResponse
|
||||
{
|
||||
Bearer = token,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="UpdatedUser"/>s to <see cref="UserResponse"/>s.
|
||||
/// </summary>
|
||||
sealed class UpdatedUserResponseTransformer : TransformerBase<UpdatedUser, UserResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdatedUserResponseTransformer"/> class.
|
||||
/// </summary>
|
||||
public UpdatedUserResponseTransformer()
|
||||
: base(
|
||||
BuildSubProjection<User, UserResponse, UserResponseTransformer>(
|
||||
(model, fullUser) => fullUser ?? new UserResponse
|
||||
{
|
||||
Id = model.Id,
|
||||
},
|
||||
model => model.User))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Models.ITransformer{TInput, TOutput}"/> for <see cref="UserGroup"/>s.
|
||||
/// </summary>
|
||||
sealed class UserGroupTransformer : Models.TransformerBase<Models.UserGroup, UserGroup>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGroupTransformer"/> class.
|
||||
/// </summary>
|
||||
public UserGroupTransformer()
|
||||
: base(
|
||||
BuildSubProjection<Models.PermissionSet, PermissionSet, PermissionSetTransformer>(
|
||||
(model, permissionSet) => new UserGroup
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
PermissionSet = permissionSet,
|
||||
},
|
||||
model => model.PermissionSet))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="UserName"/>s.
|
||||
/// </summary>
|
||||
sealed class UserNameTransformer : TransformerBase<User, UserName>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserNameTransformer"/> class.
|
||||
/// </summary>
|
||||
public UserNameTransformer()
|
||||
: base(
|
||||
model => new UserName
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Models.ITransformer{TInput, TOutput}"/> for <see cref="UserResponse"/>s.
|
||||
/// </summary>
|
||||
sealed class UserResponseTransformer : Models.TransformerBase<Models.User, UserResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserResponseTransformer"/> class.
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IRestAuthorityInvoker<IUserAuthority> userAuthority;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the request.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="userAuthority">The value of <see cref="userAuthority"/>.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
public UserController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContext authenticationContext,
|
||||
IRestAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
ILogger<UserController> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,7 +78,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpPut]
|
||||
[ProducesResponseType(typeof(UserResponse), 201)]
|
||||
public ValueTask<IActionResult> Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<UpdatedUser, UserResponse>(this, authority => authority.Create(model, null, cancellationToken));
|
||||
=> userAuthority.InvokeTransformable<UpdatedUser, UserResponse, UpdatedUserResponseTransformer>(this, authority => authority.Create(model, null, cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Update a <see cref="User"/>.
|
||||
@@ -86,7 +96,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 404)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
public ValueTask<IActionResult> Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<UpdatedUser, UserResponse>(this, authority => authority.Update(model, cancellationToken));
|
||||
=> userAuthority.InvokeTransformable<UpdatedUser, UserResponse, UpdatedUserResponseTransformer>(this, authority => authority.Update(model, cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Get information about the current <see cref="User"/>.
|
||||
@@ -98,7 +108,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(UserResponse), 200)]
|
||||
public ValueTask<IActionResult> Read(CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<User, UserResponse>(this, authority => authority.Read(cancellationToken));
|
||||
=> userAuthority.InvokeTransformable<User, UserResponse, UserResponseTransformer>(this, authority => authority.GetId<UserResponse>(
|
||||
claimsPrincipalAccessor.User.RequireTgsUserId(),
|
||||
false,
|
||||
cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// List all <see cref="User"/>s in the server.
|
||||
@@ -111,7 +124,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpGet(Routes.List)]
|
||||
[ProducesResponseType(typeof(PaginatedResponse<UserResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<User, UserResponse>(
|
||||
=> Paginated<User, UserResponse, UserResponseTransformer>(
|
||||
async () =>
|
||||
{
|
||||
var queryable = await userAuthority.InvokeQueryable(
|
||||
@@ -121,7 +134,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
return new PaginatableResult<User>(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<User, UserResponse>(
|
||||
return await userAuthority.InvokeTransformable<User, UserResponse, UserResponseTransformer>(
|
||||
this,
|
||||
authority => authority.GetId(id, true, false, cancellationToken));
|
||||
authority => authority.GetId<UserResponse>(id, false, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,7 +861,7 @@ namespace Tgstation.Server.Host.Core
|
||||
var authenticationContext = services
|
||||
.GetRequiredService<IAuthenticationContext>();
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<LoginResult, LoginResult>(
|
||||
return loginAuthority.InvokeTransformable<string, LoginResult, LoginResultTransformer>(
|
||||
authority => authority.AttemptLogin(cancellationToken));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Success response for a login attempt.
|
||||
/// </summary>
|
||||
public sealed class LoginResult : ILegacyApiTransformable<TokenResponse>
|
||||
public sealed class LoginResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 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<JwtType>]
|
||||
[GraphQLNonNullType]
|
||||
public required string Bearer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> that was logged in.
|
||||
/// </summary>
|
||||
public required Types.User User { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[GraphQLIgnore]
|
||||
public TokenResponse ToApi()
|
||||
=> new()
|
||||
{
|
||||
Bearer = Bearer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Models.ITransformer{TInput, TOutput}"/> for <see cref="LoginResult"/>s.
|
||||
/// </summary>
|
||||
sealed class LoginResultTransformer : TransformerBase<string, LoginResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginResultTransformer"/> class.
|
||||
/// </summary>
|
||||
public LoginResultTransformer()
|
||||
: base(
|
||||
token => new LoginResult
|
||||
{
|
||||
Bearer = token,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,17 +32,5 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
Name = copy.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedEntity"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID for the <see cref="Entity"/>.</param>
|
||||
/// <param name="name">The value of <see cref="Name"/>.</param>
|
||||
[SetsRequiredMembers]
|
||||
protected NamedEntity(long id, string name)
|
||||
: base(id)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// A user registered in the server.
|
||||
/// </summary>
|
||||
[Node]
|
||||
public sealed class User : NamedEntity, IUserName
|
||||
public sealed class User : UserName
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[IsProjected(true)]
|
||||
@@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <summary>
|
||||
/// Implements the <see cref="IUserGroupsDataLoader"/>.
|
||||
/// </summary>
|
||||
/// <param name="ids">The <see cref="IReadOnlyList{T}"/> of <see cref="User"/> <see cref="Api.Models.EntityId.Id"/>s to load.</param>
|
||||
/// <param name="ids">The <see cref="IReadOnlyList{T}"/> of <see cref="User"/> <see cref="Api.Models.EntityId.Id"/>s to load paired with if the system user should be allowed.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="queryContext">The <see cref="QueryContext{TEntity}"/> for <see cref="User"/> mapped to an <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
@@ -108,31 +108,43 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <param name="queryContext">The <see cref="QueryContext{TEntity}"/> for the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> resulting in the queried <see cref="User"/>, if present.</returns>
|
||||
public static ValueTask<User?> GetUser(
|
||||
public static async ValueTask<User?> GetUser(
|
||||
long id,
|
||||
[Service] IUsersDataLoader usersDataLoader,
|
||||
QueryContext<User>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> who created this <see cref="User"/>.
|
||||
/// </summary>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="queryContext">The <see cref="QueryContext{TEntity}"/> for the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The <see cref="IUserName"/> that created this <see cref="User"/>, if any.</returns>
|
||||
public async ValueTask<IUserName?> CreatedBy(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
QueryContext<User>? queryContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
if (!CreatedById.HasValue)
|
||||
return null;
|
||||
|
||||
var user = await userAuthority.InvokeTransformable<Models.User, User, UserTransformer>(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<Models.User, User, UserTransformer>(
|
||||
authority => authority.GetId<User>(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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="User"/> with limited fields.
|
||||
/// </summary>
|
||||
[Node]
|
||||
public sealed class UserName : NamedEntity, IUserName
|
||||
public class UserName : NamedEntity, IUserName
|
||||
{
|
||||
/// <summary>
|
||||
/// Node resolver for <see cref="UserName"/>s.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Entity.Id"/> to lookup.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> resulting in the queried <see cref="UserName"/>, if present.</returns>
|
||||
public static ValueTask<UserName?> GetUserName(
|
||||
long id,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformableAllowMissing<Models.User, UserName, UserNameTransformer>(
|
||||
authority => authority.GetId(id, false, true, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserName"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="NamedEntity"/> to copy.</param>
|
||||
/// <param name="user">The <see cref="User"/> to copy.</param>
|
||||
[SetsRequiredMembers]
|
||||
public UserName(NamedEntity copy)
|
||||
: base(copy)
|
||||
public UserName(User user)
|
||||
: base(user)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="User"/>.
|
||||
/// </summary>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The <see cref="IClaimsPrincipalAccessor"/> for getting the current user ID.</param>
|
||||
/// <param name="usersDataLoader">The <see cref="IUsersDataLoader"/> to use.</param>
|
||||
/// <param name="queryContext">The active <see cref="QueryContext{TEntity}"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the current <see cref="User"/>.</returns>
|
||||
public ValueTask<User> Current(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public async ValueTask<User> Current(
|
||||
[Service] IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
[Service] IUsersDataLoader usersDataLoader,
|
||||
QueryContext<User>? queryContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserTransformer>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+7
-9
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
|
||||
#pragma warning disable CA1005
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <summary>
|
||||
@@ -9,17 +7,17 @@ namespace Tgstation.Server.Host.Models
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The internal model <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The API model <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TModel, TApiModel}"/> <see cref="Type"/>.</typeparam>
|
||||
public interface IApiTransformable<TModel, TApiModel, TTransformer>
|
||||
public interface IApiTransformable<TModel, TApiModel>
|
||||
where TApiModel : notnull
|
||||
where TModel : IApiTransformable<TModel, TApiModel, TTransformer>
|
||||
where TTransformer : ITransformer<TModel, TApiModel>, new()
|
||||
where TModel : IApiTransformable<TModel, TApiModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert the <see cref="IApiTransformable{TModel, TApiModel, TApiTransformer}"/> to it's <typeparamref name="TApiModel"/>.
|
||||
/// Convert the <see cref="IApiTransformable{TModel, TApiModel}"/> to it's <typeparamref name="TApiModel"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <typeparamref name="TApiModel"/> based on the <see cref="IApiTransformable{TModel, TApiModel, TApiTransformer}"/>.</returns>
|
||||
TApiModel ToApi()
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TModel, TApiModel}"/> <see cref="Type"/>.</typeparam>
|
||||
/// <returns>A new <typeparamref name="TApiModel"/> based on the <see cref="IApiTransformable{TModel, TApiModel}"/>.</returns>
|
||||
TApiModel ToApi<TTransformer>()
|
||||
where TTransformer : ITransformer<TModel, TApiModel>, new()
|
||||
=> new TTransformer()
|
||||
.CompiledExpression((TModel)this);
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Tgstation.Server.Host.GraphQL.Transformers;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.OAuthConnection" />
|
||||
public sealed class OAuthConnection : Api.Models.OAuthConnection,
|
||||
ILegacyApiTransformable<Api.Models.OAuthConnection>,
|
||||
IApiTransformable<OAuthConnection, GraphQL.Types.OAuth.OAuthConnection, OAuthConnectionTransformer>
|
||||
IApiTransformable<OAuthConnection, GraphQL.Types.OAuth.OAuthConnection>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Tgstation.Server.Host.GraphQL.Transformers;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.OidcConnection" />
|
||||
public sealed class OidcConnection : Api.Models.OidcConnection,
|
||||
ILegacyApiTransformable<Api.Models.OidcConnection>,
|
||||
IApiTransformable<OidcConnection, GraphQL.Types.OAuth.OidcConnection, OidcConnectionTransformer>
|
||||
IApiTransformable<OidcConnection, GraphQL.Types.OAuth.OidcConnection>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Tgstation.Server.Host.GraphQL.Transformers;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class PermissionSet : Api.Models.PermissionSet, IApiTransformable<PermissionSet, GraphQL.Types.PermissionSet, PermissionSetTransformer>
|
||||
public sealed class PermissionSet : Api.Models.PermissionSet, IApiTransformable<PermissionSet, GraphQL.Types.PermissionSet>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.EntityId.Id"/> of <see cref="User"/>.
|
||||
|
||||
@@ -137,6 +137,47 @@ namespace Tgstation.Server.Host.Models
|
||||
return global::System.Linq.Expressions.Expression.Lambda<Func<TInput, TOutput>>(outputExpression, primaryInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build an <see cref="Expression{TDelegate}"/> for <typeparamref name="TInput"/> to <typeparamref name="TOutput"/> when <typeparamref name="TInput"/> contains three sub-inputs with their own <see cref="ITransformer{TInput, TOutput}"/>s.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSubInput1">The first field <see cref="Type"/> in <typeparamref name="TInput"/> that needs transforming.</typeparam>
|
||||
/// <typeparam name="TSubInput2">The second field <see cref="Type"/> in <typeparamref name="TInput"/> that needs transforming.</typeparam>
|
||||
/// <typeparam name="TSubInput3">The third field <see cref="Type"/> in <typeparamref name="TInput"/> that needs transforming.</typeparam>
|
||||
/// <typeparam name="TSubOutput1">The first transformed <see cref="Type"/> of <typeparamref name="TSubInput1"/>.</typeparam>
|
||||
/// <typeparam name="TSubOutput2">The second transformed <see cref="Type"/> of <typeparamref name="TSubInput2"/>.</typeparam>
|
||||
/// <typeparam name="TSubOutput3">The third transformed <see cref="Type"/> of <typeparamref name="TSubInput2"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer1">The <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TSubInput1"/>/<typeparamref name="TSubOutput2"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer2">The <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TSubInput2"/>/<typeparamref name="TSubOutput2"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer3">The <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TSubInput3"/>/<typeparamref name="TSubOutput3"/>.</typeparam>
|
||||
/// <param name="transformerExpression">The <see cref="Expression{TDelegate}"/> to take a <typeparamref name="TInput"/>, <typeparamref name="TSubOutput1"/>, and <typeparamref name="TSubOutput1"/> and produce a <typeparamref name="TOutput"/>.</param>
|
||||
/// <param name="subInput1SelectionExpression">The <see cref="Expression{TDelegate}"/> to select <typeparamref name="TSubInput1"/> from <typeparamref name="TInput"/>.</param>
|
||||
/// <param name="subInput2SelectionExpression">The <see cref="Expression{TDelegate}"/> to select <typeparamref name="TSubInput2"/> from <typeparamref name="TInput"/>.</param>
|
||||
/// <param name="subInput3SelectionExpression">The <see cref="Expression{TDelegate}"/> to select <typeparamref name="TSubInput3"/> from <typeparamref name="TInput"/>.</param>
|
||||
/// <returns>An expression converting <typeparamref name="TInput"/> into <typeparamref name="TOutput"/> based on <paramref name="transformerExpression"/> with its other arguments generated from the transformation result of <paramref name="subInput1SelectionExpression"/>, <paramref name="subInput2SelectionExpression"/>, <paramref name="subInput3SelectionExpression"/>.</returns>
|
||||
protected static Expression<Func<TInput, TOutput>> BuildSubProjection<
|
||||
TSubInput1,
|
||||
TSubInput2,
|
||||
TSubInput3,
|
||||
TSubOutput1,
|
||||
TSubOutput2,
|
||||
TSubOutput3,
|
||||
TTransformer1,
|
||||
TTransformer2,
|
||||
TTransformer3>(
|
||||
Expression<Func<TInput, TSubOutput1?, TSubOutput2?, TSubOutput3?, TOutput>> transformerExpression,
|
||||
Expression<Func<TInput, TSubInput1?>> subInput1SelectionExpression,
|
||||
Expression<Func<TInput, TSubInput2?>> subInput2SelectionExpression,
|
||||
Expression<Func<TInput, TSubInput3?>> subInput3SelectionExpression)
|
||||
where TSubOutput1 : class
|
||||
where TSubOutput2 : class
|
||||
where TSubOutput3 : class
|
||||
where TTransformer1 : ITransformer<TSubInput1, TSubOutput1>, new()
|
||||
where TTransformer2 : ITransformer<TSubInput2, TSubOutput2>, new()
|
||||
where TTransformer3 : ITransformer<TSubInput3, TSubOutput3>, new()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransformerBase{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -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 <see cref="User"/> that has been updated.
|
||||
/// </summary>
|
||||
public sealed class UpdatedUser :
|
||||
ILegacyApiTransformable<UserResponse>,
|
||||
IApiTransformable<UpdatedUser, GraphQL.Types.UpdatedUser, UpdatedUserTransformer>
|
||||
IApiTransformable<UpdatedUser, UserResponse>,
|
||||
IApiTransformable<UpdatedUser, GraphQL.Types.UpdatedUser>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="User"/>'s <see cref="Api.Models.EntityId.Id"/>.
|
||||
@@ -40,12 +39,5 @@ namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UserResponse ToApi()
|
||||
=> User?.ToApi() ?? new UserResponse
|
||||
{
|
||||
Id = Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.UserModelBase" />
|
||||
public sealed class User : Api.Models.Internal.UserModelBase,
|
||||
ILegacyApiTransformable<UserResponse>,
|
||||
IApiTransformable<User, GraphQL.Types.User, UserTransformer>,
|
||||
IApiTransformable<User, GraphQL.Types.UserName, UserNameTransformer>
|
||||
IApiTransformable<User, UserResponse>,
|
||||
IApiTransformable<User, GraphQL.Types.User>,
|
||||
IApiTransformable<User, GraphQL.Types.UserName>
|
||||
{
|
||||
/// <summary>
|
||||
/// Username used when creating jobs automatically.
|
||||
@@ -88,33 +86,5 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <param name="name">The <see cref="UserName.Name"/>.</param>
|
||||
/// <returns>The <see cref="CanonicalName"/>.</returns>
|
||||
public static string CanonicalizeName(string name) => name?.ToUpperInvariant() ?? throw new ArgumentNullException(nameof(name));
|
||||
|
||||
/// <inheritdoc />
|
||||
public UserResponse ToApi() => CreateUserResponse(true);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a <see cref="UserResponse"/> from <see langword="this"/>.
|
||||
/// </summary>
|
||||
/// <param name="recursive">If we should recurse on <see cref="CreatedBy"/>.</param>
|
||||
/// <returns>A new <see cref="UserResponse"/>.</returns>
|
||||
UserResponse CreateUserResponse(bool recursive)
|
||||
{
|
||||
var result = CreateUserName<UserResponse>();
|
||||
if (recursive)
|
||||
result.CreatedBy = CreatedBy?.CreateUserName<UserName>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a group of <see cref="User"/>s.
|
||||
/// </summary>
|
||||
public sealed class UserGroup : NamedEntity, ILegacyApiTransformable<UserGroupResponse>, IApiTransformable<UserGroup, GraphQL.Types.UserGroup, UserGroupTransformer>
|
||||
public sealed class UserGroup : NamedEntity, ILegacyApiTransformable<UserGroupResponse>, IApiTransformable<UserGroup, GraphQL.Types.UserGroup>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Models.PermissionSet"/> the <see cref="UserGroup"/> has.
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <summary>
|
||||
/// Interface for accessing the current request's <see cref="ClaimsPrincipal"/>.
|
||||
/// </summary>
|
||||
interface IClaimsPrincipalAccessor
|
||||
public interface IClaimsPrincipalAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the current <see cref="ClaimsPrincipal"/>.
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.EntityId.Id"/> field available.</param>
|
||||
/// <param name="serviceLogin">Whether or not this is an external service login.</param>
|
||||
/// <returns>A new token <see cref="string"/>.</returns>
|
||||
string CreateToken(Models.User user, bool serviceLogin);
|
||||
/// <returns>A new token <see cref="string"/> and the <see cref="DateTimeOffset"/> that it expires.</returns>
|
||||
(string Token, DateTimeOffset Expiry) CreateToken(Models.User user, bool serviceLogin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user