diff --git a/bitwarden_license/src/Scim/Users/PostUserCommand.cs b/bitwarden_license/src/Scim/Users/PostUserCommand.cs index a8a9199f56..b1692bdd48 100644 --- a/bitwarden_license/src/Scim/Users/PostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PostUserCommand.cs @@ -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 successfulResponse) { - var invitedUser = await organizationUserRepository.GetDetailsByIdAsync(result.Value.Id); + var invitedUser = await organizationUserRepository.GetDetailsByIdAsync(successfulResponse.Value.InvitedUser.Id); return invitedUser; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs new file mode 100644 index 0000000000..b9ad362784 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs @@ -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> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 0979d5747d..cb6f79eaef 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -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> 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> 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> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request) { var result = await InviteOrganizationUsersAsync(InviteOrganizationUsersRequest.Create(request)); if (result is Failure> failure) { - return new Failure(failure.ErrorMessage); + return new Failure(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(result.Value.FirstOrDefault()); + return new Success(new ScimInviteOrganizationUsersResponse + { + InvitedUser = result.Value.FirstOrDefault() + }); } private async Task>> 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>([]); } @@ -93,38 +88,38 @@ public class InviteOrganizationUsersCommand(IEventService eventService, return new Failure>(invalid.ErrorMessageString); } - var valid = validationResult as Valid; + var validatedRequest = validationResult as Valid; 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>(InviteOrganizationUsersErrorMessages.FailedToInviteUsers); + return new Failure>(FailedToInviteUsers); } return new Success>(organizationUserCollection.Select(x => x.User)); @@ -132,7 +127,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, private async Task RevertPasswordManagerChangesAsync(Valid 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 valid, + private async Task PublishReferenceEventAsync(Valid 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 valid, Organization organization) + private async Task SendAdditionalEmailsAsync(Valid 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) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs new file mode 100644 index 0000000000..7fd22bac7f --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs @@ -0,0 +1,21 @@ +using Bit.Core.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; + +public class InviteOrganizationUsersResponse +{ + public IEnumerable InvitedUsers { get; set; } = []; +} + +public class ScimInviteOrganizationUsersResponse +{ + public OrganizationUser InvitedUser { get; init; } + + public static ScimInviteOrganizationUsersResponse Create(OrganizationUser invitedUser) + { + return new ScimInviteOrganizationUsersResponse + { + InvitedUser = invitedUser + }; + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index abbee309e9..df47a38f5c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -161,8 +161,8 @@ public class InviteOrganizationUserCommandTests var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); // Assert - Assert.IsType>(result); - var failure = result as Failure; + Assert.IsType>(result); + var failure = result as Failure; Assert.Equal(errorMessage, failure.ErrorMessage);