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

created response model and split interface out.

This commit is contained in:
jrmccannon 2025-03-05 07:42:09 -06:00
parent 5d4f58aacb
commit 4ff27fd668
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
5 changed files with 63 additions and 39 deletions

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Commands;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -65,9 +66,9 @@ public class PostUserCommand(
var result = await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(request);
if (result.Success)
if (result is Success<ScimInviteOrganizationUsersResponse> successfulResponse)
{
var invitedUser = await organizationUserRepository.GetDetailsByIdAsync(result.Value.Id);
var invitedUser = await organizationUserRepository.GetDetailsByIdAsync(successfulResponse.Value.InvitedUser.Id);
return invitedUser;
}

View File

@ -0,0 +1,9 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Models.Commands;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
public interface IInviteOrganizationUsersCommand
{
Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.Context;
@ -17,17 +18,6 @@ using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Invite
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
public static class InviteOrganizationUsersErrorMessages
{
public const string IssueNotifyingOwnersOfSeatLimitReached = "Error encountered notifying organization owners of seat limit reached.";
public const string FailedToInviteUsers = "Failed to invite user(s).";
}
public interface IInviteOrganizationUsersCommand
{
Task<CommandResult<OrganizationUser>> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request);
}
public class InviteOrganizationUsersCommand(IEventService eventService,
IOrganizationUserRepository organizationUserRepository,
IInviteUsersValidation inviteUsersValidation,
@ -42,23 +32,28 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand
) : IInviteOrganizationUsersCommand
{
public async Task<CommandResult<OrganizationUser>> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request)
public const string IssueNotifyingOwnersOfSeatLimitReached = "Error encountered notifying organization owners of seat limit reached.";
public const string FailedToInviteUsers = "Failed to invite user(s).";
public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request)
{
var result = await InviteOrganizationUsersAsync(InviteOrganizationUsersRequest.Create(request));
if (result is Failure<IEnumerable<OrganizationUser>> failure)
{
return new Failure<OrganizationUser>(failure.ErrorMessage);
return new Failure<ScimInviteOrganizationUsersResponse>(failure.ErrorMessage);
}
if (result.Value.Any())
{
(OrganizationUser User, EventType type, EventSystemUser system, DateTime performedAt) log = (result.Value.First(), EventType.OrganizationUser_Invited, EventSystemUser.SCIM, request.PerformedAt.UtcDateTime);
await eventService.LogOrganizationUserEventsAsync([log]);
await eventService.LogOrganizationUserEventAsync((IOrganizationUser)result.Value.First(), EventType.OrganizationUser_Invited, EventSystemUser.SCIM, request.PerformedAt.UtcDateTime);
}
return new Success<OrganizationUser>(result.Value.FirstOrDefault());
return new Success<ScimInviteOrganizationUsersResponse>(new ScimInviteOrganizationUsersResponse
{
InvitedUser = result.Value.FirstOrDefault()
});
}
private async Task<CommandResult<IEnumerable<OrganizationUser>>> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request)
@ -71,9 +66,9 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
.SelectMany(invite => invite.Emails
.Where(email => !existingEmails.Contains(email))
.Select(email => OrganizationUserInviteDto.Create(email, invite, request.Organization.OrganizationId))
);
).ToArray();
if (invitesToSend.Any() is false)
if (invitesToSend.Length == 0)
{
return new Success<IEnumerable<OrganizationUser>>([]);
}
@ -93,38 +88,38 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
return new Failure<IEnumerable<OrganizationUser>>(invalid.ErrorMessageString);
}
var valid = validationResult as Valid<InviteUserOrganizationValidationRequest>;
var validatedRequest = validationResult as Valid<InviteUserOrganizationValidationRequest>;
var organizationUserCollection = invitesToSend
.Select(MapToDataModel(request.PerformedAt))
.ToArray();
var organization = await organizationRepository.GetByIdAsync(valid.Value.Organization.OrganizationId);
var organization = await organizationRepository.GetByIdAsync(validatedRequest.Value.Organization.OrganizationId);
try
{
await organizationUserRepository.CreateManyAsync(organizationUserCollection);
await AdjustPasswordManagerSeatsAsync(valid, organization);
await AdjustPasswordManagerSeatsAsync(validatedRequest, organization);
await AdjustSecretsManagerSeatsAsync(valid, organization);
await AdjustSecretsManagerSeatsAsync(validatedRequest, organization);
await SendNotificationsAsync(valid, organization);
await SendAdditionalEmailsAsync(validatedRequest, organization);
await SendInvitesAsync(organizationUserCollection, organization);
await PublishEventAsync(valid, organization);
await PublishReferenceEventAsync(validatedRequest, organization);
}
catch (Exception ex)
{
logger.LogError(ex, InviteOrganizationUsersErrorMessages.FailedToInviteUsers);
logger.LogError(ex, FailedToInviteUsers);
await organizationUserRepository.DeleteManyAsync(organizationUserCollection.Select(x => x.User.Id));
await RevertSecretsManagerChangesAsync(valid, organization);
await RevertSecretsManagerChangesAsync(validatedRequest, organization);
await RevertPasswordManagerChangesAsync(valid, organization);
await RevertPasswordManagerChangesAsync(validatedRequest, organization);
return new Failure<IEnumerable<OrganizationUser>>(InviteOrganizationUsersErrorMessages.FailedToInviteUsers);
return new Failure<IEnumerable<OrganizationUser>>(FailedToInviteUsers);
}
return new Success<IEnumerable<OrganizationUser>>(organizationUserCollection.Select(x => x.User));
@ -132,7 +127,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
private async Task RevertPasswordManagerChangesAsync(Valid<InviteUserOrganizationValidationRequest> valid, Organization organization)
{
if (valid.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd < 0)
if (valid.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0)
{
await paymentService.AdjustSeatsAsync(organization, valid.Value.Organization.Plan, -valid.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd);
@ -156,7 +151,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
}
}
private async Task PublishEventAsync(Valid<InviteUserOrganizationValidationRequest> valid,
private async Task PublishReferenceEventAsync(Valid<InviteUserOrganizationValidationRequest> valid,
Organization organization) =>
await referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext)
@ -170,7 +165,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
users.Select(x => x.User),
organization));
private async Task SendNotificationsAsync(Valid<InviteUserOrganizationValidationRequest> valid, Organization organization)
private async Task SendAdditionalEmailsAsync(Valid<InviteUserOrganizationValidationRequest> valid, Organization organization)
{
await SendPasswordManagerMaxSeatLimitEmailsAsync(valid, organization);
}
@ -194,7 +189,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
}
catch (Exception ex)
{
logger.LogError(ex, InviteOrganizationUsersErrorMessages.IssueNotifyingOwnersOfSeatLimitReached);
logger.LogError(ex, IssueNotifyingOwnersOfSeatLimitReached);
}
}
@ -218,7 +213,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
return;
}
// These are the important steps
await paymentService.AdjustSeatsAsync(organization, valid.Value.Organization.Plan, valid.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd);
organization.Seats = (short?)valid.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal;
@ -226,7 +220,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
// Do we want to fail if this fails?
await referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustSeats, organization, currentContext)
{

View File

@ -0,0 +1,21 @@
using Bit.Core.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
public class InviteOrganizationUsersResponse
{
public IEnumerable<OrganizationUser> InvitedUsers { get; set; } = [];
}
public class ScimInviteOrganizationUsersResponse
{
public OrganizationUser InvitedUser { get; init; }
public static ScimInviteOrganizationUsersResponse Create(OrganizationUser invitedUser)
{
return new ScimInviteOrganizationUsersResponse
{
InvitedUser = invitedUser
};
}
}

View File

@ -161,8 +161,8 @@ public class InviteOrganizationUserCommandTests
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
// Assert
Assert.IsType<Failure<OrganizationUser>>(result);
var failure = result as Failure<OrganizationUser>;
Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);
var failure = result as Failure<ScimInviteOrganizationUsersResponse>;
Assert.Equal(errorMessage, failure.ErrorMessage);