mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-23 21:16:52 +01:00
Implement GraphQL user creation, UserAuthority.Update, and make controller fully authority reliant
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Models;
|
||||
@@ -21,7 +22,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="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize]
|
||||
public ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
|
||||
@@ -32,7 +33,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="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
public ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="OAuthConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
|
||||
@@ -40,7 +41,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <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.OAuthConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
public ValueTask<AuthorityResponse<GraphQL.Types.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
|
||||
ValueTask<AuthorityResponse<GraphQL.Types.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
@@ -48,6 +49,24 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
public IQueryable<User> Queryable(bool includeJoins);
|
||||
IQueryable<User> Queryable(bool includeJoins);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="User"/>.
|
||||
/// </summary>
|
||||
/// <param name="createRequest">The <see cref="UserCreateRequest"/>.</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>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<User>> Create(UserCreateRequest createRequest, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a <see cref="User"/>.
|
||||
/// </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>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)]
|
||||
ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,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="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
public ValueTask<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
|
||||
ValueTask<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="UserGroup"/>s.
|
||||
|
||||
@@ -8,9 +8,15 @@ using GreenDonut;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
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.Core;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
@@ -30,10 +36,30 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly ISystemIdentityFactory systemIdentityFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPermissionsUpdateNotifyee"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICryptographySuite"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly ICryptographySuite cryptographySuite;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> of <see cref="GeneralConfiguration"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="usersDataLoader"/>.
|
||||
/// </summary>
|
||||
/// <param name="ids">The <see cref="IReadOnlyList{T}"/> of <see cref="User"/> <see cref="Api.Models.EntityId.Id"/>s to load.</param>
|
||||
/// <param name="ids">The <see cref="IReadOnlyList{T}"/> of <see cref="User"/> <see cref="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="User"/>s.</returns>
|
||||
@@ -56,7 +82,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Implements the <see cref="usersDataLoader"/>.
|
||||
/// </summary>
|
||||
/// <param name="userIds">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="Api.Models.EntityId.Id"/>s to load the OAuthConnections for.</param>
|
||||
/// <param name="userIds">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="EntityId.Id"/>s to load the OAuthConnections for.</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="User"/>s.</returns>
|
||||
@@ -80,6 +106,24 @@ namespace Tgstation.Server.Host.Authority
|
||||
x => new GraphQL.Types.OAuthConnection(x.ExternalUserId!, x.Provider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given <paramref name="model"/> has a valid <see cref="UserName.Name"/> specified.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="UserUpdateRequest"/> to check.</param>
|
||||
/// <param name="newUser">If this is a new <see cref="User"/>.</param>
|
||||
/// <returns><see langword="null"/> if <paramref name="model"/> is valid, an <see cref="AuthorityResponse{TResult}"/> errored otherwise.</returns>
|
||||
static AuthorityResponse<User>? CheckValidName(UserUpdateRequest model, bool newUser)
|
||||
{
|
||||
var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null;
|
||||
if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
|
||||
return BadRequest<User>(ErrorCode.UserMissingName);
|
||||
|
||||
model.Name = model.Name?.Trim();
|
||||
if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
|
||||
return BadRequest<User>(ErrorCode.UserColonInName);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserAuthority"/> class.
|
||||
/// </summary>
|
||||
@@ -88,12 +132,20 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="usersDataLoader">The value of <see cref="usersDataLoader"/>.</param>
|
||||
/// <param name="oAuthConnectionsDataLoader">The value of <see cref="oAuthConnectionsDataLoader"/>.</param>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
|
||||
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
public UserAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<UserAuthority> logger,
|
||||
IUsersDataLoader usersDataLoader,
|
||||
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader)
|
||||
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader,
|
||||
ISystemIdentityFactory systemIdentityFactory,
|
||||
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
@@ -101,6 +153,10 @@ namespace Tgstation.Server.Host.Authority
|
||||
{
|
||||
this.usersDataLoader = usersDataLoader ?? throw new ArgumentNullException(nameof(usersDataLoader));
|
||||
this.oAuthConnectionsDataLoader = oAuthConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oAuthConnectionsDataLoader));
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
|
||||
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
|
||||
this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -143,6 +199,237 @@ namespace Tgstation.Server.Host.Authority
|
||||
=> new AuthorityResponse<GraphQL.Types.OAuthConnection[]>(
|
||||
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> Create(
|
||||
UserCreateRequest createRequest,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(createRequest);
|
||||
|
||||
if (createRequest.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest<User>(ErrorCode.ModelValidationFailure);
|
||||
|
||||
if ((createRequest.Password != null && createRequest.SystemIdentifier != null)
|
||||
|| (createRequest.Password == null && createRequest.SystemIdentifier == null && (createRequest.OAuthConnections?.Count > 0) != true))
|
||||
return BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
|
||||
|
||||
if (createRequest.Group != null && createRequest.PermissionSet != null)
|
||||
return BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
|
||||
|
||||
createRequest.Name = createRequest.Name?.Trim();
|
||||
if (createRequest.Name?.Length == 0)
|
||||
createRequest.Name = null;
|
||||
|
||||
if (!(createRequest.Name == null ^ createRequest.SystemIdentifier == null))
|
||||
return BadRequest<User>(ErrorCode.UserMismatchNameSid);
|
||||
|
||||
var fail = CheckValidName(createRequest, true);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
|
||||
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
|
||||
{
|
||||
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 if (!(createRequest.Password?.Length == 0 && (createRequest.OAuthConnections?.Count > 0) == true))
|
||||
{
|
||||
var result = TrySetPassword(dbUser, createRequest.Password!, true);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
dbUser.CanonicalName = User.CanonicalizeName(dbUser.Name!);
|
||||
|
||||
DatabaseContext.Users.Add(dbUser);
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id);
|
||||
|
||||
return new AuthorityResponse<User>(dbUser, HttpSuccessResponse.Created);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
{
|
||||
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.EditOwnOAuthConnections);
|
||||
|
||||
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.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;
|
||||
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;
|
||||
originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (model.Name != null && User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
|
||||
return BadRequest<User>(ErrorCode.UserNameChange);
|
||||
|
||||
bool userWasDisabled;
|
||||
if (model.Enabled.HasValue)
|
||||
{
|
||||
userWasDisabled = originalUser.Require(x => x.Enabled) && !model.Enabled.Value;
|
||||
if (userWasDisabled)
|
||||
originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow;
|
||||
|
||||
originalUser.Enabled = model.Enabled.Value;
|
||||
}
|
||||
else
|
||||
userWasDisabled = false;
|
||||
|
||||
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 (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotOAuth);
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
|
||||
|
||||
originalUser.OAuthConnections.Clear();
|
||||
foreach (var updatedConnection in model.OAuthConnections)
|
||||
originalUser.OAuthConnections.Add(new Models.OAuthConnection
|
||||
{
|
||||
Provider = updatedConnection.Provider,
|
||||
ExternalUserId = updatedConnection.ExternalUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model.Group != null)
|
||||
{
|
||||
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 (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;
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id);
|
||||
|
||||
if (userWasDisabled)
|
||||
await permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken);
|
||||
|
||||
// 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>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
@@ -170,5 +457,71 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
return queryable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="User"/> from a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.Internal.UserApiBase"/> to use as a template.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="User"/> on success, <see langword="null"/> if the requested <see cref="UserGroup"/> did not exist.</returns>
|
||||
async ValueTask<User> CreateNewUserFromModel(Api.Models.Internal.UserApiBase model, CancellationToken cancellationToken)
|
||||
{
|
||||
Models.PermissionSet? permissionSet = null;
|
||||
UserGroup? group = null;
|
||||
if (model.Group != null)
|
||||
group = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Group.Id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
else
|
||||
permissionSet = new Models.PermissionSet
|
||||
{
|
||||
AdministrationRights = model.PermissionSet?.AdministrationRights ?? AdministrationRights.None,
|
||||
InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None,
|
||||
};
|
||||
|
||||
return new User
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedBy = AuthenticationContext.User,
|
||||
Enabled = model.Enabled ?? false,
|
||||
PermissionSet = permissionSet,
|
||||
Group = group,
|
||||
Name = model.Name,
|
||||
SystemIdentifier = model.SystemIdentifier,
|
||||
OAuthConnections = model
|
||||
.OAuthConnections
|
||||
?.Select(x => new Models.OAuthConnection
|
||||
{
|
||||
Provider = x.Provider,
|
||||
ExternalUserId = x.ExternalUserId,
|
||||
})
|
||||
.ToList()
|
||||
?? new List<Models.OAuthConnection>(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to change the password of a given <paramref name="dbUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="dbUser">The user to update.</param>
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
/// <param name="newUser">If this is for a new <see cref="UserResponse"/>.</param>
|
||||
/// <returns><see langword="null"/> on success, an errored <see cref="AuthorityResponse{TResult}"/> if <paramref name="newPassword"/> is too short.</returns>
|
||||
AuthorityResponse<User>? TrySetPassword(User dbUser, string newPassword, bool newUser)
|
||||
{
|
||||
newPassword ??= String.Empty;
|
||||
if (newPassword.Length < generalConfigurationOptions.Value.MinimumPasswordLength)
|
||||
return new AuthorityResponse<User>(
|
||||
new ErrorMessageResponse(ErrorCode.UserPasswordLength)
|
||||
{
|
||||
AdditionalData = $"Required password length: {generalConfigurationOptions.Value.MinimumPasswordLength}",
|
||||
},
|
||||
HttpFailureResponse.BadRequest);
|
||||
cryptographySuite.SetUserPassword(dbUser, newPassword, newUser);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,17 +186,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <returns>A <see cref="NotFoundObjectResult"/> with an appropriate <see cref="ErrorMessageResponse"/>.</returns>
|
||||
protected new NotFoundObjectResult NotFound() => NotFound(new ErrorMessageResponse(ErrorCode.ResourceNeverPresent));
|
||||
|
||||
/// <summary>
|
||||
/// Generic 501 response.
|
||||
/// </summary>
|
||||
/// <param name="ex">The <see cref="NotImplementedException"/> that was thrown.</param>
|
||||
/// <returns>An <see cref="ObjectResult"/> with <see cref="HttpStatusCode.NotImplemented"/>.</returns>
|
||||
protected ObjectResult RequiresPosixSystemIdentity(NotImplementedException ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "System identities not implemented!");
|
||||
return this.StatusCode(HttpStatusCode.NotImplemented, new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strongly type calls to <see cref="ControllerBase.StatusCode(int)"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
@@ -15,10 +12,8 @@ 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;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
@@ -31,52 +26,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[Route(Routes.User)]
|
||||
public sealed class UserController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="UserController"/>.
|
||||
/// </summary>
|
||||
readonly ISystemIdentityFactory systemIdentityFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICryptographySuite"/> for the <see cref="UserController"/>.
|
||||
/// </summary>
|
||||
readonly ICryptographySuite cryptographySuite;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPermissionsUpdateNotifyee"/> for the <see cref="UserController"/>.
|
||||
/// </summary>
|
||||
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IRestAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IRestAuthorityInvoker<IUserAuthority> userAuthority;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GeneralConfiguration"/> for the <see cref="UserController"/>.
|
||||
/// </summary>
|
||||
readonly GeneralConfiguration generalConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserController"/> class.
|
||||
/// </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="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
/// <param name="userAuthority">The value of <see cref="userAuthority"/>.</param>
|
||||
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
public UserController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContext authenticationContext,
|
||||
ISystemIdentityFactory systemIdentityFactory,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
|
||||
IRestAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
ILogger<UserController> logger,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IApiHeadersProvider apiHeaders)
|
||||
: base(
|
||||
databaseContext,
|
||||
@@ -85,11 +52,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
logger,
|
||||
true)
|
||||
{
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
|
||||
this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
|
||||
this.userAuthority = userAuthority ?? throw new ArgumentNullException(nameof(userAuthority));
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,81 +64,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <response code="201"><see cref="User"/> created successfully.</response>
|
||||
/// <response code="410">The requested system identifier could not be found.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
[TgsRestAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[ProducesResponseType(typeof(UserResponse), 201)]
|
||||
#pragma warning disable CA1502, CA1506
|
||||
public async ValueTask<IActionResult> Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
|
||||
if (model.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
|
||||
|
||||
if ((model.Password != null && model.SystemIdentifier != null)
|
||||
|| (model.Password == null && model.SystemIdentifier == null && (model.OAuthConnections?.Count > 0) != true))
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserMismatchPasswordSid));
|
||||
|
||||
if (model.Group != null && model.PermissionSet != null)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserGroupAndPermissionSet));
|
||||
|
||||
model.Name = model.Name?.Trim();
|
||||
if (model.Name?.Length == 0)
|
||||
model.Name = null;
|
||||
|
||||
if (!(model.Name == null ^ model.SystemIdentifier == null))
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserMismatchNameSid));
|
||||
|
||||
var fail = CheckValidName(model, true);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
|
||||
var totalUsers = await DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.CountAsync(cancellationToken);
|
||||
if (totalUsers >= generalConfiguration.UserLimit)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.UserLimitReached));
|
||||
|
||||
var dbUser = await CreateNewUserFromModel(model, cancellationToken);
|
||||
if (dbUser == null)
|
||||
return this.Gone();
|
||||
|
||||
if (model.SystemIdentifier != null)
|
||||
try
|
||||
{
|
||||
using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken);
|
||||
if (sysIdentity == null)
|
||||
return this.Gone();
|
||||
dbUser.Name = sysIdentity.Username;
|
||||
dbUser.SystemIdentifier = sysIdentity.Uid;
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
return RequiresPosixSystemIdentity(ex);
|
||||
}
|
||||
else if (!(model.Password?.Length == 0 && (model.OAuthConnections?.Count > 0) == true))
|
||||
{
|
||||
var result = TrySetPassword(dbUser, model.Password!, true);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name!);
|
||||
|
||||
DatabaseContext.Users.Add(dbUser);
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id);
|
||||
|
||||
return this.Created(dbUser.ToApi());
|
||||
}
|
||||
#pragma warning restore CA1502, CA1506
|
||||
public ValueTask<IActionResult> Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<User, UserResponse>(this, authority => authority.Create(model, cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Update a <see cref="User"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="UserResponse"/> to update.</param>
|
||||
/// <param name="model">The <see cref="UserUpdateRequest"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
|
||||
/// <response code="200"><see cref="User"/> updated successfully.</response>
|
||||
@@ -183,171 +80,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <response code="404">Requested <see cref="EntityId.Id"/> does not exist.</response>
|
||||
/// <response code="410">Requested <see cref="Api.Models.Internal.UserApiBase.Group"/> does not exist.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)]
|
||||
[TgsRestAuthorize<IUserAuthority>(nameof(IUserAuthority.Update))]
|
||||
[ProducesResponseType(typeof(UserResponse), 200)]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 404)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
#pragma warning disable CA1506
|
||||
public async ValueTask<IActionResult> Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
|
||||
if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
|
||||
|
||||
if (model.Group != null && model.PermissionSet != null)
|
||||
return BadRequest(new ErrorMessageResponse(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.EditOwnOAuthConnections);
|
||||
|
||||
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.Group!)
|
||||
.ThenInclude(x => x.PermissionSet)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (originalUser == default)
|
||||
return NotFound();
|
||||
|
||||
if (originalUser.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
|
||||
return Forbid();
|
||||
|
||||
// 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();
|
||||
|
||||
var originalUserHasSid = originalUser.SystemIdentifier != null;
|
||||
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;
|
||||
originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserSidChange));
|
||||
|
||||
if (model.Password != null)
|
||||
{
|
||||
if (originalUserHasSid)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserMismatchPasswordSid));
|
||||
|
||||
var result = TrySetPassword(originalUser, model.Password, false);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserNameChange));
|
||||
|
||||
bool userWasDisabled;
|
||||
if (model.Enabled.HasValue)
|
||||
{
|
||||
userWasDisabled = originalUser.Require(x => x.Enabled) && !model.Enabled.Value;
|
||||
if (userWasDisabled)
|
||||
originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow;
|
||||
|
||||
originalUser.Enabled = model.Enabled.Value;
|
||||
}
|
||||
else
|
||||
userWasDisabled = false;
|
||||
|
||||
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 (originalUser.CanonicalName == Models.User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.AdminUserCannotOAuth));
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.CannotRemoveLastAuthenticationOption));
|
||||
|
||||
originalUser.OAuthConnections.Clear();
|
||||
foreach (var updatedConnection in model.OAuthConnections)
|
||||
originalUser.OAuthConnections.Add(new Models.OAuthConnection
|
||||
{
|
||||
Provider = updatedConnection.Provider,
|
||||
ExternalUserId = updatedConnection.ExternalUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model.Group != null)
|
||||
{
|
||||
originalUser.Group = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Group.Id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (originalUser.Group == default)
|
||||
return this.Gone();
|
||||
|
||||
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 (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;
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id);
|
||||
|
||||
if (userWasDisabled)
|
||||
await permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken);
|
||||
|
||||
// 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
|
||||
? Json(originalUser.ToApi())
|
||||
: NoContent();
|
||||
}
|
||||
#pragma warning restore CA1506
|
||||
#pragma warning restore CA1502
|
||||
public ValueTask<IActionResult> Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<User, UserResponse>(this, authority => authority.Update(model, cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Get information about the current <see cref="User"/>.
|
||||
@@ -408,87 +147,5 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this,
|
||||
authority => authority.GetId(id, true, false, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="User"/> from a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.Internal.UserApiBase"/> to use as a template.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="User"/> on success, <see langword="null"/> if the requested <see cref="UserGroup"/> did not exist.</returns>
|
||||
async ValueTask<User> CreateNewUserFromModel(Api.Models.Internal.UserApiBase model, CancellationToken cancellationToken)
|
||||
{
|
||||
Models.PermissionSet? permissionSet = null;
|
||||
UserGroup? group = null;
|
||||
if (model.Group != null)
|
||||
group = await DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Group.Id)
|
||||
.Include(x => x.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
else
|
||||
permissionSet = new Models.PermissionSet
|
||||
{
|
||||
AdministrationRights = model.PermissionSet?.AdministrationRights ?? AdministrationRights.None,
|
||||
InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None,
|
||||
};
|
||||
|
||||
return new User
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedBy = AuthenticationContext.User,
|
||||
Enabled = model.Enabled ?? false,
|
||||
PermissionSet = permissionSet,
|
||||
Group = group,
|
||||
Name = model.Name,
|
||||
SystemIdentifier = model.SystemIdentifier,
|
||||
OAuthConnections = model
|
||||
.OAuthConnections
|
||||
?.Select(x => new Models.OAuthConnection
|
||||
{
|
||||
Provider = x.Provider,
|
||||
ExternalUserId = x.ExternalUserId,
|
||||
})
|
||||
.ToList()
|
||||
?? new List<Models.OAuthConnection>(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given <paramref name="model"/> has a valid <see cref="UserName.Name"/> specified.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="UserUpdateRequest"/> to check.</param>
|
||||
/// <param name="newUser">If this is a new <see cref="UserResponse"/>.</param>
|
||||
/// <returns><see langword="null"/> if <paramref name="model"/> is valid, a <see cref="BadRequestObjectResult"/> otherwise.</returns>
|
||||
BadRequestObjectResult? CheckValidName(UserUpdateRequest model, bool newUser)
|
||||
{
|
||||
var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null;
|
||||
if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserMissingName));
|
||||
|
||||
model.Name = model.Name?.Trim();
|
||||
if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserColonInName));
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to change the password of a given <paramref name="dbUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="dbUser">The user to update.</param>
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
/// <param name="newUser">If this is for a new <see cref="UserResponse"/>.</param>
|
||||
/// <returns><see langword="null"/> on success, <see cref="BadRequestObjectResult"/> if <paramref name="newPassword"/> is too short.</returns>
|
||||
BadRequestObjectResult? TrySetPassword(User dbUser, string newPassword, bool newUser)
|
||||
{
|
||||
newPassword ??= String.Empty;
|
||||
if (newPassword.Length < generalConfiguration.MinimumPasswordLength)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.UserPasswordLength)
|
||||
{
|
||||
AdditionalData = $"Required password length: {generalConfiguration.MinimumPasswordLength}",
|
||||
});
|
||||
cryptographySuite.SetUserPassword(dbUser, newPassword, newUser);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.GraphQL
|
||||
ArgumentNullException.ThrowIfNull(loginAuthority);
|
||||
|
||||
return loginAuthority.Invoke<LoginPayload, LoginPayload>(
|
||||
authority => authority.AttemptLogin(cancellationToken))!;
|
||||
authority => authority.AttemptLogin(cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -6,8 +8,11 @@ using HotChocolate;
|
||||
using HotChocolate.Types;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.GraphQL.Types;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
{
|
||||
@@ -23,15 +28,18 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="password">The password of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
|
||||
/// <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 created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByPasswordAndPermissionSet(
|
||||
string name,
|
||||
string password,
|
||||
bool enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
PermissionSet permissionSet,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -41,7 +49,27 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
ArgumentNullException.ThrowIfNull(permissionSet);
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
|
||||
throw new NotImplementedException();
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.Create(
|
||||
new UserCreateRequest
|
||||
{
|
||||
Name = name,
|
||||
Password = password,
|
||||
Enabled = enabled,
|
||||
PermissionSet = new Api.Models.PermissionSet
|
||||
{
|
||||
AdministrationRights = permissionSet.AdministrationRights,
|
||||
InstanceManagerRights = permissionSet.InstanceManagerRights,
|
||||
},
|
||||
OAuthConnections = oAuthConnections
|
||||
?.Select(oAuthConnection => new Api.Models.OAuthConnection
|
||||
{
|
||||
ExternalUserId = oAuthConnection.ExternalUserId,
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,14 +77,17 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// </summary>
|
||||
/// <param name="systemIdentifier">The <see cref="User.SystemIdentifier"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
|
||||
/// <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 created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserBySystemIDAndPermissionSet(
|
||||
string systemIdentifier,
|
||||
bool enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
PermissionSet permissionSet,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -65,7 +96,26 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
ArgumentNullException.ThrowIfNull(permissionSet);
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
|
||||
throw new NotImplementedException();
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.Create(
|
||||
new UserCreateRequest
|
||||
{
|
||||
SystemIdentifier = systemIdentifier,
|
||||
Enabled = enabled,
|
||||
PermissionSet = new Api.Models.PermissionSet
|
||||
{
|
||||
AdministrationRights = permissionSet.AdministrationRights,
|
||||
InstanceManagerRights = permissionSet.InstanceManagerRights,
|
||||
},
|
||||
OAuthConnections = oAuthConnections
|
||||
?.Select(oAuthConnection => new Api.Models.OAuthConnection
|
||||
{
|
||||
ExternalUserId = oAuthConnection.ExternalUserId,
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,15 +124,18 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="password">The password of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
|
||||
/// <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 created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByPasswordAndGroup(
|
||||
string name,
|
||||
string password,
|
||||
bool enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
[ID(nameof(UserGroup))] long groupId,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -91,7 +144,26 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
ArgumentException.ThrowIfNullOrEmpty(password);
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
|
||||
throw new NotImplementedException();
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.Create(
|
||||
new UserCreateRequest
|
||||
{
|
||||
Name = name,
|
||||
Password = password,
|
||||
Enabled = enabled,
|
||||
Group = new Api.Models.Internal.UserGroup
|
||||
{
|
||||
Id = groupId,
|
||||
},
|
||||
OAuthConnections = oAuthConnections
|
||||
?.Select(oAuthConnection => new Api.Models.OAuthConnection
|
||||
{
|
||||
ExternalUserId = oAuthConnection.ExternalUserId,
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -99,14 +171,17 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// </summary>
|
||||
/// <param name="systemIdentifier">The <see cref="User.SystemIdentifier"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
|
||||
/// <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 created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserBySystemIDAndGroup(
|
||||
string systemIdentifier,
|
||||
bool enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
[ID(nameof(UserGroup))] long groupId,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -114,7 +189,25 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(systemIdentifier);
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
|
||||
throw new NotImplementedException();
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.Create(
|
||||
new UserCreateRequest
|
||||
{
|
||||
SystemIdentifier = systemIdentifier,
|
||||
Enabled = enabled,
|
||||
Group = new Api.Models.Internal.UserGroup
|
||||
{
|
||||
Id = groupId,
|
||||
},
|
||||
OAuthConnections = oAuthConnections
|
||||
?.Select(oAuthConnection => new Api.Models.OAuthConnection
|
||||
{
|
||||
ExternalUserId = oAuthConnection.ExternalUserId,
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
return userAuthority.InvokeTransformableAllowMissing<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.GetId(id, false, false, cancellationToken));
|
||||
}
|
||||
|
||||
@@ -83,9 +83,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
return null;
|
||||
|
||||
var user = await userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(authority => authority.GetId(CreatedById.Value, false, true, cancellationToken));
|
||||
if (user == null)
|
||||
return null;
|
||||
|
||||
if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
|
||||
return new UserName(user);
|
||||
|
||||
@@ -98,13 +95,13 @@ 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{TResult}"/> resulting in a new <see cref="Array"/> of <see cref="OAuthConnection"/>s for the <see cref="User"/> if OAuth is configured.</returns>
|
||||
public async ValueTask<OAuthConnection[]> OAuthConnections(
|
||||
public ValueTask<OAuthConnection[]> OAuthConnections(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return (await userAuthority.Invoke<OAuthConnection[], OAuthConnection[]>(
|
||||
authority => authority.OAuthConnections(Id, cancellationToken)))!;
|
||||
return userAuthority.Invoke<OAuthConnection[], OAuthConnection[]>(
|
||||
authority => authority.OAuthConnections(Id, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,7 +110,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <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(
|
||||
public ValueTask<PermissionSet> EffectivePermissionSet(
|
||||
[Service] IGraphQLAuthorityInvoker<IPermissionSetAuthority> permissionSetAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -132,8 +129,8 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
lookupType = PermissionSetLookupType.UserId;
|
||||
}
|
||||
|
||||
return (await permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
|
||||
authority => authority.GetId(lookupId, lookupType, cancellationToken)))!;
|
||||
return permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
|
||||
authority => authority.GetId(lookupId, lookupType, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -148,7 +145,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(permissionSetAuthority);
|
||||
|
||||
return permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
|
||||
return permissionSetAuthority.InvokeTransformableAllowMissing<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
|
||||
authority => authority.GetId(Id, PermissionSetLookupType.UserId, cancellationToken));
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userGroupAuthority);
|
||||
return userGroupAuthority.InvokeTransformable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(
|
||||
return userGroupAuthority.InvokeTransformableAllowMissing<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(
|
||||
authority => authority.GetId(id, false, cancellationToken));
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(permissionSetAuthority);
|
||||
|
||||
return (await permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
|
||||
authority => authority.GetId(Id, PermissionSetLookupType.GroupId, cancellationToken)))!;
|
||||
return await permissionSetAuthority.InvokeTransformable<Models.PermissionSet, PermissionSet, PermissionSetGraphQLTransformer>(
|
||||
authority => authority.GetId(Id, PermissionSetLookupType.GroupId, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
[Service] IGraphQLAuthorityInvoker<IUserGroupAuthority> userGroupAuthority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userGroupAuthority);
|
||||
return userGroupAuthority.InvokeTransformable<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Read());
|
||||
return userGroupAuthority.InvokeTransformableAllowMissing<Models.UserGroup, UserGroup, UserGroupGraphQLTransformer>(authority => authority.Read());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(authority => authority.Read(cancellationToken))!;
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(authority => authority.Read(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user