From 46d36b1ef8a15b7c27654fe46ffa6aaa8c4b8e86 Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Thu, 27 Mar 2025 15:38:32 -0500 Subject: [PATCH] Code Review changes. --- .../IInviteOrganizationUsersCommand.cs | 3 +- .../InviteOrganizationUsersCommand.cs | 26 ++--- ...InviteUserOrganizationValidationRequest.cs | 2 +- .../CannotAutoScaleOnSelfHostError.cs | 3 +- .../GlobalSettings/EnvironmentRequest.cs | 18 ++++ .../GlobalSettings/EnvironmentValidator.cs | 13 +++ .../InviteOrganizationUserValidator.cs | 97 +++++++------------ .../InviteUserOrganizationValidator.cs | 10 +- .../PasswordManagerInviteUserValidator.cs | 72 +++++++++++++- .../PasswordManagerSubscriptionUpdate.cs | 14 ++- ...OrganizationServiceCollectionExtensions.cs | 9 +- ...UpdateSecretsManagerSubscriptionCommand.cs | 1 + ...UpdateSecretsManagerSubscriptionCommand.cs | 2 +- ...serOrganizationValidationRequestHelpers.cs | 10 +- .../InviteOrganizationUserCommandTests.cs | 26 ++--- .../InviteUserOrganizationValidationTests.cs | 22 ++--- ...PasswordManagerInviteUserValidatorTests.cs | 22 +++-- 17 files changed, 220 insertions(+), 130 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentRequest.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentValidator.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs index e948f99a73..3e4c7652a5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs @@ -18,6 +18,5 @@ public interface IInviteOrganizationUsersCommand /// Contains the details for inviting a single organization user via email. /// /// Response from InviteScimOrganiation - Task> InviteScimOrganizationUserAsync( - InviteOrganizationUsersRequest request); + Task> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 6e1fa801d9..a9e862abda 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -112,7 +112,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await AdjustPasswordManagerSeatsAsync(validatedRequest, organization); - await AdjustSecretsManagerSeatsAsync(validatedRequest, organization); + await AdjustSecretsManagerSeatsAsync(validatedRequest); await SendAdditionalEmailsAsync(validatedRequest, organization); @@ -126,7 +126,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await organizationUserRepository.DeleteManyAsync(organizationUserToInviteEntities.Select(x => x.OrganizationUser.Id)); - await RevertSecretsManagerChangesAsync(validatedRequest, organization); + await RevertSecretsManagerChangesAsync(validatedRequest, organization, validatedRequest.Value.InviteOrganization.SmSeats); await RevertPasswordManagerChangesAsync(validatedRequest, organization); @@ -164,16 +164,19 @@ public class InviteOrganizationUsersCommand(IEventService eventService, } } - private async Task RevertSecretsManagerChangesAsync(Valid validatedResult, Organization organization) + private async Task RevertSecretsManagerChangesAsync(Valid validatedResult, Organization organization, int? initialSmSeats) { - if (validatedResult.Value.SecretsManagerSubscriptionUpdate.SeatsRequiredToAdd < 0) + if (validatedResult.Value.InviteOrganization.UseSecretsManager && validatedResult.Value.SecretsManagerSubscriptionUpdate.SmSeatsChanged) { - var updateRevert = new SecretsManagerSubscriptionUpdate(organization, validatedResult.Value.InviteOrganization.Plan, false) + var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate( + organization: organization, + plan: validatedResult.Value.InviteOrganization.Plan, + autoscaling: false) { - SmSeats = validatedResult.Value.SecretsManagerSubscriptionUpdate.Seats + SmSeats = initialSmSeats }; - await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(updateRevert); + await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert); } } @@ -234,17 +237,14 @@ public class InviteOrganizationUsersCommand(IEventService eventService, .Select(u => u.Email).Distinct(); } - private async Task AdjustSecretsManagerSeatsAsync(Valid validatedResult, Organization organization) + private async Task AdjustSecretsManagerSeatsAsync(Valid validatedResult) { - if (validatedResult.Value.SecretsManagerSubscriptionUpdate.SeatsRequiredToAdd <= 0) + if (validatedResult.Value.SecretsManagerSubscriptionUpdate?.SmSeatsChanged is not true) { return; } - var subscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, validatedResult.Value.InviteOrganization.Plan, true) - .AdjustSeats(validatedResult.Value.SecretsManagerSubscriptionUpdate.SeatsRequiredToAdd); - - await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(subscriptionUpdate); + await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(validatedResult.Value.SecretsManagerSubscriptionUpdate); } private async Task AdjustPasswordManagerSeatsAsync(Valid validatedResult, Organization organization) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs index 733dd01bf4..a47252859c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager; +using Bit.Core.Models.Business; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs index f1a4e5d6d3..0624ffe027 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs @@ -1,9 +1,8 @@ using Bit.Core.AdminConsole.Errors; -using Bit.Core.Settings; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; -public record CannotAutoScaleOnSelfHostError(IGlobalSettings InvalidSettings) : Error(Code, InvalidSettings) +public record CannotAutoScaleOnSelfHostError(EnvironmentRequest Invalid) : Error(Code, Invalid) { public const string Code = "Cannot auto scale self-host."; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentRequest.cs new file mode 100644 index 0000000000..9c1ff43d17 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentRequest.cs @@ -0,0 +1,18 @@ +#nullable enable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; + +public class EnvironmentRequest +{ + public bool IsSelfHosted { get; init; } + public PasswordManagerSubscriptionUpdate PasswordManagerSubscriptionUpdate { get; init; } + + public EnvironmentRequest(IGlobalSettings globalSettings, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate) + { + IsSelfHosted = globalSettings.SelfHosted; + PasswordManagerSubscriptionUpdate = passwordManagerSubscriptionUpdate; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentValidator.cs new file mode 100644 index 0000000000..a568481a8e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentValidator.cs @@ -0,0 +1,13 @@ +using Bit.Core.AdminConsole.Shared.Validation; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; + +public interface IEnvironmentValidator : IValidator; + +public class EnvironmentValidator : IEnvironmentValidator +{ + public async Task> ValidateAsync(EnvironmentRequest value) => + value.IsSelfHosted && value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0 ? + new Invalid(new CannotAutoScaleOnSelfHostError(value)) : + new Valid(value); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 8575e3825b..ce485d119b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -1,89 +1,64 @@ -using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Settings; -using SecretsManagerSubscriptionUpdate = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager.SecretsManagerSubscriptionUpdate; - namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; public interface IInviteUsersValidator : IValidator; public class InviteUsersValidator( - IGlobalSettings globalSettings, - IProviderRepository providerRepository, - IPaymentService paymentService, - IOrganizationRepository organizationRepository) : IInviteUsersValidator + IOrganizationRepository organizationRepository, + IPasswordManagerInviteUserValidator passwordManagerInviteUserValidator, + IUpdateSecretsManagerSubscriptionCommand secretsManagerSubscriptionCommand) : IInviteUsersValidator { public async Task> ValidateAsync(InviteUserOrganizationValidationRequest request) { var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(request); - var passwordManagerValidationResult = PasswordManagerInviteUserValidator.Validate(subscriptionUpdate); + + var passwordManagerValidationResult = await passwordManagerInviteUserValidator.ValidateAsync(subscriptionUpdate); if (passwordManagerValidationResult is Invalid invalidSubscriptionUpdate) { return invalidSubscriptionUpdate.Map(request); } - if (ValidateEnvironment(globalSettings, subscriptionUpdate) is Invalid invalidEnvironment) + if (request.InviteOrganization.UseSecretsManager && request.Invites.Any(x => x.AccessSecretsManager)) { - return invalidEnvironment.Map(request); - } - - var organizationValidationResult = InviteUserOrganizationValidator.Validate(request.InviteOrganization, subscriptionUpdate); - - if (organizationValidationResult is Invalid organizationValidation) - { - return organizationValidation.Map(request); - } - - var smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(request, subscriptionUpdate); - - var secretsManagerValidationResult = SecretsManagerInviteUserValidation.Validate(smSubscriptionUpdate); - - if (secretsManagerValidationResult is Invalid invalidSmSubscriptionUpdate) - { - return invalidSmSubscriptionUpdate.Map(request); - } - - var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId); - if (provider is not null) - { - var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider)); - - if (providerValidationResult is Invalid invalidProviderValidation) - { - return invalidProviderValidation.Map(request); - } - } - - var paymentSubscription = await paymentService.GetSubscriptionAsync( - await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId)); - - var paymentValidationResult = InviteUserPaymentValidation.Validate( - new PaymentsSubscription(paymentSubscription, request.InviteOrganization)); - - if (paymentValidationResult is Invalid invalidPaymentValidation) - { - return invalidPaymentValidation.Map(request); + return await ValidateSecretsManagerSubscriptionUpdateAsync(request, subscriptionUpdate); } return new Valid(new InviteUserOrganizationValidationRequest( request, subscriptionUpdate, - smSubscriptionUpdate)); + null)); } - public static ValidationResult ValidateEnvironment(IGlobalSettings globalSettings, PasswordManagerSubscriptionUpdate subscriptionUpdate) => - globalSettings.SelfHosted && subscriptionUpdate.SeatsRequiredToAdd > 0 - ? new Invalid(new CannotAutoScaleOnSelfHostError(globalSettings)) - : new Valid(globalSettings); + private async Task> ValidateSecretsManagerSubscriptionUpdateAsync( + InviteUserOrganizationValidationRequest request, + PasswordManagerSubscriptionUpdate subscriptionUpdate) + { + try + { + var smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate( + organization: await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId), + plan: request.InviteOrganization.Plan, + autoscaling: true) + .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager)); + + + await secretsManagerSubscriptionCommand.ValidateUpdateAsync(smSubscriptionUpdate); + + return new Valid(new InviteUserOrganizationValidationRequest( + request, + subscriptionUpdate, + smSubscriptionUpdate)); + } + catch (Exception ex) + { + return new Invalid(new Error(ex.Message, request)); + } + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUserOrganizationValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUserOrganizationValidator.cs index 1703bde9d3..730fa5bde7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUserOrganizationValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUserOrganizationValidator.cs @@ -1,15 +1,15 @@ using Bit.Core.AdminConsole.Models.Business; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Shared.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; -public static class InviteUserOrganizationValidator +public interface IInviteUserOrganizationValidator : IValidator; + +public class InviteUserOrganizationValidator : IInviteUserOrganizationValidator { - public static ValidationResult Validate(InviteOrganization inviteOrganization, - PasswordManagerSubscriptionUpdate subscriptionUpdate) + public async Task> ValidateAsync(InviteOrganization inviteOrganization) { - if (inviteOrganization.Seats is null || subscriptionUpdate.SeatsRequiredToAdd is 0) + if (inviteOrganization.Seats is null) { return new Valid(inviteOrganization); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerInviteUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerInviteUserValidator.cs index db72e7cd23..7101c6671d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerInviteUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerInviteUserValidator.cs @@ -1,15 +1,33 @@ -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -public static class PasswordManagerInviteUserValidator +public interface IPasswordManagerInviteUserValidator : IValidator; + +public class PasswordManagerInviteUserValidator( + IGlobalSettings globalSettings, + IEnvironmentValidator environmentValidator, + IInviteUserOrganizationValidator inviteUserOrganizationValidator, + IProviderRepository providerRepository, + IPaymentService paymentService, + IOrganizationRepository organizationRepository + ) : IPasswordManagerInviteUserValidator { /// /// This is for validating if the organization can add additional users. /// /// /// - public static ValidationResult Validate(PasswordManagerSubscriptionUpdate subscriptionUpdate) + public static ValidationResult ValidatePasswordManager(PasswordManagerSubscriptionUpdate subscriptionUpdate) { if (subscriptionUpdate.Seats is null) { @@ -43,4 +61,52 @@ public static class PasswordManagerInviteUserValidator return new Valid(subscriptionUpdate); } + + public async Task> ValidateAsync(PasswordManagerSubscriptionUpdate request) + { + switch (ValidatePasswordManager(request)) + { + case Valid valid + when valid.Value.SeatsRequiredToAdd is 0: + return new Valid(request); + case Invalid invalid: + return invalid; + } + + if (await environmentValidator.ValidateAsync(new EnvironmentRequest(globalSettings, request)) is Invalid invalidEnvironment) + { + return invalidEnvironment.Map(request); + } + + var organizationValidationResult = await inviteUserOrganizationValidator.ValidateAsync(request.InviteOrganization); + + if (organizationValidationResult is Invalid organizationValidation) + { + return organizationValidation.Map(request); + } + + var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId); + if (provider is not null) + { + var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider)); + + if (providerValidationResult is Invalid invalidProviderValidation) + { + return invalidProviderValidation.Map(request); + } + } + + var paymentSubscription = await paymentService.GetSubscriptionAsync( + await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId)); + + var paymentValidationResult = InviteUserPaymentValidation.Validate( + new PaymentsSubscription(paymentSubscription, request.InviteOrganization)); + + if (paymentValidationResult is Invalid invalidPaymentValidation) + { + return invalidPaymentValidation.Map(request); + } + + return new Valid(request); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerSubscriptionUpdate.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerSubscriptionUpdate.cs index 281965c0af..43eb7c1bd3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerSubscriptionUpdate.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerSubscriptionUpdate.cs @@ -32,7 +32,7 @@ public class PasswordManagerSubscriptionUpdate public int? AvailableSeats => Seats - OccupiedSeats; /// - /// Number of seats to scale the organization to. + /// Number of seats to scale the organization by. /// /// If Organization has no seat limit (Seats is null), then there are no new seats to add. /// @@ -50,17 +50,21 @@ public class PasswordManagerSubscriptionUpdate public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; } + public InviteOrganization InviteOrganization { get; } + private PasswordManagerSubscriptionUpdate(int? organizationSeats, int? organizationAutoScaleSeatLimit, int currentSeats, int newUsersToAdd, - Plan.PasswordManagerPlanFeatures plan) + Plan.PasswordManagerPlanFeatures plan, + InviteOrganization inviteOrganization) { Seats = organizationSeats; MaxAutoScaleSeats = organizationAutoScaleSeatLimit; OccupiedSeats = currentSeats; NewUsersToAdd = newUsersToAdd; PasswordManagerPlan = plan; + InviteOrganization = inviteOrganization; } public PasswordManagerSubscriptionUpdate(InviteOrganization inviteOrganization, int occupiedSeats, int newUsersToAdd) : @@ -69,7 +73,8 @@ public class PasswordManagerSubscriptionUpdate organizationAutoScaleSeatLimit: inviteOrganization.MaxAutoScaleSeats, currentSeats: occupiedSeats, newUsersToAdd: newUsersToAdd, - plan: inviteOrganization.Plan.PasswordManager) + plan: inviteOrganization.Plan.PasswordManager, + inviteOrganization: inviteOrganization) { } public PasswordManagerSubscriptionUpdate(InviteUserOrganizationValidationRequest validationRequest) : @@ -78,6 +83,7 @@ public class PasswordManagerSubscriptionUpdate organizationAutoScaleSeatLimit: validationRequest.InviteOrganization.MaxAutoScaleSeats, currentSeats: validationRequest.OccupiedPmSeats, newUsersToAdd: validationRequest.Invites.Length, - plan: validationRequest.InviteOrganization.Plan.PasswordManager) + plan: validationRequest.InviteOrganization.Plan.PasswordManager, + inviteOrganization: validationRequest.InviteOrganization) { } } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 30028c91c9..4fe8ee9237 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -15,6 +15,9 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.Models.Business.Tokenables; using Bit.Core.OrganizationFeatures.OrganizationCollections; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; @@ -175,8 +178,12 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs index 5c6758fd17..947f66a821 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs @@ -5,4 +5,5 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; public interface IUpdateSecretsManagerSubscriptionCommand { Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update); + Task ValidateUpdateAsync(SecretsManagerSubscriptionUpdate update); } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs index 78ab35c38c..91f6516501 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs @@ -124,7 +124,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs } - private async Task ValidateUpdateAsync(SecretsManagerSubscriptionUpdate update) + public async Task ValidateUpdateAsync(SecretsManagerSubscriptionUpdate update) { if (_globalSettings.SelfHosted) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Helpers/InviteUserOrganizationValidationRequestHelpers.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Helpers/InviteUserOrganizationValidationRequestHelpers.cs index e58e5a9026..4ffdfedc84 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Helpers/InviteUserOrganizationValidationRequestHelpers.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Helpers/InviteUserOrganizationValidationRequestHelpers.cs @@ -1,14 +1,15 @@ -using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager; +using Bit.Core.Models.Business; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers; public static class InviteUserOrganizationValidationRequestHelpers { public static InviteUserOrganizationValidationRequest GetInviteValidationRequestMock(InviteOrganizationUsersRequest request, - InviteOrganization inviteOrganization) => + InviteOrganization inviteOrganization, Organization organization) => new() { Invites = request.Invites, @@ -18,7 +19,8 @@ public static class InviteUserOrganizationValidationRequestHelpers OccupiedPmSeats = 0, OccupiedSmSeats = 0, PasswordManagerSubscriptionUpdate = new PasswordManagerSubscriptionUpdate(inviteOrganization, 0, 0), - SecretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(inviteOrganization, 0, 0, 0) + SecretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true) + .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager)) }; public static InviteUserOrganizationValidationRequest WithPasswordManagerUpdate(this InviteUserOrganizationValidationRequest request, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate) => diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 1008370206..1a06587c8c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -6,11 +6,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; 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; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager; using Bit.Core.AdminConsole.Shared.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Business; using Bit.Core.Models.Commands; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -24,6 +24,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers; +using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -66,7 +67,7 @@ public class InviteOrganizationUserCommandTests sutProvider.GetDependency() .ValidateAsync(Arg.Any()) - .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization))); + .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization, organization))); // Act var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); @@ -128,7 +129,7 @@ public class InviteOrganizationUserCommandTests sutProvider.GetDependency() .ValidateAsync(Arg.Any()) - .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization))); + .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization, organization))); // Act var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); @@ -259,7 +260,7 @@ public class InviteOrganizationUserCommandTests sutProvider.GetDependency() .ValidateAsync(Arg.Any()) - .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization) + .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization, organization) .WithPasswordManagerUpdate(new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1)))); // Act @@ -329,7 +330,7 @@ public class InviteOrganizationUserCommandTests sutProvider.GetDependency() .ValidateAsync(Arg.Any()) - .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization) + .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization, organization) .WithPasswordManagerUpdate(passwordManagerUpdate))); // Act @@ -364,6 +365,7 @@ public class InviteOrganizationUserCommandTests organization.Seats = 1; organization.SmSeats = 1; organization.MaxAutoscaleSeats = 2; + organization.MaxAutoscaleSmSeats = 2; ownerDetails.Type = OrganizationUserType.Owner; var inviteOrganization = new InviteOrganization(organization, new FreePlan()); @@ -383,11 +385,8 @@ public class InviteOrganizationUserCommandTests performedBy: Guid.Empty, timeProvider.GetUtcNow()); - var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate( - inviteOrganization, - organization.SmSeats.Value, - 1, - organization.Seats.Value); + var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true) + .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager)); var orgUserRepository = sutProvider.GetDependency(); @@ -397,6 +396,8 @@ public class InviteOrganizationUserCommandTests orgUserRepository .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner) .Returns([ownerDetails]); + orgUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(1); + orgUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1); var orgRepository = sutProvider.GetDependency(); @@ -405,7 +406,7 @@ public class InviteOrganizationUserCommandTests sutProvider.GetDependency() .ValidateAsync(Arg.Any()) - .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization) + .Returns(new Valid(GetInviteValidationRequestMock(request, inviteOrganization, organization) .WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate))); // Act @@ -416,7 +417,6 @@ public class InviteOrganizationUserCommandTests await sutProvider.GetDependency() .Received(1) - .UpdateSubscriptionAsync(Arg.Is(update => - update.SmSeats == secretsManagerSubscriptionUpdate.UpdatedSeatTotal)); + .UpdateSubscriptionAsync(secretsManagerSubscriptionUpdate); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs index fbe208867f..b8d9d65e46 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs @@ -1,40 +1,39 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Shared.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; +[SutProviderCustomize] public class InviteUserOrganizationValidationTests { [Theory] [BitAutoData] - public void Validate_WhenOrganizationIsFreeTier_ShouldReturnValidResponse(Organization organization) + public async Task Validate_WhenOrganizationIsFreeTier_ShouldReturnValidResponse(Organization organization, SutProvider sutProvider) { var inviteOrganization = new InviteOrganization(organization, new FreePlan()); - var validSubscriptionUpdate = new PasswordManagerSubscriptionUpdate(inviteOrganization, 0, 0); - var result = InviteUserOrganizationValidator.Validate(inviteOrganization, validSubscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); } [Theory] [BitAutoData] - public void Validate_WhenOrganizationDoesNotHavePaymentMethod_ShouldReturnInvalidResponseWithPaymentMethodMessage( - Organization organization) + public async Task Validate_WhenOrganizationDoesNotHavePaymentMethod_ShouldReturnInvalidResponseWithPaymentMethodMessage( + Organization organization, SutProvider sutProvider) { organization.GatewayCustomerId = string.Empty; organization.Seats = 3; var inviteOrganization = new InviteOrganization(organization, new FreePlan()); - var validSubscriptionUpdate = new PasswordManagerSubscriptionUpdate(inviteOrganization, 3, 1); - var result = InviteUserOrganizationValidator.Validate(inviteOrganization, validSubscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.ErrorMessageString); @@ -42,17 +41,16 @@ public class InviteUserOrganizationValidationTests [Theory] [BitAutoData] - public void Validate_WhenOrganizationDoesNotHaveSubscription_ShouldReturnInvalidResponseWithSubscriptionMessage( - Organization organization) + public async Task Validate_WhenOrganizationDoesNotHaveSubscription_ShouldReturnInvalidResponseWithSubscriptionMessage( + Organization organization, SutProvider sutProvider) { organization.GatewaySubscriptionId = string.Empty; organization.Seats = 3; organization.MaxAutoscaleSeats = 4; var inviteOrganization = new InviteOrganization(organization, new FreePlan()); - var validSubscriptionUpdate = new PasswordManagerSubscriptionUpdate(inviteOrganization, 3, 1); - var result = InviteUserOrganizationValidator.Validate(inviteOrganization, validSubscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.ErrorMessageString); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs index 17fb58dfb4..3b5efc242d 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs @@ -4,17 +4,20 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V using Bit.Core.AdminConsole.Shared.Validation; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; +[SutProviderCustomize] public class PasswordManagerInviteUserValidatorTests { [Theory] [BitAutoData] - public void Validate_OrganizationDoesNotHaveSeatsLimit_ShouldReturnValidResult(Organization organization) + public async Task Validate_OrganizationDoesNotHaveSeatsLimit_ShouldReturnValidResult(Organization organization, + SutProvider sutProvider) { organization.Seats = null; @@ -22,14 +25,15 @@ public class PasswordManagerInviteUserValidatorTests var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, 0, 0); - var result = PasswordManagerInviteUserValidator.Validate(subscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); } [Theory] [BitAutoData] - public void Validate_NumberOfSeatsToAddMatchesSeatsAvailable_ShouldReturnValidResult(Organization organization) + public async Task Validate_NumberOfSeatsToAddMatchesSeatsAvailable_ShouldReturnValidResult(Organization organization, + SutProvider sutProvider) { organization.Seats = 8; organization.PlanType = PlanType.EnterpriseAnnually; @@ -40,14 +44,15 @@ public class PasswordManagerInviteUserValidatorTests var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, seatsOccupiedByUsers, additionalSeats); - var result = PasswordManagerInviteUserValidator.Validate(subscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); } [Theory] [BitAutoData] - public void Validate_NumberOfSeatsToAddIsGreaterThanMaxSeatsAllowed_ShouldBeInvalidWithSeatLimitMessage(Organization organization) + public async Task Validate_NumberOfSeatsToAddIsGreaterThanMaxSeatsAllowed_ShouldBeInvalidWithSeatLimitMessage(Organization organization, + SutProvider sutProvider) { organization.Seats = 4; organization.MaxAutoscaleSeats = 4; @@ -59,7 +64,7 @@ public class PasswordManagerInviteUserValidatorTests var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, seatsOccupiedByUsers, additionalSeats); - var result = PasswordManagerInviteUserValidator.Validate(subscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.ErrorMessageString); @@ -67,7 +72,8 @@ public class PasswordManagerInviteUserValidatorTests [Theory] [BitAutoData] - public void Validate_GivenThePlanDoesNotAllowAdditionalSeats_ShouldBeInvalidMessageOfPlanNotAllowingSeats(Organization organization) + public async Task Validate_GivenThePlanDoesNotAllowAdditionalSeats_ShouldBeInvalidMessageOfPlanNotAllowingSeats(Organization organization, + SutProvider sutProvider) { organization.Seats = 8; organization.MaxAutoscaleSeats = 9; @@ -79,7 +85,7 @@ public class PasswordManagerInviteUserValidatorTests var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, seatsOccupiedByUsers, additionalSeats); - var result = PasswordManagerInviteUserValidator.Validate(subscriptionUpdate); + var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.ErrorMessageString);