mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-31 00:50:45 +01:00
WIP
This commit is contained in:
@@ -90,154 +90,163 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
async Task<AdministrationResponse> CacheFactory()
|
||||
public RequirementsGated<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.ChangeVersion),
|
||||
async () =>
|
||||
{
|
||||
Version? greatestVersion = null;
|
||||
Uri? repoUrl = null;
|
||||
var scopeCancellationToken = CancellationToken.None; // DCT: None available
|
||||
try
|
||||
{
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(scopeCancellationToken);
|
||||
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(scopeCancellationToken);
|
||||
var releases = await gitHubService.GetTgsReleases(scopeCancellationToken);
|
||||
|
||||
foreach (var kvp in releases)
|
||||
async Task<AdministrationResponse> CacheFactory()
|
||||
{
|
||||
var version = kvp.Key;
|
||||
var release = kvp.Value;
|
||||
if (version.Major > 3 // Forward/backward compatible but not before TGS4
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
Version? greatestVersion = null;
|
||||
Uri? repoUrl = null;
|
||||
var scopeCancellationToken = CancellationToken.None; // DCT: None available
|
||||
try
|
||||
{
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(scopeCancellationToken);
|
||||
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(scopeCancellationToken);
|
||||
var releases = await gitHubService.GetTgsReleases(scopeCancellationToken);
|
||||
|
||||
foreach (var kvp in releases)
|
||||
{
|
||||
var version = kvp.Key;
|
||||
var release = kvp.Value;
|
||||
if (version.Major > 3 // Forward/backward compatible but not before TGS4
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
}
|
||||
|
||||
repoUrl = await repositoryUrlTask;
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
|
||||
}
|
||||
|
||||
return new AdministrationResponse
|
||||
{
|
||||
LatestVersion = greatestVersion,
|
||||
TrackedRepositoryUrl = repoUrl,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
repoUrl = await repositoryUrlTask;
|
||||
var ttl = TimeSpan.FromMinutes(30);
|
||||
Task<AdministrationResponse> task;
|
||||
if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
|
||||
{
|
||||
using var entry = cacheService.CreateEntry(ReadCacheKey);
|
||||
entry.AbsoluteExpirationRelativeToNow = ttl;
|
||||
entry.Value = task = CacheFactory();
|
||||
}
|
||||
else
|
||||
task = (Task<AdministrationResponse>)rawCacheObject!;
|
||||
|
||||
var result = await task.WaitAsync(cancellationToken);
|
||||
return new AuthorityResponse<AdministrationResponse>(result);
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
|
||||
return RateLimit<AdministrationResponse>(e);
|
||||
}
|
||||
|
||||
return new AdministrationResponse
|
||||
catch (ApiException e)
|
||||
{
|
||||
LatestVersion = greatestVersion,
|
||||
TrackedRepositoryUrl = repoUrl,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
var ttl = TimeSpan.FromMinutes(30);
|
||||
Task<AdministrationResponse> task;
|
||||
if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
|
||||
{
|
||||
using var entry = cacheService.CreateEntry(ReadCacheKey);
|
||||
entry.AbsoluteExpirationRelativeToNow = ttl;
|
||||
entry.Value = task = CacheFactory();
|
||||
}
|
||||
else
|
||||
task = (Task<AdministrationResponse>)rawCacheObject!;
|
||||
|
||||
var result = await task.WaitAsync(cancellationToken);
|
||||
return new AuthorityResponse<AdministrationResponse>(result);
|
||||
}
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
return RateLimit<AdministrationResponse>(e);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<AdministrationResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message,
|
||||
},
|
||||
HttpFailureResponse.FailedDependency);
|
||||
}
|
||||
}
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<AdministrationResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message,
|
||||
},
|
||||
HttpFailureResponse.FailedDependency);
|
||||
}
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken)
|
||||
{
|
||||
var attemptingUpload = uploadZip == true;
|
||||
if (attemptingUpload)
|
||||
{
|
||||
if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion))
|
||||
return Forbid<ServerUpdateResponse>();
|
||||
}
|
||||
else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion))
|
||||
return Forbid<ServerUpdateResponse>();
|
||||
|
||||
if (targetVersion.Major < 4)
|
||||
return BadRequest<ServerUpdateResponse>(ErrorCode.CannotChangeServerSuite);
|
||||
|
||||
if (!serverControl.WatchdogPresent)
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
|
||||
HttpFailureResponse.UnprocessableEntity);
|
||||
|
||||
IFileUploadTicket? uploadTicket = attemptingUpload
|
||||
? fileTransferService.CreateUpload(FileUploadStreamKind.None)
|
||||
: null;
|
||||
|
||||
ServerUpdateResult updateResult;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken);
|
||||
}
|
||||
catch
|
||||
return new(
|
||||
() =>
|
||||
{
|
||||
if (attemptingUpload)
|
||||
await uploadTicket!.DisposeAsync();
|
||||
return Flag(AdministrationRights.UploadVersion);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (RateLimitExceededException ex)
|
||||
{
|
||||
return RateLimit<ServerUpdateResponse>(ex);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
return Flag(AdministrationRights.ChangeVersion);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
if (targetVersion.Major < 4)
|
||||
return BadRequest<ServerUpdateResponse>(ErrorCode.CannotChangeServerSuite);
|
||||
|
||||
if (!serverControl.WatchdogPresent)
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
|
||||
HttpFailureResponse.UnprocessableEntity);
|
||||
|
||||
IFileUploadTicket? uploadTicket = attemptingUpload
|
||||
? fileTransferService.CreateUpload(FileUploadStreamKind.None)
|
||||
: null;
|
||||
|
||||
ServerUpdateResult updateResult;
|
||||
try
|
||||
{
|
||||
AdditionalData = e.Message,
|
||||
},
|
||||
HttpFailureResponse.FailedDependency);
|
||||
}
|
||||
try
|
||||
{
|
||||
updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (attemptingUpload)
|
||||
await uploadTicket!.DisposeAsync();
|
||||
|
||||
return updateResult switch
|
||||
{
|
||||
ServerUpdateResult.Started => new AuthorityResponse<ServerUpdateResponse>(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted),
|
||||
ServerUpdateResult.ReleaseMissing => Gone<ServerUpdateResponse>(),
|
||||
ServerUpdateResult.UpdateInProgress => BadRequest<ServerUpdateResponse>(ErrorCode.ServerUpdateInProgress),
|
||||
ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed),
|
||||
HttpFailureResponse.FailedDependency),
|
||||
_ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
|
||||
};
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (RateLimitExceededException ex)
|
||||
{
|
||||
return RateLimit<ServerUpdateResponse>(ex);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message,
|
||||
},
|
||||
HttpFailureResponse.FailedDependency);
|
||||
}
|
||||
|
||||
return updateResult switch
|
||||
{
|
||||
ServerUpdateResult.Started => new AuthorityResponse<ServerUpdateResponse>(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted),
|
||||
ServerUpdateResult.ReleaseMissing => Gone<ServerUpdateResponse>(),
|
||||
ServerUpdateResult.UpdateInProgress => BadRequest<ServerUpdateResponse>(ErrorCode.ServerUpdateInProgress),
|
||||
ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed),
|
||||
HttpFailureResponse.FailedDependency),
|
||||
_ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse> TriggerServerRestart()
|
||||
{
|
||||
if (!serverControl.WatchdogPresent)
|
||||
{
|
||||
Logger.LogDebug("Restart request failed due to lack of host watchdog!");
|
||||
return new AuthorityResponse(
|
||||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
|
||||
HttpFailureResponse.UnprocessableEntity);
|
||||
}
|
||||
public RequirementsGated<AuthorityResponse> TriggerServerRestart()
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.RestartHost),
|
||||
async () =>
|
||||
{
|
||||
if (!serverControl.WatchdogPresent)
|
||||
{
|
||||
Logger.LogDebug("Restart request failed due to lack of host watchdog!");
|
||||
return new AuthorityResponse(
|
||||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
|
||||
HttpFailureResponse.UnprocessableEntity);
|
||||
}
|
||||
|
||||
await serverControl.Restart();
|
||||
return new AuthorityResponse();
|
||||
}
|
||||
await serverControl.Restart();
|
||||
return new AuthorityResponse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Octokit;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Security.RightsEvaluation;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
@@ -17,11 +17,6 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// </summary>
|
||||
abstract class AuthorityBase : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IAuthenticationContext"/> for the <see cref="AuthorityBase"/>.
|
||||
/// </summary>
|
||||
protected IAuthenticationContext AuthenticationContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IDatabaseContext"/> for the <see cref="AuthorityBase"/>.
|
||||
/// </summary>
|
||||
@@ -94,18 +89,47 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
new ErrorMessageResponse(errorCode),
|
||||
HttpFailureResponse.Conflict);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to quickly construct a <see cref="FlagRightsConditional{TRights}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
|
||||
/// <param name="flag">The single bit flag of the <typeparamref name="TRights"/>.</param>
|
||||
/// <returns>A new <see cref="FlagRightsConditional{TRights}"/>.</returns>
|
||||
protected static FlagRightsConditional<TRights> Flag<TRights>(TRights flag)
|
||||
where TRights : Enum
|
||||
=> new(flag);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to quickly construct an <see cref="OrRightsConditional{TRights}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
|
||||
/// <param name="lhs">The left hand side operand.</param>
|
||||
/// <param name="rhs">The right hand side operand.</param>
|
||||
/// <returns>A new <see cref="OrRightsConditional{TRights}"/>.</returns>
|
||||
protected static OrRightsConditional<TRights> Or<TRights>(RightsConditional<TRights> lhs, RightsConditional<TRights> rhs)
|
||||
where TRights : Enum
|
||||
=> new(lhs, rhs);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to quickly construct an <see cref="AndRightsConditional{TRights}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
|
||||
/// <param name="lhs">The left hand side operand.</param>
|
||||
/// <param name="rhs">The right hand side operand.</param>
|
||||
/// <returns>A new <see cref="AndRightsConditional{TRights}"/>.</returns>
|
||||
protected static AndRightsConditional<TRights> And<TRights>(RightsConditional<TRights> lhs, RightsConditional<TRights> rhs)
|
||||
where TRights : Enum
|
||||
=> new(lhs, rhs);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorityBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The value of <see cref="AuthenticationContext"/>.</param>
|
||||
/// <param name="databaseContext">The value of <see cref="DatabaseContext"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
protected AuthorityBase(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<AuthorityBase> logger)
|
||||
{
|
||||
AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
|
||||
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
@@ -14,35 +15,49 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// </summary>
|
||||
protected TAuthority Authority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.AspNetCore.Authorization.IAuthorizationService"/> for the <see cref="AuthorityInvokerBase{TAuthority}"/>.
|
||||
/// </summary>
|
||||
readonly IAuthorizationService authorizationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorityInvokerBase{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authority">The value of <see cref="Authority"/>.</param>
|
||||
public AuthorityInvokerBase(TAuthority authority)
|
||||
/// <param name="authorizationService">The value of <see cref="authorizationService"/>.</param>
|
||||
public AuthorityInvokerBase(
|
||||
TAuthority authority,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
Authority = authority ?? throw new ArgumentNullException(nameof(authority));
|
||||
this.authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IQueryable<TResult> IAuthorityInvoker<TAuthority>.InvokeQueryable<TResult>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
async ValueTask<IQueryable<TResult>?> IAuthorityInvoker<TAuthority>.InvokeQueryable<TResult>(Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
return authorityInvoker(Authority);
|
||||
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
return await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IQueryable<TApiModel> IAuthorityInvoker<TAuthority>.InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
/// <summary>
|
||||
/// Unwrap a <see cref="RequirementsGated{TResult}"/> result, returning <see langword="null"/> if the requirements weren't satisfied.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> contained by the <paramref name="requirementsGate"/>.</typeparam>
|
||||
/// <param name="requirementsGate">The <see cref="RequirementsGated{TResult}"/> result.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TResult"/> if the requirements were met, <see langword="null"/> if the requirments weren't met.</returns>
|
||||
protected async ValueTask<TResult?> ExecuteIfRequirementsSatisfied<TResult>(RequirementsGated<TResult> requirementsGate)
|
||||
where TResult : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
var requirements = await requirementsGate.GetRequirements();
|
||||
var authorizationResult = await authorizationService.AuthorizeAsync(requirements);
|
||||
|
||||
var queryable = authorityInvoker(Authority);
|
||||
if (!authorizationResult)
|
||||
return null;
|
||||
|
||||
if (typeof(EntityId).IsAssignableFrom(typeof(TResult)))
|
||||
queryable = queryable.OrderBy(item => ((EntityId)(object)item).Id!.Value); // order by ID to fix an EFCore warning
|
||||
|
||||
var expression = new TTransformer().Expression;
|
||||
return queryable
|
||||
.Select(expression);
|
||||
return await requirementsGate.Execute(authorizationService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.GraphQL;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
@@ -9,17 +13,30 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
sealed class GraphQLAuthorityInvoker<TAuthority> : AuthorityInvokerBase<TAuthority>, IGraphQLAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ErrorMessageException"/> to be thrown when a forbidden error occurs.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="ErrorMessageException"/>.</returns>
|
||||
static ErrorMessageException ForbiddenGraphQLError()
|
||||
=> new(new ErrorMessageResponse(), HttpFailureResponse.Forbidden.ToString());
|
||||
|
||||
/// <summary>
|
||||
/// Throws a <see cref="ErrorMessageException"/> for errored <paramref name="authorityResponse"/>s.
|
||||
/// </summary>
|
||||
/// <param name="authorityResponse">The potentially errored <paramref name="authorityResponse"/>.</param>
|
||||
/// <typeparam name="TAuthorityResponse">The <see cref="AuthorityResponse"/> <see cref="Type"/> being checked.</typeparam>
|
||||
/// <param name="authorityResponse">The potentially errored <paramref name="authorityResponse"/> or <see langword="null"/> if requirements evaluation failed.</param>
|
||||
/// <param name="errorOnMissing">If an error should be raised for <see cref="HttpFailureResponse.NotFound"/> and <see cref="HttpFailureResponse.Gone"/> failures.</param>
|
||||
static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse, bool errorOnMissing)
|
||||
/// <returns><paramref name="authorityResponse"/> if an <see cref="ErrorMessageException"/> wasn't thrown.</returns>
|
||||
static TAuthorityResponse ThrowGraphQLErrorIfNecessary<TAuthorityResponse>(TAuthorityResponse? authorityResponse, bool errorOnMissing)
|
||||
where TAuthorityResponse : AuthorityResponse
|
||||
{
|
||||
if (authorityResponse == null)
|
||||
throw ForbiddenGraphQLError();
|
||||
|
||||
if (authorityResponse.Success
|
||||
|| ((authorityResponse.FailureResponse.Value == HttpFailureResponse.NotFound
|
||||
|| authorityResponse.FailureResponse.Value == HttpFailureResponse.Gone) && !errorOnMissing))
|
||||
return;
|
||||
return authorityResponse;
|
||||
|
||||
var fallbackString = authorityResponse.FailureResponse.ToString()!;
|
||||
throw new ErrorMessageException(authorityResponse.ErrorMessage, fallbackString);
|
||||
@@ -29,40 +46,42 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// Initializes a new instance of the <see cref="GraphQLAuthorityInvoker{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authority">The <typeparamref name="TAuthority"/>.</param>
|
||||
public GraphQLAuthorityInvoker(TAuthority authority)
|
||||
: base(authority)
|
||||
/// <param name="authorizationService">the <see cref="IAuthorizationService"/> to use.</param>
|
||||
public GraphQLAuthorityInvoker(TAuthority authority, IAuthorizationService authorizationService)
|
||||
: base(authority, authorizationService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask IGraphQLAuthorityInvoker<TAuthority>.Invoke(Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker)
|
||||
async ValueTask IGraphQLAuthorityInvoker<TAuthority>.Invoke(Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TApiModel : default
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse, false);
|
||||
return authorityResponse.Result;
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
return ThrowGraphQLErrorIfNecessary(authorityResponse, false).Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TApiModel : default
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse, false);
|
||||
var result = authorityResponse.Result;
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
var result = ThrowGraphQLErrorIfNecessary(authorityResponse, false).Result;
|
||||
if (result == null)
|
||||
return default;
|
||||
|
||||
@@ -70,11 +89,33 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<IQueryable<TApiModel>> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker,
|
||||
Func<IQueryable<TResult>, IQueryable<TResult>>? preTransformer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var queryable = await ExecuteIfRequirementsSatisfied(requirementsGate)
|
||||
?? throw ForbiddenGraphQLError();
|
||||
|
||||
if (preTransformer != null)
|
||||
queryable = preTransformer(queryable);
|
||||
|
||||
if (typeof(EntityId).IsAssignableFrom(typeof(TResult)))
|
||||
queryable = queryable.OrderBy(item => ((EntityId)(object)item).Id!.Value); // order by ID to fix an EFCore warning
|
||||
|
||||
var expression = new TTransformer().Expression;
|
||||
return queryable
|
||||
.Select(expression);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
=> ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeAllowMissing<TResult, TApiModel>(authorityInvoker)!;
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
=> ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(authorityInvoker)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Tgstation.Server.Host.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
@@ -16,21 +15,8 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned.</returns>
|
||||
IQueryable<TResult> InvokeQueryable<TResult>(Func<TAuthority, IQueryable<TResult>> authorityInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the transformed result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> returned by the <typeparamref name="TAuthority"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned <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="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned.</returns>
|
||||
IQueryable<TApiModel> InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
where TResult : IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
/// <param name="authorityInvoker">The authority invocation returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned on success or <see langword="null"/> if the requirements weren't satisfied.</returns>
|
||||
ValueTask<IQueryable<TResult>?> InvokeQueryable<TResult>(Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a set of <see cref="IAuthorizationRequirement"/>s to be checked before executing a response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> of object the response generates.</typeparam>
|
||||
public sealed class RequirementsGated<TResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthorizationRequirement"/> retrieval function. <see cref="UserSessionValidRequirement"/> is included automatically.
|
||||
/// </summary>
|
||||
readonly Func<ValueTask<IEnumerable<IAuthorizationRequirement>>> getRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// The response generation function.
|
||||
/// </summary>
|
||||
readonly Func<Security.IAuthorizationService, ValueTask<TResult>> getResponse;
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <paramref name="result"/> into a <see cref="RequirementsGated{TResult}"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <typeparamref name="TResult"/> to convert.</param>
|
||||
/// <returns>A new <see cref="RequirementsGated{TResult}"/> based on <paramref name="result"/>.</returns>
|
||||
#pragma warning disable CA1000 // Do not declare static members on generic types
|
||||
public static RequirementsGated<TResult> FromResult(TResult result)
|
||||
#pragma warning restore CA1000 // Do not declare static members on generic types
|
||||
=> new(
|
||||
() => (IAuthorizationRequirement?)null,
|
||||
() => ValueTask.FromResult(result));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirement">The value of <see cref="getRequirements"/>. Resulting in a <see langword="null"/> value is eqivalent to returning an empty <see cref="IEnumerable{T}"/> of <see cref="IAuthorizationRequirement"/>s.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<ValueTask<IAuthorizationRequirement?>> getRequirement,
|
||||
Func<ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirement);
|
||||
ArgumentNullException.ThrowIfNull(getResponse);
|
||||
getRequirements = async () =>
|
||||
{
|
||||
var requirement = await getRequirement();
|
||||
if (requirement == null)
|
||||
return Enumerable.Empty<IAuthorizationRequirement>();
|
||||
|
||||
return new List<IAuthorizationRequirement>
|
||||
{
|
||||
requirement,
|
||||
};
|
||||
};
|
||||
this.getResponse = _ => getResponse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirements">The value of <see cref="getRequirements"/>.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<IEnumerable<IAuthorizationRequirement>> getRequirements,
|
||||
Func<ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirements);
|
||||
ArgumentNullException.ThrowIfNull(getResponse);
|
||||
this.getRequirements = () => ValueTask.FromResult(getRequirements());
|
||||
this.getResponse = _ => getResponse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirement">The value of <see cref="getRequirements"/>. Resulting in a <see langword="null"/> value is eqivalent to returning an empty <see cref="IEnumerable{T}"/> of <see cref="IAuthorizationRequirement"/>s.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<IAuthorizationRequirement?> getRequirement,
|
||||
Func<ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirement);
|
||||
ArgumentNullException.ThrowIfNull(getResponse);
|
||||
getRequirements = () =>
|
||||
{
|
||||
var requirement = getRequirement();
|
||||
if (requirement == null)
|
||||
return ValueTask.FromResult(Enumerable.Empty<IAuthorizationRequirement>());
|
||||
|
||||
return ValueTask.FromResult<IEnumerable<IAuthorizationRequirement>>(
|
||||
new List<IAuthorizationRequirement>
|
||||
{
|
||||
requirement,
|
||||
});
|
||||
};
|
||||
|
||||
this.getResponse = _ => getResponse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirement">The value of <see cref="getRequirements"/>. Resulting in a <see langword="null"/> value is eqivalent to returning an empty <see cref="IEnumerable{T}"/> of <see cref="IAuthorizationRequirement"/>s.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<IAuthorizationRequirement?> getRequirement,
|
||||
Func<Security.IAuthorizationService, ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirement);
|
||||
getRequirements = () =>
|
||||
{
|
||||
var requirement = getRequirement();
|
||||
if (requirement == null)
|
||||
return ValueTask.FromResult(Enumerable.Empty<IAuthorizationRequirement>());
|
||||
|
||||
return ValueTask.FromResult<IEnumerable<IAuthorizationRequirement>>(
|
||||
new List<IAuthorizationRequirement>
|
||||
{
|
||||
requirement,
|
||||
});
|
||||
};
|
||||
|
||||
this.getResponse = getResponse ?? throw new ArgumentNullException(nameof(getResponse));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the <see cref="IAuthorizationRequirement"/>s of the request.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IAuthorizationRequirement"/>s for the request.</returns>
|
||||
public async ValueTask<IEnumerable<IAuthorizationRequirement>> GetRequirements()
|
||||
=> (await getRequirements()).Concat([new UserSessionValidRequirement()]);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request.
|
||||
/// </summary>
|
||||
/// <param name="authorizationService">The authorization service to use.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the request <typeparamref name="TResult"/>.</returns>
|
||||
public ValueTask<TResult> Execute(Security.IAuthorizationService authorizationService)
|
||||
=> getResponse(authorizationService);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using Tgstation.Server.Host.Controllers;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
@@ -22,7 +23,10 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// <returns>An <see cref="IActionResult"/> for the <paramref name="authorityResponse"/>.</returns>
|
||||
/// <typeparam name="TResult">The result <see cref="Type"/> returned in the <paramref name="authorityResponse"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The REST API result model built from <paramref name="authorityResponse"/>.</typeparam>
|
||||
static IActionResult CreateSuccessfulActionResult<TResult, TApiModel>(ApiController controller, Func<TResult, TApiModel> resultTransformer, AuthorityResponse<TResult> authorityResponse)
|
||||
static IActionResult CreateSuccessfulActionResult<TResult, TApiModel>(
|
||||
ApiController controller,
|
||||
Func<TResult, TApiModel> resultTransformer,
|
||||
AuthorityResponse<TResult> authorityResponse)
|
||||
where TApiModel : notnull
|
||||
{
|
||||
if (authorityResponse.IsNoContent!.Value)
|
||||
@@ -44,9 +48,14 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// </summary>
|
||||
/// <param name="controller">The <see cref="ApiController"/> to use.</param>
|
||||
/// <param name="authorityResponse">The <see cref="AuthorityResponse"/>.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> if the <paramref name="authorityResponse"/> is not successful, <see langword="null"/> otherwise.</returns>
|
||||
static IActionResult? CreateErroredActionResult(ApiController controller, AuthorityResponse authorityResponse)
|
||||
/// <returns>An <see cref="IActionResult"/> if the <paramref name="authorityResponse"/> is not successful, <see langword="null"/> otherwise. If <see langword="null"/> is returned, <paramref name="authorityResponse"/> is not <see langword="null"/>.</returns>
|
||||
static IActionResult? CreateErroredActionResult(
|
||||
ApiController controller,
|
||||
AuthorityResponse? authorityResponse)
|
||||
{
|
||||
if (authorityResponse == null)
|
||||
return controller.Forbid();
|
||||
|
||||
if (authorityResponse.Success)
|
||||
return null;
|
||||
|
||||
@@ -74,47 +83,51 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// Initializes a new instance of the <see cref="RestAuthorityInvoker{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authority">The <typeparamref name="TAuthority"/>.</param>
|
||||
public RestAuthorityInvoker(TAuthority authority)
|
||||
: base(authority)
|
||||
/// <param name="authorizationService">The <see cref="IAuthorizationService"/> to use.</param>
|
||||
public RestAuthorityInvoker(TAuthority authority, IAuthorizationService authorizationService)
|
||||
: base(authority, authorizationService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker)
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
return CreateErroredActionResult(controller, authorityResponse) ?? controller.NoContent();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
var erroredResult = CreateErroredActionResult(controller, authorityResponse);
|
||||
if (erroredResult != null)
|
||||
return erroredResult;
|
||||
|
||||
return CreateSuccessfulActionResult(controller, result => result, authorityResponse);
|
||||
return CreateSuccessfulActionResult(controller, result => result, authorityResponse!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
var erroredResult = CreateErroredActionResult(controller, authorityResponse);
|
||||
if (erroredResult != null)
|
||||
return erroredResult;
|
||||
|
||||
return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse);
|
||||
return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
@@ -19,9 +18,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <param name="forceFresh">Bypass the caching that the authority performs for this request, forcing it to contact GitHub.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AdministrationResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AdministrationResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion)]
|
||||
ValueTask<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a restart of tgstation-server without terminating running game instances, setting its version to a given <paramref name="targetVersion"/>.
|
||||
@@ -29,15 +28,15 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="targetVersion">The <see cref="Version"/> TGS will switch to upon reboot.</param>
|
||||
/// <param name="uploadZip">If <see langword="true"/> a <see cref="FileTicketResponse.FileTicket"/> will be returned and the call must provide an uploaded zip file containing the update data to the file transfer service.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="ServerUpdateResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="ServerUpdateResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)]
|
||||
ValueTask<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a restart of tgstation-server without terminating running game instances.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.RestartHost)]
|
||||
ValueTask<AuthorityResponse> TriggerServerRestart();
|
||||
RequirementsGated<AuthorityResponse> TriggerServerRestart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
@@ -10,24 +11,25 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Invokes <typeparamref name="TAuthority"/>s from GraphQL endpoints.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> invoked.</typeparam>
|
||||
/// <remarks>We take the approach that fields should be non-nullable if that is the case under ideal circumstances. Authorization issues should throw.</remarks>
|
||||
public interface IGraphQLAuthorityInvoker<TAuthority> : IAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method with no success result.
|
||||
/// </summary>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask Invoke(Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker);
|
||||
ValueTask Invoke(Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker);
|
||||
|
||||
/// <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 resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</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 <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel?> InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel?> InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
|
||||
@@ -37,9 +39,9 @@ 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>
|
||||
/// <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}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</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 <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
@@ -49,9 +51,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </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>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</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 <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel> Invoke<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
|
||||
@@ -61,11 +63,27 @@ 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>
|
||||
/// <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}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</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 <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the transformed result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> returned by the <typeparamref name="TAuthority"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned <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="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <param name="preTransformer">Optional transformer for the <see cref="IQueryable{T}"/> run once it has been acquired.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned on success or <see langword="null"/> if the requirements weren't satisfied.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </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>
|
||||
ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to login to an OAuth service with the current OAuth credentials.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="OAuthGatewayLoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority
|
||||
{
|
||||
@@ -20,7 +18,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="lookupType">The <see cref="PermissionSetLookupType"/> of <paramref name="id"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="PermissionSet"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
ValueTask<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method with no success result.
|
||||
/// </summary>
|
||||
/// <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}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse"/>.</returns>
|
||||
ValueTask<IActionResult> Invoke(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker);
|
||||
ValueTask<IActionResult> Invoke(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
@@ -30,9 +30,9 @@ 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 <see cref="IActionResult"/>.</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}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</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> Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<IActionResult> Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned REST <see cref="Type"/>.</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}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</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>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, ILegacyApiTransformable<TApiModel>
|
||||
where TApiModel : notnull;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
@@ -20,9 +19,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Gets the currently authenticated user.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize]
|
||||
ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
|
||||
@@ -31,33 +30,33 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <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="ValueTask{TResult}"/> resulting in a <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="GraphQL.Types.OAuth.OAuthConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
|
||||
/// </summary>
|
||||
/// <param name="userId">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OAuthConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OAuthConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="GraphQL.Types.OAuth.OidcConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
|
||||
/// </summary>
|
||||
/// <param name="userId">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OidcConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OidcConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
IQueryable<User> Queryable(bool includeJoins);
|
||||
RequirementsGated<IQueryable<User>> Queryable(bool includeJoins);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="User"/>.
|
||||
@@ -65,9 +64,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="createRequest">The <see cref="UserCreateRequest"/>.</param>
|
||||
/// <param name="needZeroLengthPasswordWithOAuthConnections">If a zero-length <see cref="UserUpdateRequest.Password"/> indicates and OAuth only user.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in am <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<User>> Create(
|
||||
RequirementsGated<AuthorityResponse<User>> Create(
|
||||
UserCreateRequest createRequest,
|
||||
bool? needZeroLengthPasswordWithOAuthConnections,
|
||||
CancellationToken cancellationToken);
|
||||
@@ -77,8 +76,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <param name="updateRequest">The <see cref="UserUpdateRequest"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in am <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnServiceConnections)]
|
||||
ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<User>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="UserGroup"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<UserGroup>> Read();
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> Read(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="UserGroup"/> with a given <paramref name="id"/>.
|
||||
@@ -26,17 +27,17 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="UserGroup"/>.</param>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
ValueTask<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="UserGroup"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
IQueryable<UserGroup> Queryable(bool includeJoins);
|
||||
RequirementsGated<IQueryable<UserGroup>> Queryable(bool includeJoins);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="UserGroup"/>.
|
||||
@@ -44,9 +45,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="name">The created <see cref="UserGroup"/>'s <see cref="Api.Models.NamedEntity.Name"/>.</param>
|
||||
/// <param name="permissionSet">The created <see cref="UserGroup"/>'s <see cref="UserGroup.PermissionSet"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<UserGroup>> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a <see cref="UserGroup"/>.
|
||||
@@ -55,17 +56,17 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="newName">The optional new <see cref="Api.Models.NamedEntity.Name"/> for the <see cref="UserGroup"/>.</param>
|
||||
/// <param name="newPermissionSet">The optional new <see cref="UserGroup.PermissionSet"/> for the <see cref="UserGroup"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="RequirementsGated{TResult}"/> <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<UserGroup>> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an empty <see cref="UserGroup"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="UserGroup"/> to delete.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/> representing the running operation.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse> DeleteEmpty(long id, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse> DeleteEmpty(long id, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -103,7 +104,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="apiHeadersProvider">The value of <see cref="apiHeadersProvider"/>.</param>
|
||||
@@ -115,7 +115,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="sessionInvalidationTracker">The value of <see cref="sessionInvalidationTracker"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="securityConfiguration"/>.</param>
|
||||
public LoginAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<LoginAuthority> logger,
|
||||
IApiHeadersProvider apiHeadersProvider,
|
||||
@@ -127,7 +126,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
ISessionInvalidationTracker sessionInvalidationTracker,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
logger)
|
||||
{
|
||||
@@ -142,7 +140,44 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => (IAuthorizationRequirement?)null,
|
||||
() => AttemptLoginImpl(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => (IAuthorizationRequirement?)null,
|
||||
async () =>
|
||||
{
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
|
||||
|
||||
var oAuthProvider = headers.OAuthProvider;
|
||||
if (!oAuthProvider.HasValue)
|
||||
return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
|
||||
|
||||
var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
|
||||
if (errorResponse != null)
|
||||
return errorResponse;
|
||||
|
||||
Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
|
||||
|
||||
return new(
|
||||
new OAuthGatewayLoginResult
|
||||
{
|
||||
AccessCode = oAuthResult!.Value.AccessCode,
|
||||
});
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
// password and oauth logins disabled
|
||||
if (securityConfiguration.OidcStrictMode)
|
||||
@@ -278,30 +313,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
|
||||
{
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
|
||||
|
||||
var oAuthProvider = headers.OAuthProvider;
|
||||
if (!oAuthProvider.HasValue)
|
||||
return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
|
||||
|
||||
var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
|
||||
if (errorResponse != null)
|
||||
return errorResponse;
|
||||
|
||||
Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
|
||||
|
||||
return new AuthorityResponse<OAuthGatewayLoginResult>(
|
||||
new OAuthGatewayLoginResult
|
||||
{
|
||||
AccessCode = oAuthResult!.Value.AccessCode,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a given <paramref name="systemIdentity"/> to the <see cref="identityCache"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
@@ -25,6 +26,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly IPermissionSetsDataLoader permissionSetsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the <see cref="PermissionSetAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="permissionSetsDataLoader"/>.
|
||||
/// </summary>
|
||||
@@ -84,34 +90,59 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PermissionSetAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="permissionSetsDataLoader">The value of <see cref="permissionSetsDataLoader"/>.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
public PermissionSetAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<AuthorityBase> logger,
|
||||
IPermissionSetsDataLoader permissionSetsDataLoader)
|
||||
IPermissionSetsDataLoader permissionSetsDataLoader,
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
logger)
|
||||
{
|
||||
this.permissionSetsDataLoader = permissionSetsDataLoader ?? throw new ArgumentNullException(nameof(permissionSetsDataLoader));
|
||||
this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != AuthenticationContext.PermissionSet.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid<PermissionSet>();
|
||||
var permissionSetTask = permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken);
|
||||
return new(
|
||||
async () =>
|
||||
{
|
||||
var userId = claimsPrincipalAccessor.User.GetTgsUserId();
|
||||
|
||||
var permissionSet = await permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken);
|
||||
if (permissionSet == null)
|
||||
return NotFound<PermissionSet>();
|
||||
var groupIdQuery = DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(user => user.Id == userId)
|
||||
.Select(user => user.GroupId);
|
||||
|
||||
return new AuthorityResponse<PermissionSet>(permissionSet);
|
||||
var permissionSetId = await DatabaseContext
|
||||
.PermissionSets
|
||||
.Where(permissionSet => permissionSet.UserId == userId
|
||||
|| groupIdQuery.Contains(permissionSet.GroupId))
|
||||
.Select(permissionSet => permissionSet.Id!.Value)
|
||||
.FirstAsync(cancellationToken);
|
||||
|
||||
if (permissionSetId == id)
|
||||
return null;
|
||||
|
||||
return Flag(AdministrationRights.ReadUsers);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
var permissionSet = await permissionSetTask;
|
||||
|
||||
if (permissionSet == null)
|
||||
return NotFound<PermissionSet>();
|
||||
|
||||
return new AuthorityResponse<PermissionSet>(permissionSet);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using GreenDonut;
|
||||
|
||||
using HotChocolate.Subscriptions;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -22,9 +23,11 @@ using Tgstation.Server.Common.Extensions;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Security.RightsEvaluation;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority
|
||||
{
|
||||
@@ -71,6 +74,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly ITopicEventSender topicEventSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> of <see cref="GeneralConfiguration"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
@@ -179,7 +187,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="usersDataLoader">The value of <see cref="usersDataLoader"/>.</param>
|
||||
@@ -190,10 +197,10 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
/// <param name="sessionInvalidationTracker">The value of <see cref="sessionInvalidationTracker"/>.</param>
|
||||
/// <param name="topicEventSender">The value of <see cref="topicEventSender"/>.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The value of <see cref="securityConfigurationOptions"/>.</param>
|
||||
public UserAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<UserAuthority> logger,
|
||||
IUsersDataLoader usersDataLoader,
|
||||
@@ -204,10 +211,10 @@ namespace Tgstation.Server.Host.Authority
|
||||
ICryptographySuite cryptographySuite,
|
||||
ISessionInvalidationTracker sessionInvalidationTracker,
|
||||
ITopicEventSender topicEventSender,
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
logger)
|
||||
{
|
||||
@@ -219,6 +226,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
|
||||
this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker));
|
||||
this.topicEventSender = topicEventSender ?? throw new ArgumentNullException(nameof(topicEventSender));
|
||||
this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor));
|
||||
this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
}
|
||||
@@ -293,316 +301,359 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(new AuthorityResponse<User>(AuthenticationContext.User));
|
||||
public RequirementsGated<AuthorityResponse<User>> Read(CancellationToken cancellationToken)
|
||||
=> GetId(
|
||||
claimsPrincipalAccessor.User.GetTgsUserId(),
|
||||
false,
|
||||
false,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != AuthenticationContext.User.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid<User>();
|
||||
public RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
if (id != claimsPrincipalAccessor.User.GetTgsUserId())
|
||||
return Enumerable.Empty<IAuthorizationRequirement>();
|
||||
|
||||
User? user;
|
||||
if (includeJoins)
|
||||
{
|
||||
var queryable = Queryable(true, true);
|
||||
return new List<IAuthorizationRequirement>
|
||||
{
|
||||
Flag(AdministrationRights.ReadUsers),
|
||||
};
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
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);
|
||||
user = await queryable.FirstOrDefaultAsync(
|
||||
dbModel => dbModel.Id == id,
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
user = await usersDataLoader.LoadAsync(id, cancellationToken);
|
||||
|
||||
if (user == default)
|
||||
return NotFound<User>();
|
||||
if (user == default)
|
||||
return NotFound<User>();
|
||||
|
||||
if (!allowSystemUser && user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<User>();
|
||||
if (!allowSystemUser && user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<User>();
|
||||
|
||||
return new AuthorityResponse<User>(user);
|
||||
}
|
||||
return new AuthorityResponse<User>(user);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<User> Queryable(bool includeJoins)
|
||||
=> Queryable(includeJoins, false);
|
||||
public RequirementsGated<IQueryable<User>> Queryable(bool includeJoins)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.ReadUsers),
|
||||
() => ValueTask.FromResult(Queryable(includeJoins, false)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>(
|
||||
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
|
||||
public RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => claimsPrincipalAccessor.User.GetTgsUserId() != userId
|
||||
? Flag(AdministrationRights.ReadUsers)
|
||||
: null,
|
||||
async () => new AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>(
|
||||
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>(
|
||||
await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
|
||||
public RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => claimsPrincipalAccessor.User.GetTgsUserId() != userId
|
||||
? Flag(AdministrationRights.ReadUsers)
|
||||
: null,
|
||||
async () => new AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>(
|
||||
await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> Create(
|
||||
public RequirementsGated<AuthorityResponse<User>> Create(
|
||||
UserCreateRequest createRequest,
|
||||
bool? needZeroLengthPasswordWithOAuthConnections,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(createRequest);
|
||||
|
||||
if (BadCreateRequestChecks(createRequest, needZeroLengthPasswordWithOAuthConnections, out var failResponse))
|
||||
return failResponse;
|
||||
|
||||
var totalUsers = await DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.CountAsync(cancellationToken);
|
||||
if (totalUsers >= generalConfigurationOptions.Value.UserLimit)
|
||||
return Conflict<User>(ErrorCode.UserLimitReached);
|
||||
|
||||
var dbUser = await CreateNewUserFromModel(createRequest, cancellationToken);
|
||||
if (dbUser == null)
|
||||
return Gone<User>();
|
||||
|
||||
if (createRequest.SystemIdentifier != null)
|
||||
try
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.WriteUsers),
|
||||
async () =>
|
||||
{
|
||||
using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken);
|
||||
if (sysIdentity == null)
|
||||
ArgumentNullException.ThrowIfNull(createRequest);
|
||||
|
||||
if (BadCreateRequestChecks(createRequest, needZeroLengthPasswordWithOAuthConnections, out var failResponse))
|
||||
return failResponse;
|
||||
|
||||
var totalUsers = await DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.CountAsync(cancellationToken);
|
||||
if (totalUsers >= generalConfigurationOptions.Value.UserLimit)
|
||||
return Conflict<User>(ErrorCode.UserLimitReached);
|
||||
|
||||
var dbUser = await CreateNewUserFromModel(
|
||||
createRequest,
|
||||
cancellationToken);
|
||||
if (dbUser == null)
|
||||
return Gone<User>();
|
||||
dbUser.Name = sysIdentity.Username;
|
||||
dbUser.SystemIdentifier = sysIdentity.Uid;
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "System identities not implemented!");
|
||||
return new AuthorityResponse<User>(
|
||||
new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity),
|
||||
HttpFailureResponse.NotImplemented);
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasZeroLengthPassword = createRequest.Password?.Length == 0;
|
||||
var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true;
|
||||
|
||||
// special case allow PasswordHash to be null by setting Password to "" if OAuthConnections are set
|
||||
if (!(needZeroLengthPasswordWithOAuthConnections != false && hasZeroLengthPassword && hasOAuthConnections))
|
||||
{
|
||||
var result = TrySetPassword(dbUser, createRequest.Password!, true);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (createRequest.SystemIdentifier != null)
|
||||
try
|
||||
{
|
||||
using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken);
|
||||
if (sysIdentity == null)
|
||||
return Gone<User>();
|
||||
dbUser.Name = sysIdentity.Username;
|
||||
dbUser.SystemIdentifier = sysIdentity.Uid;
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "System identities not implemented!");
|
||||
return new AuthorityResponse<User>(
|
||||
new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity),
|
||||
HttpFailureResponse.NotImplemented);
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasZeroLengthPassword = createRequest.Password?.Length == 0;
|
||||
var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true;
|
||||
|
||||
dbUser.CanonicalName = User.CanonicalizeName(dbUser.Name!);
|
||||
// special case allow PasswordHash to be null by setting Password to "" if OAuthConnections are set
|
||||
if (!(needZeroLengthPasswordWithOAuthConnections != false && hasZeroLengthPassword && hasOAuthConnections))
|
||||
{
|
||||
var result = TrySetPassword(dbUser, createRequest.Password!, true);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseContext.Users.Add(dbUser);
|
||||
dbUser.CanonicalName = User.CanonicalizeName(dbUser.Name!);
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
DatabaseContext.Users.Add(dbUser);
|
||||
|
||||
Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id);
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
await SendUserUpdatedTopics(dbUser);
|
||||
Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id);
|
||||
|
||||
return new AuthorityResponse<User>(dbUser, HttpSuccessResponse.Created);
|
||||
}
|
||||
await SendUserUpdatedTopics(dbUser);
|
||||
|
||||
return new AuthorityResponse<User>(dbUser, HttpSuccessResponse.Created);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<User>> Update(UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1502
|
||||
#pragma warning restore CA1506
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
|
||||
if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest<User>(ErrorCode.ModelValidationFailure);
|
||||
|
||||
if (model.Group != null && model.PermissionSet != null)
|
||||
return BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
|
||||
|
||||
var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
|
||||
var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
|
||||
var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword);
|
||||
var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnServiceConnections);
|
||||
|
||||
var originalUser = !canEditAllUsers
|
||||
? AuthenticationContext.User
|
||||
: await DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Id)
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.Include(x => x.OidcConnections)
|
||||
.Include(x => x.Group!)
|
||||
.ThenInclude(x => x.PermissionSet)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (originalUser == default)
|
||||
return NotFound<User>();
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<User>();
|
||||
|
||||
// Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request)
|
||||
if ((!canEditAllUsers
|
||||
&& (model.Id != originalUser.Id
|
||||
|| model.Enabled.HasValue
|
||||
|| model.Group != null
|
||||
|| model.PermissionSet != null
|
||||
|| model.Name != null))
|
||||
|| (!passwordEdit && model.Password != null)
|
||||
|| (!oAuthEdit && model.OAuthConnections != null))
|
||||
return Forbid<User>();
|
||||
|
||||
var originalUserHasSid = originalUser.SystemIdentifier != null;
|
||||
var invalidateSessions = false;
|
||||
if (originalUserHasSid && originalUser.PasswordHash != null)
|
||||
{
|
||||
// cleanup from https://github.com/tgstation/tgstation-server/issues/1528
|
||||
Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", originalUser.Id);
|
||||
originalUser.PasswordHash = null;
|
||||
|
||||
invalidateSessions = true;
|
||||
}
|
||||
|
||||
if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
|
||||
return BadRequest<User>(ErrorCode.UserSidChange);
|
||||
|
||||
if (model.Password != null)
|
||||
{
|
||||
if (originalUserHasSid)
|
||||
return BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
|
||||
|
||||
var result = TrySetPassword(originalUser, model.Password, false);
|
||||
if (result != null)
|
||||
return result;
|
||||
|
||||
invalidateSessions = true;
|
||||
}
|
||||
|
||||
if (model.Name != null && User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
|
||||
return BadRequest<User>(ErrorCode.UserNameChange);
|
||||
|
||||
if (model.OAuthConnections != null
|
||||
&& (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count
|
||||
|| !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId))))
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
|
||||
|
||||
DatabaseContext.OAuthConnections.RemoveRange(originalUser.OAuthConnections);
|
||||
originalUser.OAuthConnections.Clear();
|
||||
|
||||
foreach (var updatedConnection in model.OAuthConnections)
|
||||
originalUser.OAuthConnections.Add(new Models.OAuthConnection
|
||||
{
|
||||
Provider = updatedConnection.Provider,
|
||||
ExternalUserId = updatedConnection.ExternalUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model.OidcConnections != null
|
||||
&& (model.OidcConnections.Count != originalUser.OidcConnections!.Count
|
||||
|| !model.OidcConnections.All(x => originalUser.OidcConnections.Any(y => y.SchemeKey == x.SchemeKey && y.ExternalUserId == x.ExternalUserId))))
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
|
||||
|
||||
DatabaseContext.OidcConnections.RemoveRange(originalUser.OidcConnections);
|
||||
originalUser.OidcConnections.Clear();
|
||||
foreach (var updatedConnection in model.OidcConnections)
|
||||
originalUser.OidcConnections.Add(new Models.OidcConnection
|
||||
{
|
||||
SchemeKey = updatedConnection.SchemeKey,
|
||||
ExternalUserId = updatedConnection.ExternalUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model.Group != null)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
originalUser.Group = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Group.Id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (originalUser.Group == default)
|
||||
return Gone<User>();
|
||||
|
||||
DatabaseContext.Groups.Attach(originalUser.Group);
|
||||
if (originalUser.PermissionSet != null)
|
||||
var userQuery = DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Id)
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.Include(x => x.OidcConnections)
|
||||
.Include(x => x.Group!)
|
||||
.ThenInclude(x => x.PermissionSet)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return new(
|
||||
() =>
|
||||
{
|
||||
Logger.LogInformation("Deleting permission set {permissionSetId}...", originalUser.PermissionSet.Id);
|
||||
DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet);
|
||||
originalUser.PermissionSet = null;
|
||||
}
|
||||
}
|
||||
else if (model.PermissionSet != null)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
RightsConditional<AdministrationRights>? conditional = null;
|
||||
|
||||
if (originalUser.PermissionSet == null)
|
||||
// Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request)
|
||||
if (model.OidcConnections != null || model.OAuthConnections != null)
|
||||
conditional = Flag(AdministrationRights.EditOwnServiceConnections);
|
||||
|
||||
if (model.Password != null && model.Id == claimsPrincipalAccessor.User.GetTgsUserId())
|
||||
{
|
||||
var newFlag = Flag(AdministrationRights.EditOwnPassword);
|
||||
if (conditional != null)
|
||||
conditional = And(conditional, newFlag);
|
||||
else
|
||||
conditional = newFlag;
|
||||
}
|
||||
|
||||
if (conditional != null)
|
||||
conditional = Or(conditional, Flag(AdministrationRights.WriteUsers));
|
||||
else if (model.Enabled.HasValue
|
||||
|| model.Group != null
|
||||
|| model.Name != null
|
||||
|| model.PermissionSet != null)
|
||||
conditional = Flag(AdministrationRights.WriteUsers);
|
||||
|
||||
return conditional;
|
||||
},
|
||||
async authorizationService =>
|
||||
{
|
||||
Logger.LogTrace("Creating new permission set...");
|
||||
originalUser.PermissionSet = new Models.PermissionSet();
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
|
||||
originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? AdministrationRights.None;
|
||||
originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? InstanceManagerRights.None;
|
||||
if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest<User>(ErrorCode.ModelValidationFailure);
|
||||
|
||||
originalUser.Group = null;
|
||||
originalUser.GroupId = null;
|
||||
}
|
||||
if (model.Group != null && model.PermissionSet != null)
|
||||
return BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
|
||||
|
||||
var fail = CheckValidName(model, false);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
var originalUser = await userQuery;
|
||||
|
||||
originalUser.Name = model.Name ?? originalUser.Name;
|
||||
if (originalUser == default)
|
||||
return NotFound<User>();
|
||||
|
||||
if (model.Enabled.HasValue)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<User>();
|
||||
|
||||
invalidateSessions = originalUser.Require(x => x.Enabled) && !model.Enabled.Value;
|
||||
originalUser.Enabled = model.Enabled.Value;
|
||||
}
|
||||
var originalUserHasSid = originalUser.SystemIdentifier != null;
|
||||
var invalidateSessions = false;
|
||||
if (originalUserHasSid && originalUser.PasswordHash != null)
|
||||
{
|
||||
// cleanup from https://github.com/tgstation/tgstation-server/issues/1528
|
||||
Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", originalUser.Id);
|
||||
originalUser.PasswordHash = null;
|
||||
|
||||
if (invalidateSessions)
|
||||
sessionInvalidationTracker.UserModifiedInvalidateSessions(originalUser);
|
||||
invalidateSessions = true;
|
||||
}
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
|
||||
return BadRequest<User>(ErrorCode.UserSidChange);
|
||||
|
||||
Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id);
|
||||
if (model.Password != null)
|
||||
{
|
||||
if (originalUserHasSid)
|
||||
return BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
|
||||
|
||||
if (invalidateSessions)
|
||||
await permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken);
|
||||
var result = TrySetPassword(originalUser, model.Password, false);
|
||||
if (result != null)
|
||||
return result;
|
||||
|
||||
await SendUserUpdatedTopics(originalUser);
|
||||
invalidateSessions = true;
|
||||
}
|
||||
|
||||
// return id only if not a self update and cannot read users
|
||||
var canReadBack = AuthenticationContext.User.Id == originalUser.Id
|
||||
|| callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers);
|
||||
return canReadBack
|
||||
? new AuthorityResponse<User>(originalUser)
|
||||
: new AuthorityResponse<User>();
|
||||
if (model.Name != null && User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
|
||||
return BadRequest<User>(ErrorCode.UserNameChange);
|
||||
|
||||
if (model.OAuthConnections != null
|
||||
&& (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count
|
||||
|| !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId))))
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
|
||||
|
||||
DatabaseContext.OAuthConnections.RemoveRange(originalUser.OAuthConnections);
|
||||
originalUser.OAuthConnections.Clear();
|
||||
|
||||
foreach (var updatedConnection in model.OAuthConnections)
|
||||
originalUser.OAuthConnections.Add(new Models.OAuthConnection
|
||||
{
|
||||
Provider = updatedConnection.Provider,
|
||||
ExternalUserId = updatedConnection.ExternalUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model.OidcConnections != null
|
||||
&& (model.OidcConnections.Count != originalUser.OidcConnections!.Count
|
||||
|| !model.OidcConnections.All(x => originalUser.OidcConnections.Any(y => y.SchemeKey == x.SchemeKey && y.ExternalUserId == x.ExternalUserId))))
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
|
||||
|
||||
DatabaseContext.OidcConnections.RemoveRange(originalUser.OidcConnections);
|
||||
originalUser.OidcConnections.Clear();
|
||||
foreach (var updatedConnection in model.OidcConnections)
|
||||
originalUser.OidcConnections.Add(new Models.OidcConnection
|
||||
{
|
||||
SchemeKey = updatedConnection.SchemeKey,
|
||||
ExternalUserId = updatedConnection.ExternalUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model.Group != null)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
originalUser.Group = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Group.Id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (originalUser.Group == default)
|
||||
return Gone<User>();
|
||||
|
||||
DatabaseContext.Groups.Attach(originalUser.Group);
|
||||
if (originalUser.PermissionSet != null)
|
||||
{
|
||||
Logger.LogInformation("Deleting permission set {permissionSetId}...", originalUser.PermissionSet.Id);
|
||||
DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet);
|
||||
originalUser.PermissionSet = null;
|
||||
}
|
||||
}
|
||||
else if (model.PermissionSet != null)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.PermissionSet == null)
|
||||
{
|
||||
Logger.LogTrace("Creating new permission set...");
|
||||
originalUser.PermissionSet = new Models.PermissionSet();
|
||||
}
|
||||
|
||||
originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? AdministrationRights.None;
|
||||
originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? InstanceManagerRights.None;
|
||||
|
||||
originalUser.Group = null;
|
||||
originalUser.GroupId = null;
|
||||
}
|
||||
|
||||
var fail = CheckValidName(model, false);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
|
||||
originalUser.Name = model.Name ?? originalUser.Name;
|
||||
|
||||
if (model.Enabled.HasValue)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
invalidateSessions = originalUser.Require(x => x.Enabled) && !model.Enabled.Value;
|
||||
originalUser.Enabled = model.Enabled.Value;
|
||||
}
|
||||
|
||||
if (invalidateSessions)
|
||||
sessionInvalidationTracker.UserModifiedInvalidateSessions(originalUser);
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id);
|
||||
|
||||
if (invalidateSessions)
|
||||
await permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken);
|
||||
|
||||
await SendUserUpdatedTopics(originalUser);
|
||||
|
||||
// return id only if not a self update and cannot read users
|
||||
var canReadBack = claimsPrincipalAccessor.User.GetTgsUserId() == originalUser.Id
|
||||
|| await authorizationService.AuthorizeAsync(
|
||||
[Flag(AdministrationRights.ReadUsers)]);
|
||||
return canReadBack
|
||||
? new AuthorityResponse<User>(originalUser)
|
||||
: new AuthorityResponse<User>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -672,10 +723,17 @@ namespace Tgstation.Server.Host.Authority
|
||||
InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None,
|
||||
};
|
||||
|
||||
var currentUser = new User
|
||||
{
|
||||
Id = claimsPrincipalAccessor.User.GetTgsUserId(),
|
||||
};
|
||||
|
||||
DatabaseContext.Users.Attach(currentUser);
|
||||
|
||||
return new User
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedBy = AuthenticationContext.User,
|
||||
CreatedBy = currentUser,
|
||||
Enabled = model.Enabled ?? false,
|
||||
PermissionSet = permissionSet,
|
||||
Group = group,
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
|
||||
using GreenDonut;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -16,6 +17,7 @@ using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
@@ -29,6 +31,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly IUserGroupsDataLoader userGroupsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the <see cref="UserGroupAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> of the <see cref="GeneralConfiguration"/>.
|
||||
/// </summary>
|
||||
@@ -59,58 +66,181 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGroupAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
/// <param name="userGroupsDataLoader">The value of <see cref="userGroupsDataLoader"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
public UserGroupAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<UserGroupAuthority> logger,
|
||||
IUserGroupsDataLoader userGroupsDataLoader,
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
logger)
|
||||
{
|
||||
this.userGroupsDataLoader = userGroupsDataLoader ?? throw new ArgumentNullException(nameof(userGroupsDataLoader));
|
||||
this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor));
|
||||
this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
if (id != claimsPrincipalAccessor.User.GetTgsUserId())
|
||||
return Flag(AdministrationRights.ReadUsers);
|
||||
|
||||
return null;
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
UserGroup? userGroup;
|
||||
if (includeJoins)
|
||||
userGroup = await QueryableImpl(true)
|
||||
.Where(x => x.Id == id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
else
|
||||
userGroup = await userGroupsDataLoader.LoadAsync(id, cancellationToken);
|
||||
|
||||
if (userGroup == null)
|
||||
return Gone<UserGroup>();
|
||||
|
||||
return new AuthorityResponse<UserGroup>(userGroup);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> Read(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => (IAuthorizationRequirement?)null,
|
||||
async () =>
|
||||
{
|
||||
var userId = claimsPrincipalAccessor.User.GetTgsUserId();
|
||||
var group = await DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(user => user.Id == userId)
|
||||
.Select(user => user.Group)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (group == null)
|
||||
return Gone<UserGroup>();
|
||||
|
||||
return new AuthorityResponse<UserGroup>(group);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<IQueryable<UserGroup>> Queryable(bool includeJoins)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.ReadUsers),
|
||||
() => ValueTask.FromResult(QueryableImpl(includeJoins)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> Create(string name, Models.PermissionSet? permissionSet, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != AuthenticationContext.User.GroupId && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid<UserGroup>();
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
return new(
|
||||
() => Flag(AdministrationRights.WriteUsers),
|
||||
async () =>
|
||||
{
|
||||
var totalGroups = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.CountAsync(cancellationToken);
|
||||
if (totalGroups >= generalConfigurationOptions.Value.UserGroupLimit)
|
||||
return Conflict<UserGroup>(ErrorCode.UserGroupLimitReached);
|
||||
|
||||
UserGroup? userGroup;
|
||||
if (includeJoins)
|
||||
userGroup = await Queryable(true)
|
||||
.Where(x => x.Id == id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
else
|
||||
userGroup = await userGroupsDataLoader.LoadAsync(id, cancellationToken);
|
||||
var modelPermissionSet = new Models.PermissionSet
|
||||
{
|
||||
AdministrationRights = permissionSet?.AdministrationRights ?? AdministrationRights.None,
|
||||
InstanceManagerRights = permissionSet?.InstanceManagerRights ?? InstanceManagerRights.None,
|
||||
};
|
||||
|
||||
if (userGroup == null)
|
||||
return Gone<UserGroup>();
|
||||
var dbGroup = new UserGroup
|
||||
{
|
||||
Name = name,
|
||||
PermissionSet = modelPermissionSet,
|
||||
};
|
||||
|
||||
return new AuthorityResponse<UserGroup>(userGroup);
|
||||
DatabaseContext.Groups.Add(dbGroup);
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
Logger.LogInformation("Created new user group {groupName} ({groupId})", dbGroup.Name, dbGroup.Id);
|
||||
|
||||
return new AuthorityResponse<UserGroup>(
|
||||
dbGroup,
|
||||
HttpSuccessResponse.Created);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<AuthorityResponse<UserGroup>> Read()
|
||||
{
|
||||
var group = AuthenticationContext.User!.Group;
|
||||
if (group == null)
|
||||
return ValueTask.FromResult(Gone<UserGroup>());
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> Update(long id, string? newName, Models.PermissionSet? newPermissionSet, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.WriteUsers),
|
||||
async () =>
|
||||
{
|
||||
var currentGroup = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return ValueTask.FromResult(new AuthorityResponse<UserGroup>(group));
|
||||
}
|
||||
if (currentGroup == default)
|
||||
return Gone<UserGroup>();
|
||||
|
||||
if (newPermissionSet != null)
|
||||
{
|
||||
currentGroup.PermissionSet!.AdministrationRights = newPermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights;
|
||||
currentGroup.PermissionSet.InstanceManagerRights = newPermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights;
|
||||
}
|
||||
|
||||
currentGroup.Name = newName ?? currentGroup.Name;
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
return new AuthorityResponse<UserGroup>(currentGroup);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<UserGroup> Queryable(bool includeJoins)
|
||||
public RequirementsGated<AuthorityResponse> DeleteEmpty(long id, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.WriteUsers),
|
||||
async () =>
|
||||
{
|
||||
var numDeleted = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id && x.Users!.Count == 0)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (numDeleted > 0)
|
||||
return new();
|
||||
|
||||
// find out how we failed
|
||||
var groupExists = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id)
|
||||
.AnyAsync(cancellationToken);
|
||||
|
||||
return new(
|
||||
groupExists
|
||||
? new ErrorMessageResponse(ErrorCode.UserGroupNotEmpty)
|
||||
: new ErrorMessageResponse(),
|
||||
groupExists
|
||||
? HttpFailureResponse.Conflict
|
||||
: HttpFailureResponse.Gone);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="IQueryable{T}"/> <see cref="UserGroup"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If <see cref="UserGroup.Users"/> and <see cref="UserGroup.PermissionSet"/> should be included.</param>
|
||||
/// <returns>An <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
|
||||
IQueryable<UserGroup> QueryableImpl(bool includeJoins)
|
||||
{
|
||||
var queryable = DatabaseContext
|
||||
.Groups
|
||||
@@ -123,92 +253,5 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
return queryable;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<UserGroup>> Create(string name, Models.PermissionSet? permissionSet, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
|
||||
var totalGroups = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.CountAsync(cancellationToken);
|
||||
if (totalGroups >= generalConfigurationOptions.Value.UserGroupLimit)
|
||||
return Conflict<UserGroup>(ErrorCode.UserGroupLimitReached);
|
||||
|
||||
var modelPermissionSet = new Models.PermissionSet
|
||||
{
|
||||
AdministrationRights = permissionSet?.AdministrationRights ?? AdministrationRights.None,
|
||||
InstanceManagerRights = permissionSet?.InstanceManagerRights ?? InstanceManagerRights.None,
|
||||
};
|
||||
|
||||
var dbGroup = new UserGroup
|
||||
{
|
||||
Name = name,
|
||||
PermissionSet = modelPermissionSet,
|
||||
};
|
||||
|
||||
DatabaseContext.Groups.Add(dbGroup);
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
Logger.LogInformation("Created new user group {groupName} ({groupId})", dbGroup.Name, dbGroup.Id);
|
||||
|
||||
return new AuthorityResponse<UserGroup>(
|
||||
dbGroup,
|
||||
HttpSuccessResponse.Created);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<UserGroup>> Update(long id, string? newName, Models.PermissionSet? newPermissionSet, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentGroup = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (currentGroup == default)
|
||||
return Gone<UserGroup>();
|
||||
|
||||
if (newPermissionSet != null)
|
||||
{
|
||||
currentGroup.PermissionSet!.AdministrationRights = newPermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights;
|
||||
currentGroup.PermissionSet.InstanceManagerRights = newPermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights;
|
||||
}
|
||||
|
||||
currentGroup.Name = newName ?? currentGroup.Name;
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
return new AuthorityResponse<UserGroup>(currentGroup);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse> DeleteEmpty(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var numDeleted = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id && x.Users!.Count == 0)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (numDeleted > 0)
|
||||
return new();
|
||||
|
||||
// find out how we failed
|
||||
var groupExists = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id)
|
||||
.AnyAsync(cancellationToken);
|
||||
|
||||
return new(
|
||||
groupExists
|
||||
? new ErrorMessageResponse(ErrorCode.UserGroupNotEmpty)
|
||||
: new ErrorMessageResponse(),
|
||||
groupExists
|
||||
? HttpFailureResponse.Conflict
|
||||
: HttpFailureResponse.Gone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <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<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
Func<TModel, ValueTask>? resultTransformer,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
@@ -272,7 +272,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <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<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
Func<TApiModel, ValueTask>? resultTransformer,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
@@ -290,14 +290,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
/// <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}"/>.</param>
|
||||
/// <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="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<ValueTask<PaginatableResult<TModel>?>> queryGenerator,
|
||||
Func<TResultModel, ValueTask>? resultTransformer,
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
@@ -318,6 +318,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var page = pageQuery ?? 1;
|
||||
|
||||
var paginationResult = await queryGenerator();
|
||||
if (paginationResult == null)
|
||||
return Forbid();
|
||||
|
||||
if (!paginationResult.Valid)
|
||||
return paginationResult.EarlyOut;
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
|
||||
return Paginated<ChatBot, ChatBotResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<ChatBot>?>(
|
||||
new PaginatableResult<ChatBot>(
|
||||
DatabaseContext
|
||||
.ChatBots
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(PaginatedResponse<CompileJobResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<CompileJob, CompileJobResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<CompileJob>?>(
|
||||
new PaginatableResult<CompileJob>(
|
||||
BaseCompileJobsQuery()
|
||||
.OrderByDescending(x => x.Job.StoppedAt))),
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> WithComponentInstance(
|
||||
instance => Paginated(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<EngineResponse>?>(
|
||||
new PaginatableResult<EngineResponse>(
|
||||
instance
|
||||
.EngineManager
|
||||
|
||||
@@ -612,7 +612,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
var needsUpdate = false;
|
||||
var result = await Paginated<Models.Instance, InstanceResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<Models.Instance>?>(
|
||||
new PaginatableResult<Models.Instance>(
|
||||
GetBaseQuery()
|
||||
.OrderBy(x => x.Id))),
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(PaginatedResponse<InstancePermissionSetResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<InstancePermissionSet, InstancePermissionSetResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<InstancePermissionSet>?>(
|
||||
new PaginatableResult<InstancePermissionSet>(
|
||||
DatabaseContext
|
||||
.Instances
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(PaginatedResponse<JobResponse>), 200)]
|
||||
public ValueTask<IActionResult> Read([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<Job, JobResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<Job>?>(
|
||||
new PaginatableResult<Job>(
|
||||
DatabaseContext
|
||||
.Jobs
|
||||
@@ -99,7 +99,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(PaginatedResponse<JobResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<Job, JobResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
() => ValueTask.FromResult<PaginatableResult<Job>?>(
|
||||
new PaginatableResult<Job>(
|
||||
DatabaseContext
|
||||
.Jobs
|
||||
|
||||
@@ -113,11 +113,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(PaginatedResponse<UserResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<User, UserResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
new PaginatableResult<User>(
|
||||
userAuthority.InvokeQueryable(
|
||||
authority => authority.Queryable(true))
|
||||
.OrderBy(x => x.Id))),
|
||||
async () =>
|
||||
{
|
||||
var queryable = await userAuthority.InvokeQueryable(
|
||||
authority => authority.Queryable(true));
|
||||
if (queryable == null)
|
||||
return null;
|
||||
|
||||
return new PaginatableResult<User>(queryable.OrderBy(x => x.Id));
|
||||
},
|
||||
null,
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@@ -146,11 +146,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(PaginatedResponse<UserGroupResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<UserGroup, UserGroupResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
new PaginatableResult<UserGroup>(
|
||||
userGroupAuthority
|
||||
.InvokeQueryable(authority => authority.Queryable(true))
|
||||
.OrderBy(x => x.Id))),
|
||||
async () =>
|
||||
{
|
||||
var queryable = await userGroupAuthority
|
||||
.InvokeQueryable(authority => authority.Queryable(true));
|
||||
if (queryable == null)
|
||||
return null;
|
||||
|
||||
return new PaginatableResult<UserGroup>(queryable.OrderBy(x => x.Id));
|
||||
},
|
||||
null,
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@@ -330,15 +330,6 @@ namespace Tgstation.Server.Host.Core
|
||||
services
|
||||
.AddScoped<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
|
||||
.AddGraphQLServer()
|
||||
.AddAuthorization(
|
||||
options =>
|
||||
{
|
||||
options.AddPolicy(
|
||||
TgsAuthorizeAttribute.PolicyName,
|
||||
builder => builder
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireRole(TgsAuthorizeAttribute.UserEnabledRole));
|
||||
})
|
||||
.ModifyOptions(options =>
|
||||
{
|
||||
options.EnsureAllNodesCanBeResolved = true;
|
||||
@@ -864,6 +855,17 @@ namespace Tgstation.Server.Host.Core
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(
|
||||
TgsAuthorizeAttribute.PolicyName,
|
||||
builder => builder
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireRole(TgsAuthorizeAttribute.UserEnabledRole));
|
||||
|
||||
options.DefaultPolicy = options.GetPolicy(TgsAuthorizeAttribute.PolicyName)!;
|
||||
});
|
||||
|
||||
var oidcConfig = securityConfiguration.OpenIDConnect;
|
||||
if (oidcConfig == null || oidcConfig.Count == 0)
|
||||
return;
|
||||
|
||||
@@ -6,10 +6,8 @@ using HotChocolate;
|
||||
using HotChocolate.Types;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.GraphQL.Scalars;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
{
|
||||
@@ -25,7 +23,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// </summary>
|
||||
/// <param name="administrationAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IAdministrationAuthority"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
[TgsGraphQLAuthorize<IAdministrationAuthority>(nameof(IAdministrationAuthority.TriggerServerRestart))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public async ValueTask<Query> RestartServerNode(
|
||||
[Service] IGraphQLAuthorityInvoker<IAdministrationAuthority> administrationAuthority)
|
||||
@@ -44,7 +41,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="administrationAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IAdministrationAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.ChangeVersion)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public async ValueTask<Query> ChangeServerNodeVersionViaTrackedRepository(
|
||||
Version targetVersion,
|
||||
@@ -65,7 +61,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="administrationAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IAdministrationAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A FileTicket that should be used to upload a zip containing the update data to the file transfer service.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.UploadVersion)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
[GraphQLType<FileUploadTicketType>]
|
||||
public async ValueTask<string> ChangeServerNodeVersionViaUpload(
|
||||
|
||||
@@ -10,7 +10,6 @@ using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
|
||||
using Tgstation.Server.Host.GraphQL.Types;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
{
|
||||
@@ -43,7 +42,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserGroupAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The created <see cref="UserGroup"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserGroupAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<UserGroup> CreateUserGroup(
|
||||
string name,
|
||||
@@ -67,7 +65,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserGroupAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The updated <see cref="UserGroup"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserGroupAuthority.Update))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<UserGroup> UpdateUserGroup(
|
||||
[ID(nameof(UserGroup))] long id,
|
||||
@@ -88,7 +85,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserGroupAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The <see cref="Query"/> root.</returns>
|
||||
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserGroupAuthority.DeleteEmpty))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public async ValueTask<Query> DeleteEmptyUserGroup(
|
||||
[ID(nameof(UserGroup))] long id,
|
||||
|
||||
@@ -9,7 +9,6 @@ using HotChocolate.Types;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
|
||||
using Tgstation.Server.Host.GraphQL.Types;
|
||||
@@ -38,7 +37,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByPasswordAndPermissionSet(
|
||||
string name,
|
||||
@@ -99,7 +97,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByPasswordAndGroup(
|
||||
string name,
|
||||
@@ -156,7 +153,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByServiceConnectionAndPermissionSet(
|
||||
string name,
|
||||
@@ -215,7 +211,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByServiceConnectionAndGroup(
|
||||
string name,
|
||||
@@ -271,7 +266,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserBySystemIDAndPermissionSet(
|
||||
string systemIdentifier,
|
||||
@@ -328,7 +322,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserBySystemIDAndGroup(
|
||||
string systemIdentifier,
|
||||
@@ -379,7 +372,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The updated current <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> SetCurrentUserPassword(
|
||||
string newPassword,
|
||||
@@ -390,7 +382,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
ArgumentNullException.ThrowIfNull(newPassword);
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
async authority => await authority.Update(
|
||||
authority => authority.Update(
|
||||
new UserUpdateRequest
|
||||
{
|
||||
Id = authenticationContext.User.Id,
|
||||
@@ -408,7 +400,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The updated current <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnServiceConnections)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> SetCurrentServiceConnections(
|
||||
IEnumerable<OAuthConnection>? newOAuthConnections,
|
||||
@@ -420,7 +411,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
ArgumentNullException.ThrowIfNull(newOAuthConnections);
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
async authority => await authority.Update(
|
||||
authority => authority.Update(
|
||||
new UserUpdateRequest
|
||||
{
|
||||
Id = authenticationContext.User.Id,
|
||||
@@ -454,7 +445,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The updated <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> UpdateUser(
|
||||
[ID(nameof(User))] long id,
|
||||
@@ -493,7 +483,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The updated <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> UpdateUserSetOwnedPermissionSet(
|
||||
[ID(nameof(User))] long id,
|
||||
@@ -533,7 +522,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <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>The updated <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> UpdateUserSetGroup(
|
||||
[ID(nameof(User))] long id,
|
||||
@@ -586,7 +574,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
async authority => await authority.Update(
|
||||
authority => authority.Update(
|
||||
new UserUpdateRequest
|
||||
{
|
||||
Id = id,
|
||||
|
||||
@@ -63,7 +63,6 @@ namespace Tgstation.Server.Host.GraphQL
|
||||
/// <param name="sessionInvalidationReason">The <see cref="SessionInvalidationReason"/> received from the publisher.</param>
|
||||
/// <returns>The <see cref="SessionInvalidationReason"/>.</returns>
|
||||
[Subscribe(With = nameof(SessionInvalidatedStream))]
|
||||
[TgsGraphQLAuthorize]
|
||||
public SessionInvalidationReason SessionInvalidated([EventMessage] SessionInvalidationReason sessionInvalidationReason)
|
||||
=> sessionInvalidationReason;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using HotChocolate.Execution;
|
||||
using HotChocolate.Types;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.GraphQL.Types;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
@@ -68,7 +67,6 @@ namespace Tgstation.Server.Host.GraphQL.Subscriptions
|
||||
/// <param name="user">The <see cref="Models.User"/> received from the publisher.</param>
|
||||
/// <returns>The updated <see cref="User"/>.</returns>
|
||||
[Subscribe(With = nameof(UserUpdatedStream))]
|
||||
[TgsGraphQLAuthorize(AdministrationRights.ReadUsers)]
|
||||
public User UserUpdated([EventMessage] User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
@@ -98,7 +96,6 @@ namespace Tgstation.Server.Host.GraphQL.Subscriptions
|
||||
/// <param name="user">The <see cref="Models.User"/> received from the publisher.</param>
|
||||
/// <returns>The updated <see cref="User"/>.</returns>
|
||||
[Subscribe(With = nameof(CurrentUserUpdatedStream))]
|
||||
[TgsGraphQLAuthorize]
|
||||
public User CurrentUserUpdated([EventMessage] User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
@@ -30,8 +30,10 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <returns>A <see cref="uint"/> specifying the minimumn valid password length for TGS users.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
|
||||
public uint MinimumPasswordLength(
|
||||
[Service] IAuthorizationService authorizationService,
|
||||
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorizationService);
|
||||
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
|
||||
return generalConfigurationOptions.Value.MinimumPasswordLength;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.GraphQL.Interfaces;
|
||||
using Tgstation.Server.Host.GraphQL.Types.OAuth;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
@@ -58,7 +57,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <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="User"/>, if present.</returns>
|
||||
[TgsGraphQLAuthorize]
|
||||
public static ValueTask<User?> GetUser(
|
||||
long id,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
|
||||
@@ -63,14 +63,14 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
[UseFiltering]
|
||||
[UseSorting]
|
||||
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserAuthority.Queryable))]
|
||||
public IQueryable<User> QueryableUsersByGroup(
|
||||
public async ValueTask<IQueryable<User>> QueryableUsersByGroup(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
var dtoQueryable = userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(
|
||||
var dtoQueryable = await userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority
|
||||
.Queryable(false)
|
||||
.Where(user => user.GroupId == Id));
|
||||
.Queryable(false),
|
||||
queryable => queryable.Where(user => user.GroupId == Id));
|
||||
return dtoQueryable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// Gets the current <see cref="User"/>.
|
||||
/// </summary>
|
||||
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserGroupAuthority"/>.</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"/>'s <see cref="UserGroup"/>.</returns>
|
||||
public ValueTask<UserGroup?> Current(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority)
|
||||
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userGroupAuthority);
|
||||
return userGroupAuthority.InvokeTransformableAllowMissing<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Read());
|
||||
return userGroupAuthority.InvokeTransformableAllowMissing<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Read(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,11 +56,12 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
[UseFiltering]
|
||||
[UseSorting]
|
||||
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserGroupAuthority.Queryable))]
|
||||
public IQueryable<UserGroup> QueryableGroups(
|
||||
public async ValueTask<IQueryable<UserGroup>> QueryableGroups(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userGroupAuthority);
|
||||
var dtoQueryable = userGroupAuthority.InvokeTransformableQueryable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Queryable(false));
|
||||
var dtoQueryable = await userGroupAuthority.InvokeTransformableQueryable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(
|
||||
authority => authority.Queryable(false));
|
||||
return dtoQueryable;
|
||||
}
|
||||
|
||||
@@ -72,15 +75,15 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
[UseFiltering]
|
||||
[UseSorting]
|
||||
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserAuthority.Queryable))]
|
||||
public IQueryable<User> QueryableUsersByGroupId(
|
||||
public async ValueTask<IQueryable<User>> QueryableUsersByGroupId(
|
||||
[ID(nameof(UserGroup))]long groupId,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
var dtoQueryable = userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(
|
||||
var dtoQueryable = await userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority
|
||||
.Queryable(false)
|
||||
.Where(user => user.GroupId == groupId));
|
||||
.Queryable(false),
|
||||
queryable => queryable.Where(user => user.GroupId == groupId));
|
||||
return dtoQueryable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,12 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
[UseFiltering]
|
||||
[UseSorting]
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Queryable))]
|
||||
public IQueryable<User> QueryableUsers(
|
||||
public async ValueTask<IQueryable<User>> QueryableUsers(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
var dtoQueryable = userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(authority => authority.Queryable(false));
|
||||
var dtoQueryable = await userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.Queryable(false));
|
||||
return dtoQueryable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AuthorizationService : IAuthorizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IHttpContextAccessor"/> for the <see cref="AuthorizationService"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.AspNetCore.Authorization.IAuthorizationService"/> for the <see cref="AuthorizationService"/>.
|
||||
/// </summary>
|
||||
readonly Microsoft.AspNetCore.Authorization.IAuthorizationService aspNetCoreAuthorizationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorizationService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
/// <param name="aspNetCoreAuthorizationService">The value of <see cref="aspNetCoreAuthorizationService"/>.</param>
|
||||
public AuthorizationService(
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
Microsoft.AspNetCore.Authorization.IAuthorizationService aspNetCoreAuthorizationService)
|
||||
{
|
||||
this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor));
|
||||
this.aspNetCoreAuthorizationService = aspNetCoreAuthorizationService ?? throw new ArgumentNullException(nameof(aspNetCoreAuthorizationService));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> AuthorizeAsync(IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement> requirements)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requirements);
|
||||
var result = await aspNetCoreAuthorizationService.AuthorizeAsync(
|
||||
claimsPrincipalAccessor.User,
|
||||
null,
|
||||
requirements);
|
||||
|
||||
return result.Succeeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ClaimsPrincipalAccessor : IClaimsPrincipalAccessor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ClaimsPrincipal User => httpContextAccessor.HttpContext?.User
|
||||
?? throw new InvalidOperationException("HTTP context was not present!");
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IHttpContextAccessor"/> for the <see cref="AuthorizationService"/>.
|
||||
/// </summary>
|
||||
readonly IHttpContextAccessor httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClaimsPrincipalAccessor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The value of <see cref="httpContextAccessor"/>.</param>
|
||||
public ClaimsPrincipalAccessor(
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for evaluating <see cref="IAuthorizationRequirement"/>s.
|
||||
/// </summary>
|
||||
public interface IAuthorizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempt to authorize the current context with a given <paramref name="requirement"/>.
|
||||
/// </summary>
|
||||
/// <param name="requirement">The <see cref="IAuthorizationRequirement"/> to authorize.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if authorization succeeded. <see langword="false"/> otherwise.</returns>
|
||||
ValueTask<bool> AuthorizeAsync(IEnumerable<IAuthorizationRequirement> requirement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for accessing the current request's <see cref="ClaimsPrincipal"/>.
|
||||
/// </summary>
|
||||
interface IClaimsPrincipalAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the current <see cref="ClaimsPrincipal"/>.
|
||||
/// </summary>
|
||||
ClaimsPrincipal User { get; }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Security.RightsEvaluation
|
||||
where TRights : Enum
|
||||
{
|
||||
/// <summary>
|
||||
/// The single bit flag of the.
|
||||
/// The single bit flag of the <typeparamref name="TRights"/>.
|
||||
/// </summary>
|
||||
readonly TRights flag;
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using HotChocolate.Authorization;
|
||||
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper for using the <see cref="AuthorizeAttribute"/> with the <see cref="Api.Rights"/> system.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1019
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
|
||||
sealed class TgsGraphQLAuthorizeAttribute : AuthorizeAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Api.Rights.RightsType"/> associated with the <see cref="TgsAuthorizeAttribute"/> if any.
|
||||
/// </summary>
|
||||
public RightsType? RightsType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
public TgsGraphQLAuthorizeAttribute()
|
||||
: this(Enumerable.Empty<string>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="AdministrationRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(AdministrationRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.Administration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="InstanceManagerRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(InstanceManagerRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.InstanceManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="RepositoryRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(RepositoryRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.Repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="EngineRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(EngineRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.Engine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="DreamMakerRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(DreamMakerRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.DreamMaker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="DreamDaemonRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(DreamDaemonRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.DreamDaemon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="ChatBotRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(ChatBotRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.ChatBots;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="ConfigurationRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(ConfigurationRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.Configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requiredRights">The <see cref="InstancePermissionSetRights"/> required.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(InstancePermissionSetRights requiredRights)
|
||||
: this(RightsHelper.RoleNames(requiredRights))
|
||||
{
|
||||
RightsType = Api.Rights.RightsType.InstancePermissionSet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="roleNames"><see cref="IEnumerable{T}"/> of role names.</param>
|
||||
private TgsGraphQLAuthorizeAttribute(IEnumerable<string> roleNames)
|
||||
{
|
||||
var listRoles = roleNames.ToList();
|
||||
if (listRoles.Count != 0)
|
||||
{
|
||||
Roles = [.. listRoles];
|
||||
}
|
||||
|
||||
Policy = TgsAuthorizeAttribute.PolicyName;
|
||||
Apply = ApplyPolicy.Validation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
using HotChocolate.Authorization;
|
||||
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Inherits the roles of <see cref="TgsAuthorizeAttribute"/>s for GraphQL endpoints.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> being wrapped.</typeparam>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class TgsGraphQLAuthorizeAttribute<TAuthority> : AuthorizeAttribute
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the method targeted.
|
||||
/// </summary>
|
||||
public string MethodName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The <typeparamref name="TAuthority"/> method name to inherit roles from.</param>
|
||||
public TgsGraphQLAuthorizeAttribute(string methodName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(methodName);
|
||||
|
||||
var authorityType = typeof(TAuthority);
|
||||
var authorityMethod = authorityType.GetMethod(methodName)
|
||||
?? throw new InvalidOperationException($"Could not find method {methodName} on {authorityType}!");
|
||||
var authorizeAttribute = authorityMethod.GetCustomAttribute<TgsAuthorizeAttribute>()
|
||||
?? throw new InvalidOperationException($"Could not find method {authorityType}.{methodName}() has no {nameof(TgsAuthorizeAttribute)}!");
|
||||
MethodName = methodName;
|
||||
Roles = authorizeAttribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
Apply = ApplyPolicy.Validation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IAuthorizationRequirement"/> for testing if a user is enabled and their session is valid.
|
||||
/// </summary>
|
||||
sealed class UserSessionValidRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user