More UserGroup querying work

This commit is contained in:
Jordan Dominion
2024-09-14 11:16:53 -04:00
parent a8d8ee8c04
commit 308a08d051
7 changed files with 141 additions and 32 deletions
@@ -7,6 +7,7 @@ using Octokit;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority.Core
@@ -21,6 +22,11 @@ namespace Tgstation.Server.Host.Authority.Core
/// </summary>
protected IAuthenticationContext AuthenticationContext { get; }
/// <summary>
/// Gets the <see cref="IDatabaseContext"/> for the <see cref="AuthorityBase"/>.
/// </summary>
protected IDatabaseContext DatabaseContext { get; }
/// <summary>
/// Gets the <see cref="ILogger"/> for the <see cref="AuthorityBase"/>.
/// </summary>
@@ -71,12 +77,15 @@ namespace Tgstation.Server.Host.Authority.Core
/// 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,4 +1,5 @@
using System.Threading;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
@@ -13,6 +14,12 @@ namespace Tgstation.Server.Host.Authority
/// </summary>
public interface IUserGroupAuthority : IAuthority
{
/// <summary>
/// Gets the current <see cref="UserGroup"/>.
/// </summary>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<AuthorityResponse<UserGroup>> Read();
/// <summary>
/// Gets the <see cref="UserGroup"/> with a given <paramref name="id"/>.
/// </summary>
@@ -21,5 +28,12 @@ namespace Tgstation.Server.Host.Authority
/// <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);
/// <summary>
/// Gets all registered <see cref="UserGroup"/>s.
/// </summary>
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
[TgsAuthorize(AdministrationRights.ReadUsers)]
IQueryable<UserGroup> Queryable();
}
}
@@ -31,11 +31,6 @@ namespace Tgstation.Server.Host.Authority
/// </summary>
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="LoginAuthority"/>.
/// </summary>
readonly IDatabaseContext databaseContext;
/// <summary>
/// The <see cref="IOAuthProviders"/> for the <see cref="LoginAuthority"/>.
/// </summary>
@@ -102,29 +97,31 @@ namespace Tgstation.Server.Host.Authority
/// 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>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
/// <param name="databaseContext">The value of <see cref="databaseContext"/>.</param>
/// <param name="oAuthProviders">The value of <see cref="oAuthProviders"/>.</param>
/// <param name="tokenFactory">The value of <see cref="tokenFactory"/>.</param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
/// <param name="identityCache">The value of <see cref="identityCache"/>.</param>
public LoginAuthority(
IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger<LoginAuthority> logger,
IApiHeadersProvider apiHeadersProvider,
ISystemIdentityFactory systemIdentityFactory,
IDatabaseContext databaseContext,
IOAuthProviders oAuthProviders,
ITokenFactory tokenFactory,
ICryptographySuite cryptographySuite,
IIdentityCache identityCache)
: base(authenticationContext, logger)
: base(
authenticationContext,
databaseContext,
logger)
{
this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
@@ -158,7 +155,7 @@ namespace Tgstation.Server.Host.Authority
using (systemIdentity)
{
// Get the user from the database
IQueryable<User> query = databaseContext.Users.AsQueryable();
IQueryable<User> query = DatabaseContext.Users.AsQueryable();
if (oAuthLogin)
{
var oAuthProvider = headers.OAuthProvider!.Value;
@@ -228,9 +225,9 @@ namespace Tgstation.Server.Host.Authority
{
Id = user.Id,
};
databaseContext.Users.Attach(updatedUser);
DatabaseContext.Users.Attach(updatedUser);
updatedUser.PasswordHash = user.PasswordHash;
await databaseContext.Save(cancellationToken);
await DatabaseContext.Save(cancellationToken);
}
}
else
@@ -238,7 +235,7 @@ namespace Tgstation.Server.Host.Authority
var usernameMismatch = systemIdentity!.Username != user.Name;
if (isLikelyDbUser || usernameMismatch)
{
databaseContext.Users.Attach(user);
DatabaseContext.Users.Attach(user);
if (isLikelyDbUser)
{
// cleanup from https://github.com/tgstation/tgstation-server/issues/1528
@@ -255,7 +252,7 @@ namespace Tgstation.Server.Host.Authority
user.CanonicalName = User.CanonicalizeName(user.Name);
}
await databaseContext.Save(cancellationToken);
await DatabaseContext.Save(cancellationToken);
}
}
@@ -20,11 +20,6 @@ namespace Tgstation.Server.Host.Authority
/// <inheritdoc cref="IUserAuthority" />
sealed class UserAuthority : AuthorityBase, IUserAuthority
{
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="UserAuthority"/>.
/// </summary>
readonly IDatabaseContext databaseContext;
/// <summary>
/// The <see cref="IUsersDataLoader"/> for the <see cref="UserAuthority"/>.
/// </summary>
@@ -88,20 +83,22 @@ 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="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>
public UserAuthority(
IAuthenticationContext authenticationContext,
ILogger<UserAuthority> logger,
IDatabaseContext databaseContext,
ILogger<UserAuthority> logger,
IUsersDataLoader usersDataLoader,
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader)
: base(authenticationContext, logger)
: base(
authenticationContext,
databaseContext,
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));
}
@@ -155,7 +152,7 @@ namespace Tgstation.Server.Host.Authority
IQueryable<User> Queryable(bool includeJoins, bool allowSystemUser)
{
var tgsUserCanonicalName = User.CanonicalizeName(User.TgsSystemUserName);
var queryable = databaseContext
var queryable = DatabaseContext
.Users
.AsQueryable();
@@ -51,13 +51,18 @@ namespace Tgstation.Server.Host.Authority
/// 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="userGroupsDataLoader">The value of <see cref="userGroupsDataLoader"/>.</param>
public UserGroupAuthority(
IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger<UserGroupAuthority> logger,
IUserGroupsDataLoader userGroupsDataLoader)
: base(authenticationContext, logger)
: base(
authenticationContext,
databaseContext,
logger)
{
this.userGroupsDataLoader = userGroupsDataLoader ?? throw new ArgumentNullException(nameof(userGroupsDataLoader));
}
@@ -74,5 +79,21 @@ namespace Tgstation.Server.Host.Authority
return new AuthorityResponse<UserGroup>(userGroup);
}
/// <inheritdoc />
public ValueTask<AuthorityResponse<UserGroup>> Read()
{
var group = AuthenticationContext.User!.Group;
if (group == null)
return ValueTask.FromResult(NotFound<UserGroup>());
return ValueTask.FromResult(new AuthorityResponse<UserGroup>(group));
}
/// <inheritdoc />
public IQueryable<UserGroup> Queryable()
=> DatabaseContext
.Groups
.AsQueryable();
}
}
@@ -0,0 +1,65 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HotChocolate;
using HotChocolate.Data;
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>
/// Wrapper for accessing <see cref="UserGroup"/>s.
/// </summary>
public sealed class UserGroups
{
/// <summary>
/// Gets the current <see cref="User"/>.
/// </summary>
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserGroupAuthority"/>.</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)
{
ArgumentNullException.ThrowIfNull(userGroupAuthority);
return userGroupAuthority.InvokeTransformable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Read());
}
/// <summary>
/// Gets a <see cref="UserGroup"/> by <see cref="Entity.Id"/>.
/// </summary>
/// <param name="id">The <see cref="Entity.Id"/> of the <see cref="User"/>.</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>The <see cref="UserGroup"/> represented by <paramref name="id"/>, if any.</returns>
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserGroupAuthority.GetId))]
public ValueTask<UserGroup?> ById(
[ID(nameof(UserGroup))] long id,
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority,
CancellationToken cancellationToken)
=> UserGroup.GetUserGroup(id, userGroupAuthority, cancellationToken);
/// <summary>
/// Queries all registered <see cref="UserGroup"/>s.
/// </summary>
/// <param name="userGroupAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserGroupAuthority"/>.</param>
/// <returns>A <see cref="IQueryable{T}"/> of all registered <see cref="UserGroup"/>s.</returns>
[UsePaging]
[UseFiltering]
[UseSorting]
[TgsGraphQLAuthorize<IUserGroupAuthority>(nameof(IUserGroupAuthority.Queryable))]
public IQueryable<UserGroup>? Queryable(
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority)
{
ArgumentNullException.ThrowIfNull(userGroupAuthority);
var dtoQueryable = userGroupAuthority.InvokeTransformableQueryable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Queryable());
return dtoQueryable;
}
}
}
@@ -21,6 +21,12 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// </summary>
public sealed class Users
{
/// <summary>
/// Gets the swarm's <see cref="UserGroups"/>.
/// </summary>
/// <returns>A new <see cref="UserGroups"/>.</returns>
public UserGroups Groups() => new();
/// <summary>
/// Gets the current <see cref="User"/>.
/// </summary>
@@ -37,7 +43,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
}
/// <summary>
/// Gets a user by <see cref="Entity.Id"/>.
/// Gets a <see cref="User"/> by <see cref="Entity.Id"/>.
/// </summary>
/// <param name="id">The <see cref="Entity.Id"/> of the <see cref="User"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserAuthority"/>.</param>
@@ -45,17 +51,17 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <returns>The <see cref="User"/> represented by <paramref name="id"/>, if any.</returns>
[Error(typeof(ErrorMessageException))]
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.GetId))]
public async ValueTask<User?> ById(
public ValueTask<User?> ById(
[ID(nameof(User))] long id,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
=> await User.GetUser(id, userAuthority, cancellationToken);
=> User.GetUser(id, userAuthority, cancellationToken);
/// <summary>
/// Lists all registered <see cref="User"/>s.
/// Queries all registered <see cref="User"/>s.
/// </summary>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserAuthority"/>.</param>
/// <returns>A list of all registered <see cref="User"/>s.</returns>
/// <returns>A <see cref="IQueryable{T}"/> of all registered <see cref="User"/>s.</returns>
[UsePaging]
[UseFiltering]
[UseSorting]