diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserValidationErrorMessages.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserValidationErrorMessages.cs index 7d26628da5..5d7239b6af 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserValidationErrorMessages.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserValidationErrorMessages.cs @@ -18,4 +18,5 @@ public static class InviteUserValidationErrorMessages public const string OrganizationNoSecretsManager = "Organization has no access to Secrets Manager"; public const string SecretsManagerSeatLimitReached = "Secrets Manager seat limit has been reached."; public const string SecretsManagerCannotExceedPasswordManager = "You cannot have more Secrets Manager seats than Password Manager seats."; + public const string SecretsManagerAdditionalSeatLimitReached = "You have reached the maximum number of Secrets Manager seats ({0}) for this plan."; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/PasswordManagerSubscriptionUpdate.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/PasswordManagerSubscriptionUpdate.cs index f00c2202b5..44bf1eb5f7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/PasswordManagerSubscriptionUpdate.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/PasswordManagerSubscriptionUpdate.cs @@ -23,25 +23,34 @@ public class PasswordManagerSubscriptionUpdate public int? UpdatedSeatTotal => Seats + SeatsRequiredToAdd; - public Plan Plan { get; } + public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; } - private PasswordManagerSubscriptionUpdate(int? organizationSeats, int? organizationAutoScaleSeatLimit, int currentSeats, int seatsToAdd, Plan plan) + private PasswordManagerSubscriptionUpdate(int? organizationSeats, + int? organizationAutoScaleSeatLimit, + int currentSeats, + int seatsToAdd, + Plan.PasswordManagerPlanFeatures plan) { Seats = organizationSeats; MaxAutoScaleSeats = organizationAutoScaleSeatLimit; OccupiedSeats = currentSeats; AdditionalSeats = seatsToAdd; - Plan = plan; + PasswordManagerPlan = plan; } public static PasswordManagerSubscriptionUpdate Create(OrganizationDto organizationDto, int occupiedSeats, int seatsToAdd) { - return new PasswordManagerSubscriptionUpdate(organizationDto.Seats, organizationDto.MaxAutoScaleSeats, occupiedSeats, seatsToAdd, organizationDto.Plan); + return new PasswordManagerSubscriptionUpdate( + organizationDto.Seats, + organizationDto.MaxAutoScaleSeats, + occupiedSeats, + seatsToAdd, + organizationDto.Plan.PasswordManager); } public static PasswordManagerSubscriptionUpdate Create(InviteUserOrganizationValidationRequest refined) { return new PasswordManagerSubscriptionUpdate(refined.Organization.Seats, refined.Organization.MaxAutoScaleSeats, - refined.OccupiedPmSeats, refined.Invites.Length, refined.Organization.Plan); + refined.OccupiedPmSeats, refined.Invites.Length, refined.Organization.Plan.PasswordManager); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/SecretsManagerSubscriptionUpdate.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/SecretsManagerSubscriptionUpdate.cs index afd6f3b77f..b813e7fdd4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/SecretsManagerSubscriptionUpdate.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Models/SecretsManagerSubscriptionUpdate.cs @@ -1,35 +1,46 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.Models.StaticStore; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; public class SecretsManagerSubscriptionUpdate { - public bool UseSecretsManger { get; private init; } - public int? Seats { get; private init; } - public int? MaxAutoScaleSeats { get; private init; } - public int OccupiedSeats { get; private init; } - public int AdditionalSeats { get; private init; } - public PasswordManagerSubscriptionUpdate PasswordManagerSubscriptionUpdate { get; private init; } + public bool UseSecretsManger { get; } + public int? Seats { get; } + public int? MaxAutoScaleSeats { get; } + public int OccupiedSeats { get; } + public int AdditionalSeats { get; } + public int? PasswordManagerUpdatedSeatTotal { get; } + public Plan.SecretsManagerPlanFeatures SecretsManagerPlan { get; } public int? AvailableSeats => Seats - OccupiedSeats; public int SeatsRequiredToAdd => AdditionalSeats - AvailableSeats ?? 0; public int? UpdatedSeatTotal => Seats + SeatsRequiredToAdd; - private SecretsManagerSubscriptionUpdate(bool useSecretsManger, int? organizationSeats, - int? organizationAutoScaleSeatLimit, int currentSeats, int seatsToAdd, PasswordManagerSubscriptionUpdate passwordManagerSeats) + private SecretsManagerSubscriptionUpdate(bool useSecretsManger, + int? organizationSeats, + int? organizationAutoScaleSeatLimit, + int currentSeats, + int seatsToAdd, + int? passwordManagerUpdatedSeatTotal, + Plan.SecretsManagerPlanFeatures plan) { UseSecretsManger = useSecretsManger; Seats = organizationSeats; MaxAutoScaleSeats = organizationAutoScaleSeatLimit; OccupiedSeats = currentSeats; AdditionalSeats = seatsToAdd; - PasswordManagerSubscriptionUpdate = passwordManagerSeats; + PasswordManagerUpdatedSeatTotal = passwordManagerUpdatedSeatTotal; + SecretsManagerPlan = plan; } public static SecretsManagerSubscriptionUpdate Create(InviteUserOrganizationValidationRequest refined, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate) { return new SecretsManagerSubscriptionUpdate(refined.Organization.UseSecretsManager, - refined.Organization.SmSeats, refined.Organization.SmMaxAutoScaleSeats, - refined.OccupiedPmSeats, refined.Invites.Count(x => x.AccessSecretsManager), - passwordManagerSubscriptionUpdate); + refined.Organization.SmSeats, + refined.Organization.SmMaxAutoScaleSeats, + refined.OccupiedSmSeats, + refined.Invites.Count(x => x.AccessSecretsManager), + passwordManagerSubscriptionUpdate.UpdatedSeatTotal, + refined.Organization.Plan.SecretsManager); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidation.cs index 0ff2145be0..1412d5749d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidation.cs @@ -25,16 +25,16 @@ public static class PasswordManagerInviteUserValidation return new Invalid(SeatLimitHasBeenReachedError); } - if (subscriptionUpdate.Plan.PasswordManager.HasAdditionalSeatsOption is false) + if (subscriptionUpdate.PasswordManagerPlan.HasAdditionalSeatsOption is false) { return new Invalid(PlanDoesNotAllowAdditionalSeats); } // Apparently MaxAdditionalSeats is never set. Can probably be removed. - if (subscriptionUpdate.AdditionalSeats > subscriptionUpdate.Plan.PasswordManager.MaxAdditionalSeats) + if (subscriptionUpdate.AdditionalSeats > subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats) { return new Invalid(string.Format(PlanOnlyAllowsMaxAdditionalSeats, - subscriptionUpdate.Plan.PasswordManager.MaxAdditionalSeats)); + subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats)); } return new Valid(subscriptionUpdate); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs index 92911f82aa..3a39c8e83e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs @@ -3,12 +3,12 @@ using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Invite namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; -public class SecretsManagerInviteUserValidation +public static class SecretsManagerInviteUserValidation { - // Do we need to check if they are attempting to subtract seats? (no I don't think so because this is for inviting a User) + // NOTE This is only validating adding new users public static ValidationResult Validate(SecretsManagerSubscriptionUpdate subscriptionUpdate) { - if (subscriptionUpdate.UseSecretsManger) + if (subscriptionUpdate.UseSecretsManger is false) { return new Invalid(OrganizationNoSecretsManager); } @@ -18,63 +18,24 @@ public class SecretsManagerInviteUserValidation return new Valid(subscriptionUpdate); // no need to adjust seats...continue on } - // if (update.Autoscaling && update.SmSeats.Value < organization.SmSeats.Value) - // { - // throw new BadRequestException("Cannot use autoscaling to subtract seats."); - // } + // max additional seats is never set...maybe remove this + if (subscriptionUpdate.SecretsManagerPlan is { HasAdditionalSeatsOption: false } || + subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats is not null && + subscriptionUpdate.AdditionalSeats > subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats) + { + return new Invalid( + string.Format(SecretsManagerAdditionalSeatLimitReached, + subscriptionUpdate.SecretsManagerPlan.BaseSeats + + subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault())); + } - // Might need to check plan - - // Check plan maximum seats - // if (!plan.SecretsManager.HasAdditionalSeatsOption || - // (plan.SecretsManager.MaxAdditionalSeats.HasValue && update.SmSeatsExcludingBase > plan.SecretsManager.MaxAdditionalSeats.Value)) - // { - // var planMaxSeats = plan.SecretsManager.BaseSeats + plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(); - // throw new BadRequestException($"You have reached the maximum number of Secrets Manager seats ({planMaxSeats}) for this plan."); - // } - - // Check autoscale maximum seats if (subscriptionUpdate.UpdatedSeatTotal is not null && subscriptionUpdate.MaxAutoScaleSeats is not null && subscriptionUpdate.UpdatedSeatTotal > subscriptionUpdate.MaxAutoScaleSeats) { return new Invalid(SecretsManagerSeatLimitReached); } - // if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats.Value > update.MaxAutoscaleSmSeats.Value) - // { - // var message = update.Autoscaling - // ? "Secrets Manager seat limit has been reached." - // : "Cannot set max seat autoscaling below seat count."; - // throw new BadRequestException(message); - // } - - // Inviting a user... this shouldn't matter - // - // Check minimum seats included with plan - // if (plan.SecretsManager.BaseSeats > update.SmSeats.Value) - // { - // throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseSeats} Secrets Manager seats."); - // } - - // Check minimum seats required by business logic - // if (update.SmSeats.Value <= 0) - // { - // throw new BadRequestException("You must have at least 1 Secrets Manager seat."); - // } - - // Check minimum seats currently in use by the organization - // if (organization.SmSeats.Value > update.SmSeats.Value) - // { - // var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); - // if (occupiedSeats > update.SmSeats.Value) - // { - // throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " + - // "You cannot decrease your subscription below your current occupied seat count."); - // } - // } - - // Check that SM seats aren't greater than password manager seats - if (subscriptionUpdate.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal < subscriptionUpdate.UpdatedSeatTotal) + if (subscriptionUpdate.PasswordManagerUpdatedSeatTotal < subscriptionUpdate.UpdatedSeatTotal) { return new Invalid(SecretsManagerCannotExceedPasswordManager); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidationTests.cs index f4c9a9b1af..449984e4f6 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidationTests.cs @@ -50,6 +50,7 @@ public class PasswordManagerInviteUserValidationTests { organization.Seats = 4; organization.MaxAutoscaleSeats = 4; + organization.PlanType = PlanType.EnterpriseAnnually; var seatsOccupiedByUsers = 4; var additionalSeats = 1; @@ -81,5 +82,4 @@ public class PasswordManagerInviteUserValidationTests Assert.IsType>(result); Assert.Equal(InviteUserValidationErrorMessages.PlanDoesNotAllowAdditionalSeats, result.ErrorMessageString); } - } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs new file mode 100644 index 0000000000..780fcb63d7 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs @@ -0,0 +1,134 @@ +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; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; + +[SutProviderCustomize] +public class SecretsManagerInviteUserValidationTests +{ + [Theory] + [BitAutoData] + public void Validate_GivenOrganizationDoesNotHaveSecretsManager_ThenShouldNotBeAllowedToAddSecretsManagerUsers( + Organization organization) + { + organization.UseSecretsManager = false; + + var organizationDto = OrganizationDto.FromOrganization(organization); + var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0); + + var request = new InviteUserOrganizationValidationRequest + { + Invites = [new OrganizationUserInviteDto()], + Organization = organizationDto, + PerformedBy = Guid.Empty, + PerformedAt = default, + OccupiedPmSeats = 0, + OccupiedSmSeats = 0 + }; + + var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate); + + var result = SecretsManagerInviteUserValidation.Validate(update); + + Assert.IsType>(result); + Assert.Equal(InviteUserValidationErrorMessages.OrganizationNoSecretsManager, result.ErrorMessageString); + } + + [Theory] + [BitAutoData] + public void Validate_GivenOrganizationHasSecretsManagerWithoutASeatLimit_ThenShouldBeAllowedToAddSecretsManagerUsers( + Organization organization) + { + organization.SmSeats = null; + organization.UseSecretsManager = true; + + var organizationDto = OrganizationDto.FromOrganization(organization); + var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0); + + var request = new InviteUserOrganizationValidationRequest + { + Invites = [new OrganizationUserInviteDto()], + Organization = organizationDto, + PerformedBy = Guid.Empty, + PerformedAt = default, + OccupiedPmSeats = 0, + OccupiedSmSeats = 0 + }; + + var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate); + + var result = SecretsManagerInviteUserValidation.Validate(update); + + Assert.IsType>(result); + } + + [Theory] + [BitAutoData] + public void Validate_GivenOrganizationPlanDoesNotAllowAdditionalSeats_ThenShouldNotBeAllowedToAddSecretsManagerUsers( + Organization organization) + { + organization.SmSeats = 4; + organization.MaxAutoscaleSmSeats = 4; + organization.UseSecretsManager = true; + organization.PlanType = PlanType.EnterpriseAnnually; + + var organizationDto = OrganizationDto.FromOrganization(organization); + var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0); + + var request = new InviteUserOrganizationValidationRequest + { + Invites = [OrganizationUserInviteDto.Create("email@test.com", OrganizationUserInvite.Create(["email@test.com"], [], OrganizationUserType.User, new Permissions(), string.Empty, true))], + Organization = organizationDto, + PerformedBy = Guid.Empty, + PerformedAt = default, + OccupiedPmSeats = 0, + OccupiedSmSeats = 4 + }; + + var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate); + + var result = SecretsManagerInviteUserValidation.Validate(update); + + Assert.IsType>(result); + Assert.Equal(InviteUserValidationErrorMessages.SecretsManagerSeatLimitReached, result.ErrorMessageString); + } + + [Theory] + [BitAutoData] + public void Validate_GivenPasswordManagerSeatsAreTheSameAsSecretsManagerSeats_WhenAttemptingToAddASecretManagerSeatOnly_ThenShouldNotBeAllowedToAddSecretsManagerUsers( + Organization organization) + { + organization.SmSeats = 4; + organization.MaxAutoscaleSmSeats = 5; + organization.UseSecretsManager = true; + organization.PlanType = PlanType.EnterpriseAnnually; + + var organizationDto = OrganizationDto.FromOrganization(organization); + var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0); + + var request = new InviteUserOrganizationValidationRequest + { + Invites = [OrganizationUserInviteDto.Create("email@test.com", OrganizationUserInvite.Create(["email@test.com"], [], OrganizationUserType.User, new Permissions(), string.Empty, true))], + Organization = organizationDto, + PerformedBy = Guid.Empty, + PerformedAt = default, + OccupiedPmSeats = 0, + OccupiedSmSeats = 4 + }; + + var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate); + + var result = SecretsManagerInviteUserValidation.Validate(update); + + Assert.IsType>(result); + Assert.Equal(InviteUserValidationErrorMessages.SecretsManagerCannotExceedPasswordManager, result.ErrorMessageString); + } +}