Implement permission set querying and authority

This commit is contained in:
Jordan Dominion
2024-09-15 13:07:52 -04:00
parent 73b038cc4b
commit c420b63563
9 changed files with 279 additions and 27 deletions
@@ -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
{
/// <summary>
/// <see cref="IAuthority"/> for managing <see cref="PermissionSet"/>s.
/// </summary>
public interface IPermissionSetAuthority : IAuthority
{
/// <summary>
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
/// </summary>
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> to lookup.</param>
/// <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);
}
}
@@ -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
{
/// <inheritdoc cref="IPermissionSetAuthority" />
sealed class PermissionSetAuthority : AuthorityBase, IPermissionSetAuthority
{
/// <summary>
/// The <see cref="IPermissionSetsDataLoader"/> for the <see cref="PermissionSetAuthority"/>.
/// </summary>
readonly IPermissionSetsDataLoader permissionSetsDataLoader;
/// <summary>
/// Implements <see cref="permissionSetsDataLoader"/>.
/// </summary>
/// <param name="ids">The <see cref="IReadOnlyList{T}"/> of IDs and their <see cref="PermissionSetLookupType"/>s to load.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to load from.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of the requested <see cref="PermissionSet"/>s.</returns>
[DataLoader]
public static async ValueTask<Dictionary<(long Id, PermissionSetLookupType LookupType), PermissionSet>> GetPermissionSets(
IReadOnlyList<(long Id, PermissionSetLookupType LookupType)> ids,
IDatabaseContext databaseContext,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(ids);
ArgumentNullException.ThrowIfNull(databaseContext);
var idLookups = new List<long>(ids.Count);
var userIdLookups = new List<long>(ids.Count);
var groupIdLookups = new List<long>(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;
}
/// <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>
public PermissionSetAuthority(
IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger<AuthorityBase> logger,
IPermissionSetsDataLoader permissionSetsDataLoader)
: base(
authenticationContext,
databaseContext,
logger)
{
this.permissionSetsDataLoader = permissionSetsDataLoader ?? throw new ArgumentNullException(nameof(permissionSetsDataLoader));
}
/// <inheritdoc />
public async ValueTask<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 permissionSet = await permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken);
if (permissionSet == null)
return NotFound<PermissionSet>();
return new AuthorityResponse<PermissionSet>(permissionSet);
}
}
}
@@ -0,0 +1,23 @@
namespace Tgstation.Server.Host.Authority
{
/// <summary>
/// Indicates the type of <see cref="Api.Models.EntityId.Id"/> to lookup on a <see cref="Models.PermissionSet"/>.
/// </summary>
public enum PermissionSetLookupType
{
/// <summary>
/// Lookup the <see cref="Api.Models.EntityId.Id"/> of the <see cref="Models.PermissionSet"/>.
/// </summary>
Id,
/// <summary>
/// Lookup the <see cref="Api.Models.EntityId.Id"/> of the <see cref="Models.PermissionSet.User"/>.
/// </summary>
UserId,
/// <summary>
/// Lookup the <see cref="Api.Models.EntityId.Id"/> of the <see cref="Models.PermissionSet.Group"/>.
/// </summary>
GroupId,
}
}
@@ -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<ILoginAuthority, LoginAuthority>();
services.AddScoped<IUserAuthority, UserAuthority>();
services.AddScoped<IUserGroupAuthority, UserGroupAuthority>();
services.AddScoped<IPermissionSetAuthority, PermissionSetAuthority>();
// configure misc services
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
@@ -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
{
/// <summary>
/// Represents a set of permissions for the server.
/// </summary>
[Node]
public sealed class PermissionSet : Entity
{
/// <summary>
/// Node resolver for <see cref="PermissionSet"/>s.
/// </summary>
/// <param name="id">The <see cref="Entity.Id"/> to lookup.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IPermissionSetAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> resulting in the queried <see cref="PermissionSet"/>, if present.</returns>
[TgsGraphQLAuthorize]
public static ValueTask<PermissionSet?> GetPermissionSet(
long id,
[Service] IGraphQLAuthorityInvoker<IPermissionSetAuthority> userAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userAuthority);
return userAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
authority => authority.GetId(id, PermissionSetLookupType.Id, cancellationToken));
}
/// <summary>
/// The <see cref="Api.Rights.AdministrationRights"/> for the <see cref="PermissionSet"/>.
/// </summary>
public AdministrationRights AdministrationRights { get; }
public required AdministrationRights AdministrationRights { get; init; }
/// <summary>
/// The <see cref="Api.Rights.InstanceManagerRights"/> for the <see cref="PermissionSet"/>.
/// </summary>
public InstanceManagerRights InstanceManagerRights { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PermissionSet"/> class.
/// </summary>
/// <param name="id">The <see cref="Entity.Id"/>.</param>
/// <param name="administrationRights">The value of <see cref="AdministrationRights"/>.</param>
/// <param name="instanceManagerRights">The value of <see cref="InstanceManagerRights"/>.</param>
[SetsRequiredMembers]
public PermissionSet(long id, AdministrationRights administrationRights, InstanceManagerRights instanceManagerRights)
: base(id)
{
AdministrationRights = administrationRights;
InstanceManagerRights = instanceManagerRights;
}
public required InstanceManagerRights InstanceManagerRights { get; init; }
}
}
@@ -108,11 +108,49 @@ namespace Tgstation.Server.Host.GraphQL.Types
}
/// <summary>
/// The <see cref="Types.PermissionSet"/> directly associated with the <see cref="User"/>, if any.
/// The <see cref="PermissionSet"/> associated with the <see cref="User"/>.
/// </summary>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="Types.PermissionSet"/> directly associated with the <see cref="User"/>, if any.</returns>
public ValueTask<PermissionSet?> PermissionSet()
=> throw new NotImplementedException();
/// <param name="permissionSetAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IPermissionSetAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="PermissionSet"/> associated with the <see cref="User"/>.</returns>
public async ValueTask<PermissionSet> EffectivePermissionSet(
[Service] IGraphQLAuthorityInvoker<IPermissionSetAuthority> 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<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
authority => authority.GetId(lookupId, lookupType, cancellationToken)))!;
}
/// <summary>
/// The <see cref="PermissionSet"/> owned by the <see cref="User"/>, if any.
/// </summary>
/// <param name="permissionSetAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IPermissionSetAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="PermissionSet"/> owned by the <see cref="User"/>, if any.</returns>
public ValueTask<PermissionSet?> OwnedPermissionSet(
[Service] IGraphQLAuthorityInvoker<IPermissionSetAuthority> permissionSetAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(permissionSetAuthority);
return permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
authority => authority.GetId(Id, PermissionSetLookupType.UserId, cancellationToken));
}
/// <summary>
/// The <see cref="UserGroup"/> asociated with the user, if any.
@@ -38,11 +38,20 @@ namespace Tgstation.Server.Host.GraphQL.Types
}
/// <summary>
/// The <see cref="PermissionSet"/> of the <see cref="UserGroup"/>.
/// The <see cref="PermissionSet"/> owned by the <see cref="UserGroup"/>.
/// </summary>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="Types.PermissionSet"/> for the <see cref="UserGroup"/>.</returns>
public ValueTask<PermissionSet> PermissionSet()
=> throw new NotImplementedException();
/// <param name="permissionSetAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IPermissionSetAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="PermissionSet"/> owned by the <see cref="UserGroup"/>.</returns>
public async ValueTask<PermissionSet> PermissionSet(
[Service] IGraphQLAuthorityInvoker<IPermissionSetAuthority> permissionSetAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(permissionSetAuthority);
return (await permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
authority => authority.GetId(Id, PermissionSetLookupType.GroupId, cancellationToken)))!;
}
/// <summary>
/// Gets the <see cref="User"/>s in the <see cref="UserGroup"/>.
@@ -1,9 +1,11 @@
using System.Collections.Generic;
using Tgstation.Server.Host.Models.Transformers;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class PermissionSet : Api.Models.PermissionSet
public sealed class PermissionSet : Api.Models.PermissionSet, IApiTransformable<PermissionSet, GraphQL.Types.PermissionSet, PermissionSetGraphQLTransformer>
{
/// <summary>
/// The <see cref="Api.Models.EntityId.Id"/> of <see cref="User"/>.
@@ -0,0 +1,21 @@
namespace Tgstation.Server.Host.Models.Transformers
{
/// <summary>
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="GraphQL.Types.PermissionSet"/>s.
/// </summary>
sealed class PermissionSetGraphQLTransformer : TransformerBase<PermissionSet, GraphQL.Types.PermissionSet>
{
/// <summary>
/// Initializes a new instance of the <see cref="PermissionSetGraphQLTransformer"/> class.
/// </summary>
public PermissionSetGraphQLTransformer()
: base(model => new GraphQL.Types.PermissionSet
{
Id = model.Id!.Value,
AdministrationRights = model.AdministrationRights!.Value,
InstanceManagerRights = model.InstanceManagerRights!.Value,
})
{
}
}
}