diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
index 0d99ca88f2..d95d7d58a1 100644
--- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
@@ -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
/// The for the operation.
/// A resulting in a .
[TgsAuthorize]
- public ValueTask> Read(CancellationToken cancellationToken);
+ ValueTask> Read(CancellationToken cancellationToken);
///
/// Gets the with a given .
@@ -32,7 +33,7 @@ namespace Tgstation.Server.Host.Authority
/// The for the operation.
/// A resulting in a .
[TgsAuthorize(AdministrationRights.ReadUsers)]
- public ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
+ ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
///
/// Gets the s for the with a given .
@@ -40,7 +41,7 @@ namespace Tgstation.Server.Host.Authority
/// The of the .
/// The for the operation.
/// A resulting in an of .
- public ValueTask> OAuthConnections(long userId, CancellationToken cancellationToken);
+ ValueTask> OAuthConnections(long userId, CancellationToken cancellationToken);
///
/// Gets all registered s.
@@ -48,6 +49,24 @@ namespace Tgstation.Server.Host.Authority
/// If related entities should be loaded.
/// A of s.
[TgsAuthorize(AdministrationRights.ReadUsers)]
- public IQueryable Queryable(bool includeJoins);
+ IQueryable Queryable(bool includeJoins);
+
+ ///
+ /// Creates a .
+ ///
+ /// The .
+ /// The for the operation.
+ /// A resulting in am for the created .
+ [TgsAuthorize(AdministrationRights.WriteUsers)]
+ ValueTask> Create(UserCreateRequest createRequest, CancellationToken cancellationToken);
+
+ ///
+ /// Updates a .
+ ///
+ /// The .
+ /// The for the operation.
+ /// A resulting in am for the created .
+ [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)]
+ ValueTask> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs
index 041e530a7a..1e9d516228 100644
--- a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Authority
/// The for the operation.
/// A resulting in a .
[TgsAuthorize(AdministrationRights.ReadUsers)]
- public ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
+ ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
///
/// Gets all registered s.
diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
index f2a8e7646f..40b48be96d 100644
--- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
@@ -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
///
readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader;
+ ///
+ /// The for the .
+ ///
+ readonly ISystemIdentityFactory systemIdentityFactory;
+
+ ///
+ /// The for the .
+ ///
+ readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
+
+ ///
+ /// The for the .
+ ///
+ readonly ICryptographySuite cryptographySuite;
+
+ ///
+ /// The of for the .
+ ///
+ readonly IOptionsSnapshot generalConfigurationOptions;
+
///
/// Implements the .
///
- /// The of s to load.
+ /// The of s to load.
/// The to load from.
/// The for the operation.
/// A resulting in a of the requested s.
@@ -56,7 +82,7 @@ namespace Tgstation.Server.Host.Authority
///
/// Implements the .
///
- /// The of s to load the OAuthConnections for.
+ /// The of s to load the OAuthConnections for.
/// The to load from.
/// The for the operation.
/// A resulting in a of the requested s.
@@ -80,6 +106,24 @@ namespace Tgstation.Server.Host.Authority
x => new GraphQL.Types.OAuthConnection(x.ExternalUserId!, x.Provider));
}
+ ///
+ /// Check if a given has a valid specified.
+ ///
+ /// The to check.
+ /// If this is a new .
+ /// if is valid, an errored otherwise.
+ static AuthorityResponse? 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(ErrorCode.UserMissingName);
+
+ model.Name = model.Name?.Trim();
+ if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
+ return BadRequest(ErrorCode.UserColonInName);
+ return null;
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -88,12 +132,20 @@ namespace Tgstation.Server.Host.Authority
/// The to use.
/// The value of .
/// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
public UserAuthority(
IAuthenticationContext authenticationContext,
IDatabaseContext databaseContext,
ILogger logger,
IUsersDataLoader usersDataLoader,
- IOAuthConnectionsDataLoader oAuthConnectionsDataLoader)
+ IOAuthConnectionsDataLoader oAuthConnectionsDataLoader,
+ ISystemIdentityFactory systemIdentityFactory,
+ IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
+ ICryptographySuite cryptographySuite,
+ IOptionsSnapshot 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));
}
///
@@ -143,6 +199,237 @@ namespace Tgstation.Server.Host.Authority
=> new AuthorityResponse(
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
+ ///
+ public async ValueTask> Create(
+ UserCreateRequest createRequest,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(createRequest);
+
+ if (createRequest.OAuthConnections?.Any(x => x == null) == true)
+ return BadRequest(ErrorCode.ModelValidationFailure);
+
+ if ((createRequest.Password != null && createRequest.SystemIdentifier != null)
+ || (createRequest.Password == null && createRequest.SystemIdentifier == null && (createRequest.OAuthConnections?.Count > 0) != true))
+ return BadRequest(ErrorCode.UserMismatchPasswordSid);
+
+ if (createRequest.Group != null && createRequest.PermissionSet != null)
+ return BadRequest(ErrorCode.UserGroupAndPermissionSet);
+
+ createRequest.Name = createRequest.Name?.Trim();
+ if (createRequest.Name?.Length == 0)
+ createRequest.Name = null;
+
+ if (!(createRequest.Name == null ^ createRequest.SystemIdentifier == null))
+ return BadRequest(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(ErrorCode.UserLimitReached);
+
+ var dbUser = await CreateNewUserFromModel(createRequest, cancellationToken);
+ if (dbUser == null)
+ return Gone();
+
+ if (createRequest.SystemIdentifier != null)
+ try
+ {
+ using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken);
+ if (sysIdentity == null)
+ return Gone();
+ dbUser.Name = sysIdentity.Username;
+ dbUser.SystemIdentifier = sysIdentity.Uid;
+ }
+ catch (NotImplementedException ex)
+ {
+ Logger.LogTrace(ex, "System identities not implemented!");
+ return new AuthorityResponse(
+ 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(dbUser, HttpSuccessResponse.Created);
+ }
+
+ ///
+ public async ValueTask> Update(UserUpdateRequest model, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(model);
+
+ if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true)
+ return BadRequest(ErrorCode.ModelValidationFailure);
+
+ if (model.Group != null && model.PermissionSet != null)
+ return BadRequest(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 == User.CanonicalizeName(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(ErrorCode.UserSidChange);
+
+ if (model.Password != null)
+ {
+ if (originalUserHasSid)
+ return BadRequest(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(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(ErrorCode.AdminUserCannotOAuth);
+
+ if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
+ return BadRequest(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();
+
+ 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(originalUser)
+ : new AuthorityResponse();
+ }
+
///
/// Gets all registered s.
///
@@ -170,5 +457,71 @@ namespace Tgstation.Server.Host.Authority
return queryable;
}
+
+ ///
+ /// Creates a new from a given .
+ ///
+ /// The to use as a template.
+ /// The for the operation.
+ /// A resulting in a new on success, if the requested did not exist.
+ async ValueTask 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(),
+ };
+ }
+
+ ///
+ /// Attempt to change the password of a given .
+ ///
+ /// The user to update.
+ /// The new password.
+ /// If this is for a new .
+ /// on success, an errored if is too short.
+ AuthorityResponse? TrySetPassword(User dbUser, string newPassword, bool newUser)
+ {
+ newPassword ??= String.Empty;
+ if (newPassword.Length < generalConfigurationOptions.Value.MinimumPasswordLength)
+ return new AuthorityResponse(
+ new ErrorMessageResponse(ErrorCode.UserPasswordLength)
+ {
+ AdditionalData = $"Required password length: {generalConfigurationOptions.Value.MinimumPasswordLength}",
+ },
+ HttpFailureResponse.BadRequest);
+ cryptographySuite.SetUserPassword(dbUser, newPassword, newUser);
+ return null;
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 439b3e3c07..3914d69da2 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -186,17 +186,6 @@ namespace Tgstation.Server.Host.Controllers
/// A with an appropriate .
protected new NotFoundObjectResult NotFound() => NotFound(new ErrorMessageResponse(ErrorCode.ResourceNeverPresent));
- ///
- /// Generic 501 response.
- ///
- /// The that was thrown.
- /// An with .
- protected ObjectResult RequiresPosixSystemIdentity(NotImplementedException ex)
- {
- Logger.LogTrace(ex, "System identities not implemented!");
- return this.StatusCode(HttpStatusCode.NotImplemented, new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity));
- }
-
///
/// Strongly type calls to .
///
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index 2bb16eeffd..be6866f928 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -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
{
- ///
- /// The for the .
- ///
- readonly ISystemIdentityFactory systemIdentityFactory;
-
- ///
- /// The for the .
- ///
- readonly ICryptographySuite cryptographySuite;
-
- ///
- /// The for the .
- ///
- readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee;
-
///
/// The for the .
///
readonly IRestAuthorityInvoker userAuthority;
- ///
- /// The for the .
- ///
- readonly GeneralConfiguration generalConfiguration;
-
///
/// Initializes a new instance of the class.
///
/// The for the .
/// The for the .
- /// The value of .
- /// The value of .
/// The value of .
- /// The value of .
/// The for the .
- /// The containing the value of .
/// The for the .
public UserController(
IDatabaseContext databaseContext,
IAuthenticationContext authenticationContext,
- ISystemIdentityFactory systemIdentityFactory,
- ICryptographySuite cryptographySuite,
- IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
IRestAuthorityInvoker userAuthority,
ILogger logger,
- IOptions 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));
}
///
@@ -101,81 +64,15 @@ namespace Tgstation.Server.Host.Controllers
/// created successfully.
/// The requested system identifier could not be found.
[HttpPut]
- [TgsAuthorize(AdministrationRights.WriteUsers)]
+ [TgsRestAuthorize(nameof(IUserAuthority.Create))]
[ProducesResponseType(typeof(UserResponse), 201)]
-#pragma warning disable CA1502, CA1506
- public async ValueTask 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 Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken)
+ => userAuthority.InvokeTransformable(this, authority => authority.Create(model, cancellationToken));
///
/// Update a .
///
- /// The to update.
+ /// The .
/// The for the operation.
/// A resulting in the of the operation.
/// updated successfully.
@@ -183,171 +80,13 @@ namespace Tgstation.Server.Host.Controllers
/// Requested does not exist.
/// Requested does not exist.
[HttpPost]
- [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)]
+ [TgsRestAuthorize(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 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 Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken)
+ => userAuthority.InvokeTransformable(this, authority => authority.Update(model, cancellationToken));
///
/// Get information about the current .
@@ -408,87 +147,5 @@ namespace Tgstation.Server.Host.Controllers
this,
authority => authority.GetId(id, true, false, cancellationToken));
}
-
- ///
- /// Creates a new from a given .
- ///
- /// The to use as a template.
- /// The for the operation.
- /// A resulting in a new on success, if the requested did not exist.
- async ValueTask 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(),
- };
- }
-
- ///
- /// Check if a given has a valid specified.
- ///
- /// The to check.
- /// If this is a new .
- /// if is valid, a otherwise.
- 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;
- }
-
- ///
- /// Attempt to change the password of a given .
- ///
- /// The user to update.
- /// The new password.
- /// If this is for a new .
- /// on success, if is too short.
- 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;
- }
}
}
diff --git a/src/Tgstation.Server.Host/GraphQL/Mutation.cs b/src/Tgstation.Server.Host/GraphQL/Mutation.cs
index ff178262a5..0e1bba29d9 100644
--- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs
@@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.GraphQL
ArgumentNullException.ThrowIfNull(loginAuthority);
return loginAuthority.Invoke(
- authority => authority.AttemptLogin(cancellationToken))!;
+ authority => authority.AttemptLogin(cancellationToken));
}
}
}
diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs
index 1c9f0707e5..9c5baac4e0 100644
--- a/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs
@@ -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
/// The of the .
/// The password of the .
/// If the is .
+ /// The s for the user.
/// The owned of the user.
/// The .
/// The for the operation.
/// A resulting in the created .
+ [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask CreateUserByPasswordAndPermissionSet(
string name,
string password,
bool enabled,
+ IEnumerable? oAuthConnections,
PermissionSet permissionSet,
[Service] IGraphQLAuthorityInvoker 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(
+ 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));
}
///
@@ -49,14 +77,17 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
///
/// The of the .
/// If the is .
+ /// The s for the user.
/// The owned of the user.
/// The .
/// The for the operation.
/// A resulting in the created .
+ [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask CreateUserBySystemIDAndPermissionSet(
string systemIdentifier,
bool enabled,
+ IEnumerable? oAuthConnections,
PermissionSet permissionSet,
[Service] IGraphQLAuthorityInvoker 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(
+ 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));
}
///
@@ -74,15 +124,18 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// The of the .
/// The password of the .
/// If the is .
+ /// The s for the user.
/// The of the the will belong to.
/// The .
/// The for the operation.
/// A resulting in the created .
+ [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask CreateUserByPasswordAndGroup(
string name,
string password,
bool enabled,
+ IEnumerable? oAuthConnections,
[ID(nameof(UserGroup))] long groupId,
[Service] IGraphQLAuthorityInvoker 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(
+ 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));
}
///
@@ -99,14 +171,17 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
///
/// The of the .
/// If the is .
+ /// The s for the user.
/// The of the the will belong to.
/// The .
/// The for the operation.
/// A resulting in the created .
+ [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask CreateUserBySystemIDAndGroup(
string systemIdentifier,
bool enabled,
+ IEnumerable? oAuthConnections,
[ID(nameof(UserGroup))] long groupId,
[Service] IGraphQLAuthorityInvoker 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(
+ 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));
}
}
}
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs
index 87bde51eac..f47a3fb784 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs
@@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userAuthority);
- return userAuthority.InvokeTransformable(
+ return userAuthority.InvokeTransformableAllowMissing(
authority => authority.GetId(id, false, false, cancellationToken));
}
@@ -83,9 +83,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
return null;
var user = await userAuthority.InvokeTransformable(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
/// The .
/// The for the operation.
/// A resulting in a new of s for the if OAuth is configured.
- public async ValueTask OAuthConnections(
+ public ValueTask OAuthConnections(
[Service] IGraphQLAuthorityInvoker userAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userAuthority);
- return (await userAuthority.Invoke(
- authority => authority.OAuthConnections(Id, cancellationToken)))!;
+ return userAuthority.Invoke(
+ authority => authority.OAuthConnections(Id, cancellationToken));
}
///
@@ -113,7 +110,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// The .
/// The for the operation.
/// A resulting in the associated with the .
- public async ValueTask EffectivePermissionSet(
+ public ValueTask EffectivePermissionSet(
[Service] IGraphQLAuthorityInvoker permissionSetAuthority,
CancellationToken cancellationToken)
{
@@ -132,8 +129,8 @@ namespace Tgstation.Server.Host.GraphQL.Types
lookupType = PermissionSetLookupType.UserId;
}
- return (await permissionSetAuthority.InvokeTransformable(
- authority => authority.GetId(lookupId, lookupType, cancellationToken)))!;
+ return permissionSetAuthority.InvokeTransformable(
+ authority => authority.GetId(lookupId, lookupType, cancellationToken));
}
///
@@ -148,7 +145,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
{
ArgumentNullException.ThrowIfNull(permissionSetAuthority);
- return permissionSetAuthority.InvokeTransformable(
+ return permissionSetAuthority.InvokeTransformableAllowMissing(
authority => authority.GetId(Id, PermissionSetLookupType.UserId, cancellationToken));
}
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs
index 2aa5853d19..e8cd9cc855 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userGroupAuthority);
- return userGroupAuthority.InvokeTransformable(
+ return userGroupAuthority.InvokeTransformableAllowMissing(
authority => authority.GetId(id, false, cancellationToken));
}
@@ -50,8 +50,8 @@ namespace Tgstation.Server.Host.GraphQL.Types
{
ArgumentNullException.ThrowIfNull(permissionSetAuthority);
- return (await permissionSetAuthority.InvokeTransformable(
- authority => authority.GetId(Id, PermissionSetLookupType.GroupId, cancellationToken)))!;
+ return await permissionSetAuthority.InvokeTransformable(
+ authority => authority.GetId(Id, PermissionSetLookupType.GroupId, cancellationToken));
}
///
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs
index b54062e9f6..51240221c8 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
[Service] IGraphQLAuthorityInvoker userGroupAuthority)
{
ArgumentNullException.ThrowIfNull(userGroupAuthority);
- return userGroupAuthority.InvokeTransformable(authority => authority.Read());
+ return userGroupAuthority.InvokeTransformableAllowMissing(authority => authority.Read());
}
///
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs
index 9052345de8..d93f4ceec8 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userAuthority);
- return userAuthority.InvokeTransformable(authority => authority.Read(cancellationToken))!;
+ return userAuthority.InvokeTransformable(authority => authority.Read(cancellationToken));
}
///