diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs index 4c4fda952a..dfa9952804 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs @@ -1,9 +1,12 @@ using System.Text.Json; +using Bit.Core; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Scim.IntegrationTest.Factories; using Bit.Scim.Models; using Bit.Scim.Utilities; using Bit.Test.Common.Helpers; +using NSubstitute; using Xunit; namespace Bit.Scim.IntegrationTest.Controllers.v2; @@ -276,9 +279,15 @@ public class UsersControllerTests : IClassFixture, IAsyn AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); } - [Fact] - public async Task Post_Success() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Post_Success(bool isScimInviteUserOptimizationEnabled) { + _factory.SubstituteService((IFeatureService featureService) + => featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization) + .Returns(isScimInviteUserOptimizationEnabled)); + var email = "user5@example.com"; var displayName = "Test User 5"; var externalId = "UE"; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 7fe4b2fe2d..0979d5747d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -96,7 +96,8 @@ public class InviteOrganizationUsersCommand(IEventService eventService, var valid = validationResult as Valid; var organizationUserCollection = invitesToSend - .Select(MapToDataModel(request.PerformedAt)); + .Select(MapToDataModel(request.PerformedAt)) + .ToArray(); var organization = await organizationRepository.GetByIdAsync(valid.Value.Organization.OrganizationId); try diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs index a11bb51fc4..451c36e9b9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteUserOrganizationValidationRequest.cs @@ -5,6 +5,20 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse public class InviteUserOrganizationValidationRequest { + public InviteUserOrganizationValidationRequest() { } + + public InviteUserOrganizationValidationRequest(InviteUserOrganizationValidationRequest request, PasswordManagerSubscriptionUpdate subscriptionUpdate, SecretsManagerSubscriptionUpdate smSubscriptionUpdate) + { + Invites = request.Invites; + Organization = request.Organization; + PerformedBy = request.PerformedBy; + PerformedAt = request.PerformedAt; + OccupiedPmSeats = request.OccupiedPmSeats; + OccupiedSmSeats = request.OccupiedSmSeats; + PasswordManagerSubscriptionUpdate = subscriptionUpdate; + SecretsManagerSubscriptionUpdate = smSubscriptionUpdate; + } + public OrganizationUserInviteDto[] Invites { get; init; } = []; public OrganizationDto Organization { get; init; } public Guid PerformedBy { get; init; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidation.cs index a6b57bec1e..dea54e0c7a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidation.cs @@ -51,7 +51,8 @@ public class InviteUsersValidation( var provider = await providerRepository.GetByOrganizationIdAsync(request.Organization.OrganizationId); - if (InvitingUserOrganizationProviderValidation.Validate(ProviderDto.FromProviderEntity(provider)) is + if (provider is not null && + InvitingUserOrganizationProviderValidation.Validate(ProviderDto.FromProviderEntity(provider)) is Invalid invalidProviderValidation) { return new Invalid(invalidProviderValidation.ErrorMessageString); @@ -65,7 +66,10 @@ public class InviteUsersValidation( return new Invalid(invalidPaymentValidation.ErrorMessageString); } - return new Valid(null); + return new Valid(new InviteUserOrganizationValidationRequest( + request, + subscriptionUpdate, + smSubscriptionUpdate)); } public static ValidationResult ValidateEnvironment(IGlobalSettings globalSettings) => diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InvitingUserOrganizationValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InvitingUserOrganizationValidation.cs index 9efdd57299..9277741475 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InvitingUserOrganizationValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InvitingUserOrganizationValidation.cs @@ -7,6 +7,11 @@ public static class InvitingUserOrganizationValidation { public static ValidationResult Validate(OrganizationDto organization) { + if (organization.Seats is null) + { + return new Valid(organization); + } + if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) { return new Invalid(NoPaymentMethodFoundError); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs index 3a39c8e83e..1293f15e4f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidation.cs @@ -5,41 +5,39 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse public static class SecretsManagerInviteUserValidation { - // NOTE This is only validating adding new users - public static ValidationResult Validate(SecretsManagerSubscriptionUpdate subscriptionUpdate) - { - if (subscriptionUpdate.UseSecretsManger is false) + public static ValidationResult Validate( + SecretsManagerSubscriptionUpdate subscriptionUpdate) => + subscriptionUpdate switch { - return new Invalid(OrganizationNoSecretsManager); - } + { UseSecretsManger: false, AdditionalSeats: > 0 } => + new Invalid(OrganizationNoSecretsManager), - if (subscriptionUpdate.Seats == null) - { - return new Valid(subscriptionUpdate); // no need to adjust seats...continue on - } + { UseSecretsManger: false, AdditionalSeats: 0 } or { UseSecretsManger: true, Seats: null } => + new Valid(subscriptionUpdate), - // 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, + { UseSecretsManger: true, SecretsManagerPlan.HasAdditionalSeatsOption: false } => + new Invalid( + string.Format(SecretsManagerAdditionalSeatLimitReached, subscriptionUpdate.SecretsManagerPlan.BaseSeats + - subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault())); - } + subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault())), - if (subscriptionUpdate.UpdatedSeatTotal is not null && subscriptionUpdate.MaxAutoScaleSeats is not null && - subscriptionUpdate.UpdatedSeatTotal > subscriptionUpdate.MaxAutoScaleSeats) - { - return new Invalid(SecretsManagerSeatLimitReached); - } + { UseSecretsManger: true, SecretsManagerPlan.MaxAdditionalSeats: var planMaxSeats } + when planMaxSeats < subscriptionUpdate.AdditionalSeats => + new Invalid( + string.Format(SecretsManagerAdditionalSeatLimitReached, + subscriptionUpdate.SecretsManagerPlan.BaseSeats + + subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault())), - if (subscriptionUpdate.PasswordManagerUpdatedSeatTotal < subscriptionUpdate.UpdatedSeatTotal) - { - return new Invalid(SecretsManagerCannotExceedPasswordManager); - } + { UseSecretsManger: true, UpdatedSeatTotal: var updateSeatTotal, MaxAutoScaleSeats: var maxAutoScaleSeats } + when updateSeatTotal > maxAutoScaleSeats => + new Invalid(SecretsManagerSeatLimitReached), - return new Valid(subscriptionUpdate); - } + { + PasswordManagerUpdatedSeatTotal: var passwordManagerUpdatedSeatTotal, + UpdatedSeatTotal: var secretsManagerUpdatedSeatTotal + } when passwordManagerUpdatedSeatTotal < secretsManagerUpdatedSeatTotal => + new Invalid(SecretsManagerCannotExceedPasswordManager), + + _ => new Valid(subscriptionUpdate) + }; } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs index b7288b69e7..2241558c1a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/SecretsManagerInviteUserValidationTests.cs @@ -16,7 +16,7 @@ public class SecretsManagerInviteUserValidationTests { [Theory] [BitAutoData] - public void Validate_GivenOrganizationDoesNotHaveSecretsManager_ThenShouldNotBeAllowedToAddSecretsManagerUsers( + public void Validate_GivenOrganizationDoesNotHaveSecretsManagerAndNotTryingToAddSecretsManagerUser_ThenTheRequestIsValid( Organization organization) { organization.UseSecretsManager = false; @@ -38,6 +38,35 @@ public class SecretsManagerInviteUserValidationTests var result = SecretsManagerInviteUserValidation.Validate(update); + Assert.IsType>(result); + } + + [Theory] + [BitAutoData] + public void Validate_GivenOrganizationDoesNotHaveSecretsManagerAndTryingToAddSecretsManagerUser_ThenShouldReturnInvalidMessage( + Organization organization) + { + organization.UseSecretsManager = false; + + var organizationDto = OrganizationDto.FromOrganization(organization); + var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0); + + var invite = OrganizationUserInvite.Create(["email@test.com"], [], OrganizationUserType.User, new Permissions(), string.Empty, true); + + var request = new InviteUserOrganizationValidationRequest + { + Invites = [OrganizationUserInviteDto.Create(invite.Emails.First(), invite, organizationDto.OrganizationId)], + 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); }