Rework the 0-length password on create with OAuth scenario to be a little bit saner

This commit is contained in:
Jordan Dominion
2024-09-22 00:41:57 -04:00
parent 2ba4740fe9
commit 4ca97d2635
4 changed files with 184 additions and 57 deletions
@@ -55,10 +55,14 @@ namespace Tgstation.Server.Host.Authority
/// Creates a <see cref="User"/>.
/// </summary>
/// <param name="createRequest">The <see cref="UserCreateRequest"/>.</param>
/// <param name="needZeroLengthPasswordWithOAuthConnections">If a zero-length <see cref="UserUpdateRequest.Password"/> indicates and OAuth only user.</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);
ValueTask<AuthorityResponse<User>> Create(
UserCreateRequest createRequest,
bool? needZeroLengthPasswordWithOAuthConnections,
CancellationToken cancellationToken);
/// <summary>
/// Updates a <see cref="User"/>.
@@ -202,6 +202,7 @@ namespace Tgstation.Server.Host.Authority
/// <inheritdoc />
public async ValueTask<AuthorityResponse<User>> Create(
UserCreateRequest createRequest,
bool? needZeroLengthPasswordWithOAuthConnections,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(createRequest);
@@ -209,10 +210,28 @@ namespace Tgstation.Server.Host.Authority
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))
var hasNonNullPassword = createRequest.Password != null;
var hasNonNullSystemIdentifier = createRequest.SystemIdentifier != null;
var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true;
if ((hasNonNullPassword && hasNonNullSystemIdentifier)
|| (!hasNonNullPassword && !hasNonNullSystemIdentifier && !hasOAuthConnections))
return BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
var hasZeroLengthPassword = createRequest.Password?.Length == 0;
if (needZeroLengthPasswordWithOAuthConnections.HasValue)
{
if (needZeroLengthPasswordWithOAuthConnections.Value)
{
if (createRequest.OAuthConnections == null)
throw new InvalidOperationException($"Expected {nameof(UserCreateRequest.OAuthConnections)} to be set here!");
if (createRequest.OAuthConnections.Count == 0)
return BadRequest<User>(ErrorCode.ModelValidationFailure);
}
else if (hasZeroLengthPassword)
return BadRequest<User>(ErrorCode.ModelValidationFailure);
}
if (createRequest.Group != null && createRequest.PermissionSet != null)
return BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
@@ -254,11 +273,14 @@ namespace Tgstation.Server.Host.Authority
new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity),
HttpFailureResponse.NotImplemented);
}
else if (!(createRequest.Password?.Length == 0 && (createRequest.OAuthConnections?.Count > 0) == true))
else
{
var result = TrySetPassword(dbUser, createRequest.Password!, true);
if (result != null)
return result;
if (!(needZeroLengthPasswordWithOAuthConnections != false && hasZeroLengthPassword && hasOAuthConnections)) // special case allow PasswordHash to be null by setting Password to "" if OAuthConnections are set
{
var result = TrySetPassword(dbUser, createRequest.Password!, true);
if (result != null)
return result;
}
}
dbUser.CanonicalName = User.CanonicalizeName(dbUser.Name!);
@@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Controllers
[TgsRestAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
[ProducesResponseType(typeof(UserResponse), 201)]
public ValueTask<IActionResult> Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken)
=> userAuthority.InvokeTransformable<User, UserResponse>(this, authority => authority.Create(model, cancellationToken));
=> userAuthority.InvokeTransformable<User, UserResponse>(this, authority => authority.Create(model, null, cancellationToken));
/// <summary>
/// Update a <see cref="User"/>.
@@ -72,53 +72,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
})
.ToList(),
},
cancellationToken));
}
/// <summary>
/// Creates a system user specifying a personal <see cref="PermissionSet"/>.
/// </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}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>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,
PermissionSetInput permissionSet,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(systemIdentifier);
ArgumentNullException.ThrowIfNull(userAuthority);
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
authority => authority.Create(
new UserCreateRequest
{
SystemIdentifier = systemIdentifier,
Enabled = enabled,
PermissionSet = permissionSet != null
? new Api.Models.PermissionSet
{
AdministrationRights = permissionSet.AdministrationRights,
InstanceManagerRights = permissionSet.InstanceManagerRights,
}
: null,
OAuthConnections = oAuthConnections
?.Select(oAuthConnection => new Api.Models.OAuthConnection
{
ExternalUserId = oAuthConnection.ExternalUserId,
Provider = oAuthConnection.Provider,
})
.ToList(),
},
false,
cancellationToken));
}
@@ -167,6 +121,152 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
})
.ToList(),
},
false,
cancellationToken));
}
/// <summary>
/// Creates a TGS user authenticated with one or mor <see cref="OAuthConnection"/>s specifying a personal <see cref="PermissionSet"/>.
/// </summary>
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The created <see cref="User"/>.</returns>
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask<User> CreateUserByOAuthAndPermissionSet(
string name,
IEnumerable<OAuthConnection> oAuthConnections,
bool? enabled,
PermissionSetInput? permissionSet,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(oAuthConnections);
ArgumentNullException.ThrowIfNull(userAuthority);
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
authority => authority.Create(
new UserCreateRequest
{
Name = name,
Password = String.Empty,
Enabled = enabled,
PermissionSet = permissionSet != null
? new Api.Models.PermissionSet
{
AdministrationRights = permissionSet.AdministrationRights,
InstanceManagerRights = permissionSet.InstanceManagerRights,
}
: null,
OAuthConnections = oAuthConnections
.Select(oAuthConnection => new Api.Models.OAuthConnection
{
ExternalUserId = oAuthConnection.ExternalUserId,
Provider = oAuthConnection.Provider,
})
.ToList(),
},
true,
cancellationToken));
}
/// <summary>
/// Creates a TGS user specifying the <see cref="UserGroup"/> they will belong to.
/// </summary>
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</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="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The created <see cref="User"/>.</returns>
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask<User> CreateUserByOAuthAndGroup(
string name,
IEnumerable<OAuthConnection> oAuthConnections,
[ID(nameof(UserGroup))] long groupId,
bool? enabled,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(oAuthConnections);
ArgumentNullException.ThrowIfNull(userAuthority);
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
authority => authority.Create(
new UserCreateRequest
{
Name = name,
Password = String.Empty,
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(),
},
true,
cancellationToken));
}
/// <summary>
/// Creates a system user specifying a personal <see cref="PermissionSet"/>.
/// </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}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>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,
PermissionSetInput permissionSet,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(systemIdentifier);
ArgumentNullException.ThrowIfNull(userAuthority);
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
authority => authority.Create(
new UserCreateRequest
{
SystemIdentifier = systemIdentifier,
Enabled = enabled,
PermissionSet = permissionSet != null
? new Api.Models.PermissionSet
{
AdministrationRights = permissionSet.AdministrationRights,
InstanceManagerRights = permissionSet.InstanceManagerRights,
}
: null,
OAuthConnections = oAuthConnections
?.Select(oAuthConnection => new Api.Models.OAuthConnection
{
ExternalUserId = oAuthConnection.ExternalUserId,
Provider = oAuthConnection.Provider,
})
.ToList(),
},
false,
cancellationToken));
}
@@ -175,8 +275,8 @@ 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="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The created <see cref="User"/>.</returns>
@@ -185,8 +285,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
public ValueTask<User> CreateUserBySystemIDAndGroup(
string systemIdentifier,
bool? enabled,
IEnumerable<OAuthConnection>? oAuthConnections,
[ID(nameof(UserGroup))] long groupId,
IEnumerable<OAuthConnection>? oAuthConnections,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
@@ -211,6 +311,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
})
.ToList(),
},
false,
cancellationToken));
}