Node auth cleanup + some user group implementation

This commit is contained in:
Jordan Dominion
2024-09-14 10:53:54 -04:00
parent 0c34ff1dbc
commit a8d8ee8c04
13 changed files with 206 additions and 41 deletions
@@ -7,6 +7,7 @@ using Octokit;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority.Core
{
@@ -15,6 +16,11 @@ 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="ILogger"/> for the <see cref="AuthorityBase"/>.
/// </summary>
@@ -64,9 +70,13 @@ namespace Tgstation.Server.Host.Authority.Core
/// <summary>
/// Initializes a new instance of the <see cref="AuthorityBase"/> class.
/// </summary>
/// <param name="authenticationContext">The value of <see cref="AuthenticationContext"/>.</param>
/// <param name="logger">The value of <see cref="Logger"/>.</param>
protected AuthorityBase(ILogger<AuthorityBase> logger)
protected AuthorityBase(
IAuthenticationContext authenticationContext,
ILogger<AuthorityBase> logger)
{
AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -0,0 +1,25 @@
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="UserGroup"/>s.
/// </summary>
public interface IUserGroupAuthority : IAuthority
{
/// <summary>
/// Gets the <see cref="UserGroup"/> with a given <paramref name="id"/>.
/// </summary>
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of 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="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
[TgsAuthorize(AdministrationRights.ReadUsers)]
public ValueTask<AuthorityResponse<UserGroup>> GetId(long id, CancellationToken cancellationToken);
}
}
@@ -18,7 +18,7 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Authority
{
/// <inheritdoc />
/// <inheritdoc cref="ILoginAuthority" />
sealed class LoginAuthority : AuthorityBase, ILoginAuthority
{
/// <summary>
@@ -101,6 +101,7 @@ 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="logger">The <see cref="ILogger"/> to use.</param>
/// <param name="apiHeadersProvider">The value of <see cref="apiHeadersProvider"/>.</param>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
@@ -110,6 +111,7 @@ namespace Tgstation.Server.Host.Authority
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
/// <param name="identityCache">The value of <see cref="identityCache"/>.</param>
public LoginAuthority(
IAuthenticationContext authenticationContext,
ILogger<LoginAuthority> logger,
IApiHeadersProvider apiHeadersProvider,
ISystemIdentityFactory systemIdentityFactory,
@@ -118,7 +120,7 @@ namespace Tgstation.Server.Host.Authority
ITokenFactory tokenFactory,
ICryptographySuite cryptographySuite,
IIdentityCache identityCache)
: base(logger)
: base(authenticationContext, logger)
{
this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
@@ -9,6 +9,7 @@ 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;
@@ -16,7 +17,7 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority
{
/// <inheritdoc />
/// <inheritdoc cref="IUserAuthority" />
sealed class UserAuthority : AuthorityBase, IUserAuthority
{
/// <summary>
@@ -34,11 +35,6 @@ namespace Tgstation.Server.Host.Authority
/// </summary>
readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader;
/// <summary>
/// The <see cref="IAuthenticationContext"/> for the <see cref="UserAuthority"/>.
/// </summary>
readonly IAuthenticationContext authenticationContext;
/// <summary>
/// Implements the <see cref="usersDataLoader"/>.
/// </summary>
@@ -47,7 +43,7 @@ namespace Tgstation.Server.Host.Authority
/// <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="User"/>s.</returns>
[DataLoader]
public static async ValueTask<Dictionary<long, User>> GetUsers(
public static Task<Dictionary<long, User>> GetUsers(
IReadOnlyList<long> ids,
IDatabaseContext databaseContext,
CancellationToken cancellationToken)
@@ -55,7 +51,7 @@ namespace Tgstation.Server.Host.Authority
ArgumentNullException.ThrowIfNull(ids);
ArgumentNullException.ThrowIfNull(databaseContext);
return await databaseContext
return databaseContext
.Users
.AsQueryable()
.Where(x => ids.Contains(x.Id!.Value))
@@ -96,28 +92,30 @@ namespace Tgstation.Server.Host.Authority
/// <param name="databaseContext">The value of <see cref="databaseContext"/>.</param>
/// <param name="usersDataLoader">The value of <see cref="usersDataLoader"/>.</param>
/// <param name="oAuthConnectionsDataLoader">The value of <see cref="oAuthConnectionsDataLoader"/>.</param>
/// <param name="authenticationContext">The value of <see cref="authenticationContext"/>.</param>
/// <param name="authenticationContext">The value of <see cref="AuthenticationContext"/>.</param>
public UserAuthority(
IAuthenticationContext authenticationContext,
ILogger<UserAuthority> logger,
IDatabaseContext databaseContext,
IUsersDataLoader usersDataLoader,
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader,
IAuthenticationContext authenticationContext)
: base(logger)
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader)
: base(authenticationContext, logger)
{
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
this.usersDataLoader = usersDataLoader ?? throw new ArgumentNullException(nameof(usersDataLoader));
this.oAuthConnectionsDataLoader = oAuthConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oAuthConnectionsDataLoader));
this.authenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
}
/// <inheritdoc />
public ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken)
=> ValueTask.FromResult(new AuthorityResponse<User>(authenticationContext.User));
=> ValueTask.FromResult(new AuthorityResponse<User>(AuthenticationContext.User));
/// <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>();
User? user;
if (includeJoins)
{
@@ -0,0 +1,78 @@
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="IUserGroupAuthority" />
sealed class UserGroupAuthority : AuthorityBase, IUserGroupAuthority
{
/// <summary>
/// The <see cref="IUserGroupsDataLoader"/> for the <see cref="UserGroupAuthority"/>.
/// </summary>
readonly IUserGroupsDataLoader userGroupsDataLoader;
/// <summary>
/// Implements the <see cref="userGroupsDataLoader"/>.
/// </summary>
/// <param name="ids">The <see cref="IReadOnlyCollection{T}"/> of <see cref="UserGroup"/> <see cref="Api.Models.EntityId.Id"/>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="UserGroup"/>s.</returns>
[DataLoader]
public static Task<Dictionary<long, UserGroup>> GetUserGroups(
IReadOnlyList<long> ids,
IDatabaseContext databaseContext,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(ids);
ArgumentNullException.ThrowIfNull(databaseContext);
return databaseContext
.Groups
.Where(group => ids.Contains(group.Id!.Value))
.ToDictionaryAsync(userGroup => userGroup.Id!.Value, cancellationToken);
}
/// <summary>
/// Initializes a new instance of the <see cref="UserGroupAuthority"/> class.
/// </summary>
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
/// <param name="userGroupsDataLoader">The value of <see cref="userGroupsDataLoader"/>.</param>
public UserGroupAuthority(
IAuthenticationContext authenticationContext,
ILogger<UserGroupAuthority> logger,
IUserGroupsDataLoader userGroupsDataLoader)
: base(authenticationContext, logger)
{
this.userGroupsDataLoader = userGroupsDataLoader ?? throw new ArgumentNullException(nameof(userGroupsDataLoader));
}
/// <inheritdoc />
public async ValueTask<AuthorityResponse<UserGroup>> GetId(long id, CancellationToken cancellationToken)
{
if (id != AuthenticationContext.User.GroupId && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
return Forbid<UserGroup>();
var userGroup = await userGroupsDataLoader.LoadAsync(id, cancellationToken);
if (userGroup == null)
return NotFound<UserGroup>();
return new AuthorityResponse<UserGroup>(userGroup);
}
}
}
@@ -13,6 +13,7 @@ using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Authority;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
@@ -29,6 +30,11 @@ namespace Tgstation.Server.Host.Controllers
[Route(Routes.UserGroup)]
public class UserGroupController : ApiController
{
/// <summary>
/// The <see cref="IUserGroupAuthority"/> for the <see cref="UserGroupController"/>.
/// </summary>
readonly IRestAuthorityInvoker<IUserGroupAuthority> userGroupAuthority;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="UserGroupController"/>.
/// </summary>
@@ -39,15 +45,17 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the <see cref="ApiController"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
/// <param name="userGroupAuthority">The value of <see cref="userGroupAuthority"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
public UserGroupController(
IDatabaseContext databaseContext,
IAuthenticationContext authenticationContext,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IApiHeadersProvider apiHeaders,
ILogger<UserGroupController> logger,
IApiHeadersProvider apiHeaders)
IRestAuthorityInvoker<IUserGroupAuthority> userGroupAuthority,
IOptions<GeneralConfiguration> generalConfigurationOptions)
: base(
databaseContext,
authenticationContext,
@@ -55,6 +63,7 @@ namespace Tgstation.Server.Host.Controllers
logger,
true)
{
this.userGroupAuthority = userGroupAuthority ?? throw new ArgumentNullException(nameof(userGroupAuthority));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
@@ -155,7 +164,7 @@ namespace Tgstation.Server.Host.Controllers
/// <response code="200">Retrieve <see cref="UserGroup"/> successfully.</response>
/// <response code="410">The requested <see cref="UserGroup"/> does not currently exist.</response>
[HttpGet("{id}")]
[TgsAuthorize(AdministrationRights.ReadUsers)]
[TgsRestAuthorize<IUserGroupAuthority>(nameof(IUserGroupAuthority.GetId))]
[ProducesResponseType(typeof(UserGroupResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
public async ValueTask<IActionResult> GetId(long id, CancellationToken cancellationToken)
@@ -465,6 +465,7 @@ namespace Tgstation.Server.Host.Core
services.AddScoped(typeof(IGraphQLAuthorityInvoker<>), typeof(GraphQLAuthorityInvoker<>));
services.AddScoped<ILoginAuthority, LoginAuthority>();
services.AddScoped<IUserAuthority, UserAuthority>();
services.AddScoped<IUserGroupAuthority, UserGroupAuthority>();
// configure misc services
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
@@ -10,6 +10,7 @@ using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Swarm;
namespace Tgstation.Server.Host.GraphQL.Types
@@ -52,6 +53,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <param name="identifier">The <see cref="Identifier"/>.</param>
/// <param name="swarmService">The <see cref="ISwarmService"/> to load from.</param>
/// <returns>A new <see cref="SwarmNode"/> with the matching <paramref name="identifier"/> if found, <see langword="null"/> otherwise.</returns>
[TgsGraphQLAuthorize]
public static SwarmNode? GetSwarmNode(
string identifier,
[Service] ISwarmService swarmService)
@@ -8,6 +8,7 @@ using HotChocolate.Types.Relay;
using Tgstation.Server.Host.Authority;
using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Models.Transformers;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.GraphQL.Types
{
@@ -56,6 +57,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <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,
@@ -115,8 +117,19 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <summary>
/// The <see cref="UserGroup"/> asociated with the user, if any.
/// </summary>
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserGroupAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="UserGroup"/> associated with the <see cref="User"/>, if any.</returns>
public ValueTask<UserGroup?> Group()
=> throw new NotImplementedException();
public async ValueTask<UserGroup?> Group(
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userGroupAuthority);
if (!GroupId.HasValue)
return null;
return await userGroupAuthority.InvokeTransformable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(
authority => authority.GetId(GroupId.Value, cancellationToken));
}
}
}
@@ -1,36 +1,40 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Relay;
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 group of <see cref="User"/>s.
/// </summary>
[Node]
public sealed class UserGroup : NamedEntity
{
/// <summary>
/// The <see cref="Entity.Id"/> of the <see cref="PermissionSet"/>.
/// Node resolver for <see cref="User"/>s.
/// </summary>
readonly long permissionSetId;
/// <summary>
/// Initializes a new instance of the <see cref="UserGroup"/> class.
/// </summary>
/// <param name="id">The <see cref="Entity.Id"/>.</param>
/// <param name="name">The <see cref="NamedEntity.Name"/>.</param>
/// <param name="permissionSetId">The value of <see cref="permissionSetId"/>.</param>
[SetsRequiredMembers]
public UserGroup(
/// <param name="id">The <see cref="Entity.Id"/> to lookup.</param>
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserGroupAuthority"/>.</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<UserGroup?> GetUserGroup(
long id,
string name,
long permissionSetId)
: base(id, name)
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority,
CancellationToken cancellationToken)
{
this.permissionSetId = permissionSetId;
ArgumentNullException.ThrowIfNull(userGroupAuthority);
return userGroupAuthority.InvokeTransformable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(
authority => authority.GetId(id, cancellationToken));
}
/// <summary>
@@ -9,6 +9,7 @@ using HotChocolate.Types.Relay;
using Tgstation.Server.Host.Authority;
using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Models.Transformers;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.GraphQL.Types
{
@@ -25,6 +26,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> resulting in the queried <see cref="UserName"/>, if present.</returns>
[TgsGraphQLAuthorize]
public static async ValueTask<UserName?> GetUserName(
long id,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
@@ -0,0 +1,20 @@
namespace Tgstation.Server.Host.Models.Transformers
{
/// <summary>
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="GraphQL.Types.UserGroup"/>s.
/// </summary>
sealed class UserGroupGraphQLTransformer : TransformerBase<UserGroup, GraphQL.Types.UserGroup>
{
/// <summary>
/// Initializes a new instance of the <see cref="UserGroupGraphQLTransformer"/> class.
/// </summary>
public UserGroupGraphQLTransformer()
: base(model => new GraphQL.Types.UserGroup
{
Id = model.Id!.Value,
Name = model.Name!,
})
{
}
}
}
@@ -5,13 +5,14 @@ using System.Linq;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Models.Transformers;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// Represents a group of <see cref="User"/>s.
/// </summary>
public sealed class UserGroup : NamedEntity, ILegacyApiTransformable<UserGroupResponse>
public sealed class UserGroup : NamedEntity, ILegacyApiTransformable<UserGroupResponse>, IApiTransformable<UserGroup, GraphQL.Types.UserGroup, UserGroupGraphQLTransformer>
{
/// <summary>
/// The <see cref="Models.PermissionSet"/> the <see cref="UserGroup"/> has.