From c420b6356301e9df97c05eee9ca61bfc0cd5b8a2 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 15 Sep 2024 13:07:52 -0400 Subject: [PATCH] Implement permission set querying and authority --- .../Authority/IPermissionSetAuthority.cs | 26 ++++ .../Authority/PermissionSetAuthority.cs | 117 ++++++++++++++++++ .../Authority/PermissionSetLookupType.cs | 23 ++++ src/Tgstation.Server.Host/Core/Application.cs | 5 +- .../GraphQL/Types/PermissionSet.cs | 47 ++++--- .../GraphQL/Types/User.cs | 46 ++++++- .../GraphQL/Types/UserGroup.cs | 17 ++- .../Models/PermissionSet.cs | 4 +- .../PermissionSetGraphQLTransformer.cs | 21 ++++ 9 files changed, 279 insertions(+), 27 deletions(-) create mode 100644 src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs create mode 100644 src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs create mode 100644 src/Tgstation.Server.Host/Authority/PermissionSetLookupType.cs create mode 100644 src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs diff --git a/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs b/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs new file mode 100644 index 0000000000..bb10fed383 --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs @@ -0,0 +1,26 @@ +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 +{ + /// + /// for managing s. + /// + public interface IPermissionSetAuthority : IAuthority + { + /// + /// Gets the with a given . + /// + /// The to lookup. + /// The of . + /// The for the operation. + /// A resulting in a . + [TgsAuthorize(AdministrationRights.ReadUsers)] + ValueTask> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs b/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs new file mode 100644 index 0000000000..12e63a9ce6 --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using GreenDonut; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Authority +{ + /// + sealed class PermissionSetAuthority : AuthorityBase, IPermissionSetAuthority + { + /// + /// The for the . + /// + readonly IPermissionSetsDataLoader permissionSetsDataLoader; + + /// + /// Implements . + /// + /// The of IDs and their s to load. + /// The to load from. + /// The for the operation. + /// A resulting in a of the requested s. + [DataLoader] + public static async ValueTask> GetPermissionSets( + IReadOnlyList<(long Id, PermissionSetLookupType LookupType)> ids, + IDatabaseContext databaseContext, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(ids); + ArgumentNullException.ThrowIfNull(databaseContext); + + var idLookups = new List(ids.Count); + var userIdLookups = new List(ids.Count); + var groupIdLookups = new List(ids.Count); + + foreach (var (id, lookupType) in ids) + switch (lookupType) + { + case PermissionSetLookupType.Id: + idLookups.Add(id); + break; + case PermissionSetLookupType.UserId: + userIdLookups.Add(id); + break; + case PermissionSetLookupType.GroupId: + groupIdLookups.Add(id); + break; + default: + throw new InvalidOperationException($"Invalid {nameof(PermissionSetLookupType)}: {lookupType}"); + } + + var selectedPermissionSets = await databaseContext + .PermissionSets + .Where(dbModel => idLookups.Contains(dbModel.Id!.Value) + || (dbModel.UserId.HasValue && userIdLookups.Contains(dbModel.UserId.Value)) + || (dbModel.GroupId.HasValue && groupIdLookups.Contains(dbModel.GroupId.Value))) + .ToListAsync(cancellationToken); + + var results = new Dictionary<(long Id, PermissionSetLookupType LookupType), PermissionSet>(selectedPermissionSets.Count * 2); + foreach (var permissionSet in selectedPermissionSets) + { + results.Add((permissionSet.Id!.Value, PermissionSetLookupType.Id), permissionSet); + if (permissionSet.GroupId.HasValue) + results.Add((permissionSet.GroupId.Value, PermissionSetLookupType.GroupId), permissionSet); + if (permissionSet.UserId.HasValue) + results.Add((permissionSet.UserId.Value, PermissionSetLookupType.UserId), permissionSet); + } + + return results; + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// The to use. + /// The to use. + /// The value of . + public PermissionSetAuthority( + IAuthenticationContext authenticationContext, + IDatabaseContext databaseContext, + ILogger logger, + IPermissionSetsDataLoader permissionSetsDataLoader) + : base( + authenticationContext, + databaseContext, + logger) + { + this.permissionSetsDataLoader = permissionSetsDataLoader ?? throw new ArgumentNullException(nameof(permissionSetsDataLoader)); + } + + /// + public async ValueTask> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken) + { + if (id != AuthenticationContext.PermissionSet.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) + return Forbid(); + + var permissionSet = await permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken); + if (permissionSet == null) + return NotFound(); + + return new AuthorityResponse(permissionSet); + } + } +} diff --git a/src/Tgstation.Server.Host/Authority/PermissionSetLookupType.cs b/src/Tgstation.Server.Host/Authority/PermissionSetLookupType.cs new file mode 100644 index 0000000000..12925dbf8f --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/PermissionSetLookupType.cs @@ -0,0 +1,23 @@ +namespace Tgstation.Server.Host.Authority +{ + /// + /// Indicates the type of to lookup on a . + /// + public enum PermissionSetLookupType + { + /// + /// Lookup the of the . + /// + Id, + + /// + /// Lookup the of the . + /// + UserId, + + /// + /// Lookup the of the . + /// + GroupId, + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 353d0063a9..0cc7d0fb2b 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; @@ -299,6 +299,8 @@ namespace Tgstation.Server.Host.Core .ModifyOptions(options => { options.EnsureAllNodesCanBeResolved = true; + options.EnableFlagEnums = true; + }) }) .AddMutationConventions() .AddGlobalObjectIdentification() @@ -466,6 +468,7 @@ namespace Tgstation.Server.Host.Core services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // configure misc services services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs index ba0c070582..018fa26f47 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs @@ -1,36 +1,49 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Threading; +using System.Threading.Tasks; + +using HotChocolate; +using HotChocolate.Types.Relay; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority; +using Tgstation.Server.Host.Models.Transformers; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { /// /// Represents a set of permissions for the server. /// + [Node] public sealed class PermissionSet : Entity { + /// + /// Node resolver for s. + /// + /// The to lookup. + /// The . + /// The for the operation. + /// A resulting in the queried , if present. + [TgsGraphQLAuthorize] + public static ValueTask GetPermissionSet( + long id, + [Service] IGraphQLAuthorityInvoker userAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(userAuthority); + return userAuthority.InvokeTransformable( + authority => authority.GetId(id, PermissionSetLookupType.Id, cancellationToken)); + } + /// /// The for the . /// - public AdministrationRights AdministrationRights { get; } + public required AdministrationRights AdministrationRights { get; init; } /// /// The for the . /// - public InstanceManagerRights InstanceManagerRights { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The value of . - /// The value of . - [SetsRequiredMembers] - public PermissionSet(long id, AdministrationRights administrationRights, InstanceManagerRights instanceManagerRights) - : base(id) - { - AdministrationRights = administrationRights; - InstanceManagerRights = instanceManagerRights; - } + public required InstanceManagerRights InstanceManagerRights { get; init; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index 0a9aa5c692..6d9f062898 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -108,11 +108,49 @@ namespace Tgstation.Server.Host.GraphQL.Types } /// - /// The directly associated with the , if any. + /// The associated with the . /// - /// A resulting in the directly associated with the , if any. - public ValueTask PermissionSet() - => throw new NotImplementedException(); + /// The . + /// The for the operation. + /// A resulting in the associated with the . + public async ValueTask EffectivePermissionSet( + [Service] IGraphQLAuthorityInvoker permissionSetAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(permissionSetAuthority); + + long lookupId; + PermissionSetLookupType lookupType; + if (GroupId.HasValue) + { + lookupId = GroupId.Value; + lookupType = PermissionSetLookupType.GroupId; + } + else + { + lookupId = Id; + lookupType = PermissionSetLookupType.UserId; + } + + return (await permissionSetAuthority.InvokeTransformable( + authority => authority.GetId(lookupId, lookupType, cancellationToken)))!; + } + + /// + /// The owned by the , if any. + /// + /// The . + /// The for the operation. + /// A resulting in the owned by the , if any. + public ValueTask OwnedPermissionSet( + [Service] IGraphQLAuthorityInvoker permissionSetAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(permissionSetAuthority); + + return permissionSetAuthority.InvokeTransformable( + authority => authority.GetId(Id, PermissionSetLookupType.UserId, cancellationToken)); + } /// /// The asociated with the user, if any. diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs index 1d457be2a8..80dec4b65c 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs @@ -38,11 +38,20 @@ namespace Tgstation.Server.Host.GraphQL.Types } /// - /// The of the . + /// The owned by the . /// - /// A resulting in the for the . - public ValueTask PermissionSet() - => throw new NotImplementedException(); + /// The . + /// The for the operation. + /// A resulting in the owned by the . + public async ValueTask PermissionSet( + [Service] IGraphQLAuthorityInvoker permissionSetAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(permissionSetAuthority); + + return (await permissionSetAuthority.InvokeTransformable( + authority => authority.GetId(Id, PermissionSetLookupType.GroupId, cancellationToken)))!; + } /// /// Gets the s in the . diff --git a/src/Tgstation.Server.Host/Models/PermissionSet.cs b/src/Tgstation.Server.Host/Models/PermissionSet.cs index 2c18a2ae1f..94b769e4a8 100644 --- a/src/Tgstation.Server.Host/Models/PermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/PermissionSet.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using Tgstation.Server.Host.Models.Transformers; + namespace Tgstation.Server.Host.Models { /// - public sealed class PermissionSet : Api.Models.PermissionSet + public sealed class PermissionSet : Api.Models.PermissionSet, IApiTransformable { /// /// The of . diff --git a/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs new file mode 100644 index 0000000000..1575f04df9 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs @@ -0,0 +1,21 @@ +namespace Tgstation.Server.Host.Models.Transformers +{ + /// + /// for s. + /// + sealed class PermissionSetGraphQLTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public PermissionSetGraphQLTransformer() + : base(model => new GraphQL.Types.PermissionSet + { + Id = model.Id!.Value, + AdministrationRights = model.AdministrationRights!.Value, + InstanceManagerRights = model.InstanceManagerRights!.Value, + }) + { + } + } +}