1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 13:08:17 -05:00

Created new errors and removed references in business code to ErrorMessages property. This aligns Invite User code to use Errors instead of ErrorMessages

This commit is contained in:
jrmccannon 2025-04-01 10:12:14 -05:00
parent 8e2ac9a5bb
commit 2656ccf314
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
8 changed files with 96 additions and 23 deletions

View File

@ -4,6 +4,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Enums;
@ -78,9 +79,9 @@ public class PostUserCommand(
var invitedOrganizationUserId = result switch
{
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
Failure<ScimInviteOrganizationUsersResponse> { ErrorMessage: InviteOrganizationUsersCommand.NoUsersToInvite } => (Guid?)null,
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null,
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
Failure<ScimInviteOrganizationUsersResponse> failure when failure.ErrorMessages.Count != 0 => throw new BadRequestException(failure.ErrorMessage),
_ => throw new InvalidOperationException()
};

View File

@ -0,0 +1,6 @@
namespace Bit.Core.AdminConsole.Errors;
public record InvalidResultTypeError<T>(T Value) : Error<T>(Code, Value)
{
public const string Code = "Invalid result type.";
};

View File

@ -0,0 +1,9 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
public record FailedToInviteUsersError(InviteOrganizationUsersResponse Response) : Error<InviteOrganizationUsersResponse>(Code, Response)
{
public const string Code = "Failed to invite users";
}

View File

@ -0,0 +1,9 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
public record NoUsersToInviteError(InviteOrganizationUsersResponse Response) : Error<InviteOrganizationUsersResponse>(Code, Response)
{
public const string Code = "No users to invite";
}

View File

@ -1,13 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Commands;
@ -40,9 +41,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
{
public const string IssueNotifyingOwnersOfSeatLimitReached = "Error encountered notifying organization owners of seat limit reached.";
public const string FailedToInviteUsers = "Failed to invite user(s).";
public const string NoUsersToInvite = "No users to invite.";
public const string InvalidResultType = "Invalid result type.";
public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request)
{
@ -50,11 +48,16 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
switch (result)
{
case Failure<IEnumerable<OrganizationUser>> failure:
return new Failure<ScimInviteOrganizationUsersResponse>(failure.ErrorMessage);
case Failure<InviteOrganizationUsersResponse> failure:
return new Failure<ScimInviteOrganizationUsersResponse>(
failure.Errors.Select(error => new Error<ScimInviteOrganizationUsersResponse>(error.Message,
new ScimInviteOrganizationUsersResponse
{
InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault()
})));
case Success<IEnumerable<OrganizationUser>> success when success.Value.Any():
var user = success.Value.First();
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
var user = success.Value.InvitedUsers.First();
await eventService.LogOrganizationUserEventAsync<IOrganizationUser>(
organizationUser: user,
@ -68,17 +71,20 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
});
default:
return new Failure<ScimInviteOrganizationUsersResponse>(InvalidResultType);
return new Failure<ScimInviteOrganizationUsersResponse>(
new InvalidResultTypeError<ScimInviteOrganizationUsersResponse>(
new ScimInviteOrganizationUsersResponse()));
}
}
private async Task<CommandResult<IEnumerable<OrganizationUser>>> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request)
private async Task<CommandResult<InviteOrganizationUsersResponse>> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request)
{
var invitesToSend = (await FilterExistingUsersAsync(request)).ToArray();
if (invitesToSend.Length == 0)
{
return new Failure<IEnumerable<OrganizationUser>>(NoUsersToInvite);
return new Failure<InviteOrganizationUsersResponse>(new NoUsersToInviteError(
new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId)));
}
var validationResult = await inviteUsersValidator.ValidateAsync(new InviteUserOrganizationValidationRequest
@ -93,7 +99,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
if (validationResult is Invalid<InviteUserOrganizationValidationRequest> invalid)
{
return new Failure<IEnumerable<OrganizationUser>>(invalid.ErrorMessageString);
return invalid.MapToFailure(r => new InviteOrganizationUsersResponse(r));
}
var validatedRequest = validationResult as Valid<InviteUserOrganizationValidationRequest>;
@ -102,7 +108,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
.Select(x => x.MapToDataModel(request.PerformedAt, validatedRequest!.Value.InviteOrganization))
.ToArray();
var organization = await organizationRepository.GetByIdAsync(validatedRequest.Value.InviteOrganization.OrganizationId);
var organization = await organizationRepository.GetByIdAsync(validatedRequest!.Value.InviteOrganization.OrganizationId);
try
{
@ -120,7 +126,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
}
catch (Exception ex)
{
logger.LogError(ex, FailedToInviteUsers);
logger.LogError(ex, FailedToInviteUsersError.Code);
await organizationUserRepository.DeleteManyAsync(organizationUserToInviteEntities.Select(x => x.OrganizationUser.Id));
@ -129,10 +135,14 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
await RevertPasswordManagerChangesAsync(validatedRequest, organization);
return new Failure<IEnumerable<OrganizationUser>>(FailedToInviteUsers);
return new Failure<InviteOrganizationUsersResponse>(new FailedToInviteUsersError(
new InviteOrganizationUsersResponse(validatedRequest.Value)));
}
return new Success<IEnumerable<OrganizationUser>>(organizationUserToInviteEntities.Select(x => x.OrganizationUser));
return new Success<InviteOrganizationUsersResponse>(
new InviteOrganizationUsersResponse(
invitedOrganizationUsers: organizationUserToInviteEntities.Select(x => x.OrganizationUser).ToArray(),
organizationId: organization!.Id));
}
private async Task<IEnumerable<OrganizationUserInvite>> FilterExistingUsersAsync(InviteOrganizationUsersRequest request)

View File

@ -2,9 +2,22 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
public class InviteOrganizationUsersResponse
public class InviteOrganizationUsersResponse(Guid organizationId)
{
public IEnumerable<OrganizationUser> InvitedUsers { get; set; } = [];
public IEnumerable<OrganizationUser> InvitedUsers { get; } = [];
public Guid OrganizationId { get; } = organizationId;
public InviteOrganizationUsersResponse(InviteUserOrganizationValidationRequest validationRequest)
: this(validationRequest.InviteOrganization.OrganizationId)
{
InvitedUsers = validationRequest.Invites.Select(x => new OrganizationUser { Email = x.Email });
}
public InviteOrganizationUsersResponse(IEnumerable<OrganizationUser> invitedOrganizationUsers, Guid organizationId)
: this(organizationId)
{
InvitedUsers = invitedOrganizationUsers;
}
}
public class ScimInviteOrganizationUsersResponse

View File

@ -1,6 +1,7 @@
#nullable enable
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Shared.Validation;
namespace Bit.Core.Models.Commands;
@ -48,6 +49,11 @@ public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
{
}
public Failure(IEnumerable<Error<T>> errors) : this(errors.Select(e => e.Message))
{
Errors = errors.ToArray();
}
public Failure(Error<T> error) : this([error.Message])
{
Errors = [error];
@ -65,3 +71,18 @@ public class Partial<T> : CommandResult<T>
Failures = failedItems.ToArray();
}
}
public static class CommandResultExtensions
{
/// <summary>
/// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types.
///
/// </summary>
/// <param name="invalidResult">This is the invalid type from validating the object.</param>
/// <param name="mappingFunction">This function will map between the two types for the inner ErrorT</param>
/// <typeparam name="A">Invalid object's type</typeparam>
/// <typeparam name="B">Failure object's type</typeparam>
/// <returns></returns>
public static CommandResult<B> MapToFailure<A, B>(this Invalid<A> invalidResult, Func<A, B> mappingFunction) =>
new Failure<B>(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue))));
}

View File

@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
@ -79,7 +80,7 @@ public class InviteOrganizationUserCommandTests
// Assert
Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);
Assert.Equal(InviteOrganizationUsersCommand.NoUsersToInvite, (result as Failure<ScimInviteOrganizationUsersResponse>).ErrorMessage);
Assert.Equal(NoUsersToInviteError.Code, (result as Failure<ScimInviteOrganizationUsersResponse>).ErrorMessage);
await sutProvider.GetDependency<IPaymentService>()
.DidNotReceiveWithAnyArgs()
@ -186,6 +187,8 @@ public class InviteOrganizationUserCommandTests
performedBy: Guid.Empty,
timeProvider.GetUtcNow());
var validationRequest = GetInviteValidationRequestMock(request, inviteOrganization, organization);
sutProvider.GetDependency<IOrganizationUserRepository>()
.SelectKnownEmailsAsync(organization.Id, Arg.Any<IEnumerable<string>>(), false)
.Returns([]);
@ -196,7 +199,8 @@ public class InviteOrganizationUserCommandTests
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Invalid<InviteUserOrganizationValidationRequest>(new Error<InviteUserOrganizationValidationRequest>(errorMessage, new InviteUserOrganizationValidationRequest())));
.Returns(new Invalid<InviteUserOrganizationValidationRequest>(
new Error<InviteUserOrganizationValidationRequest>(errorMessage, validationRequest)));
// Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -496,7 +500,7 @@ public class InviteOrganizationUserCommandTests
// Assert
Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);
Assert.Equal(InviteOrganizationUsersCommand.FailedToInviteUsers, (result as Failure<ScimInviteOrganizationUsersResponse>)!.ErrorMessage);
Assert.Equal(FailedToInviteUsersError.Code, (result as Failure<ScimInviteOrganizationUsersResponse>)!.ErrorMessage);
// org user revert
await orgUserRepository.Received(1).DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(x => x.Count() == 1));