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

Corrected model name. Corrected SM seat calculation. Added test for it.

This commit is contained in:
jrmccannon 2025-04-02 20:44:17 -05:00
parent 739bc65e87
commit e80bbe1caa
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
9 changed files with 129 additions and 63 deletions

View File

@ -87,7 +87,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId)));
}
var validationResult = await inviteUsersValidator.ValidateAsync(new InviteUserOrganizationValidationRequest
var validationResult = await inviteUsersValidator.ValidateAsync(new InviteOrganizationUsersValidationRequest
{
Invites = invitesToSend.ToArray(),
InviteOrganization = request.InviteOrganization,
@ -97,12 +97,12 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
});
if (validationResult is Invalid<InviteUserOrganizationValidationRequest> invalid)
if (validationResult is Invalid<InviteOrganizationUsersValidationRequest> invalid)
{
return invalid.MapToFailure(r => new InviteOrganizationUsersResponse(r));
}
var validatedRequest = validationResult as Valid<InviteUserOrganizationValidationRequest>;
var validatedRequest = validationResult as Valid<InviteOrganizationUsersValidationRequest>;
var organizationUserToInviteEntities = invitesToSend
.Select(x => x.MapToDataModel(request.PerformedAt, validatedRequest!.Value.InviteOrganization))
@ -135,8 +135,9 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
await RevertPasswordManagerChangesAsync(validatedRequest, organization);
return new Failure<InviteOrganizationUsersResponse>(new FailedToInviteUsersError(
new InviteOrganizationUsersResponse(validatedRequest.Value)));
return new Failure<InviteOrganizationUsersResponse>(
new FailedToInviteUsersError(
new InviteOrganizationUsersResponse(validatedRequest.Value)));
}
return new Success<InviteOrganizationUsersResponse>(
@ -156,7 +157,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
.ToArray();
}
private async Task RevertPasswordManagerChangesAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult, Organization organization)
private async Task RevertPasswordManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
{
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0)
{
@ -173,7 +174,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
}
}
private async Task RevertSecretsManagerChangesAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult, Organization organization, int? initialSmSeats)
private async Task RevertSecretsManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization, int? initialSmSeats)
{
if (validatedResult.Value.SecretsManagerSubscriptionUpdate?.SmSeatsChanged is true)
{
@ -189,7 +190,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
}
}
private async Task PublishReferenceEventAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult,
private async Task PublishReferenceEventAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult,
Organization organization) =>
await referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext)
@ -203,12 +204,12 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
users.Select(x => x.OrganizationUser),
organization));
private async Task SendAdditionalEmailsAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult, Organization organization)
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
{
await SendPasswordManagerMaxSeatLimitEmailsAsync(validatedResult, organization);
}
private async Task SendPasswordManagerMaxSeatLimitEmailsAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult, Organization organization)
private async Task SendPasswordManagerMaxSeatLimitEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
{
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
{
@ -246,7 +247,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
.Select(u => u.Email).Distinct();
}
private async Task AdjustSecretsManagerSeatsAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult)
private async Task AdjustSecretsManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult)
{
if (validatedResult.Value.SecretsManagerSubscriptionUpdate?.SmSeatsChanged is true)
{
@ -255,7 +256,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
}
private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteUserOrganizationValidationRequest> validatedResult, Organization organization)
private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
{
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd <= 0)
{

View File

@ -7,10 +7,10 @@ public class InviteOrganizationUsersResponse(Guid organizationId)
public IEnumerable<OrganizationUser> InvitedUsers { get; } = [];
public Guid OrganizationId { get; } = organizationId;
public InviteOrganizationUsersResponse(InviteUserOrganizationValidationRequest validationRequest)
: this(validationRequest.InviteOrganization.OrganizationId)
public InviteOrganizationUsersResponse(InviteOrganizationUsersValidationRequest usersValidationRequest)
: this(usersValidationRequest.InviteOrganization.OrganizationId)
{
InvitedUsers = validationRequest.Invites.Select(x => new OrganizationUser { Email = x.Email });
InvitedUsers = usersValidationRequest.Invites.Select(x => new OrganizationUser { Email = x.Email });
}
public InviteOrganizationUsersResponse(IEnumerable<OrganizationUser> invitedOrganizationUsers, Guid organizationId)

View File

@ -4,13 +4,13 @@ using Bit.Core.Models.Business;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
public class InviteUserOrganizationValidationRequest
public class InviteOrganizationUsersValidationRequest
{
public InviteUserOrganizationValidationRequest()
public InviteOrganizationUsersValidationRequest()
{
}
public InviteUserOrganizationValidationRequest(InviteUserOrganizationValidationRequest request)
public InviteOrganizationUsersValidationRequest(InviteOrganizationUsersValidationRequest request)
{
Invites = request.Invites;
InviteOrganization = request.InviteOrganization;
@ -20,7 +20,7 @@ public class InviteUserOrganizationValidationRequest
OccupiedSmSeats = request.OccupiedSmSeats;
}
public InviteUserOrganizationValidationRequest(InviteUserOrganizationValidationRequest request,
public InviteOrganizationUsersValidationRequest(InviteOrganizationUsersValidationRequest request,
PasswordManagerSubscriptionUpdate subscriptionUpdate,
SecretsManagerSubscriptionUpdate smSubscriptionUpdate)
: this(request)

View File

@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Errors;
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.Shared.Validation;
@ -11,15 +10,15 @@ using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.Organi
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
public interface IInviteUsersValidator : IValidator<InviteUserOrganizationValidationRequest>;
public interface IInviteUsersValidator : IValidator<InviteOrganizationUsersValidationRequest>;
public class InviteUsersValidator(
public class InviteOrganizationUsersValidator(
IOrganizationRepository organizationRepository,
IInviteUsersPasswordManagerValidator inviteUsersPasswordManagerValidator,
IUpdateSecretsManagerSubscriptionCommand secretsManagerSubscriptionCommand,
IPaymentService paymentService) : IInviteUsersValidator
{
public async Task<ValidationResult<InviteUserOrganizationValidationRequest>> ValidateAsync(InviteUserOrganizationValidationRequest request)
public async Task<ValidationResult<InviteOrganizationUsersValidationRequest>> ValidateAsync(InviteOrganizationUsersValidationRequest request)
{
var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(request);
@ -34,7 +33,7 @@ public class InviteUsersValidator(
// This is an expensive call, so we're doing it now to delay the check as long as possible.
if (await paymentService.HasSecretsManagerStandalone(request.InviteOrganization))
{
request = new InviteUserOrganizationValidationRequest(request)
request = new InviteOrganizationUsersValidationRequest(request)
{
Invites = request.Invites
.Select(x => new OrganizationUserInvite(x, accessSecretsManager: true))
@ -47,14 +46,14 @@ public class InviteUsersValidator(
return await ValidateSecretsManagerSubscriptionUpdateAsync(request, subscriptionUpdate);
}
return new Valid<InviteUserOrganizationValidationRequest>(new InviteUserOrganizationValidationRequest(
return new Valid<InviteOrganizationUsersValidationRequest>(new InviteOrganizationUsersValidationRequest(
request,
subscriptionUpdate,
null));
}
private async Task<ValidationResult<InviteUserOrganizationValidationRequest>> ValidateSecretsManagerSubscriptionUpdateAsync(
InviteUserOrganizationValidationRequest request,
private async Task<ValidationResult<InviteOrganizationUsersValidationRequest>> ValidateSecretsManagerSubscriptionUpdateAsync(
InviteOrganizationUsersValidationRequest request,
PasswordManagerSubscriptionUpdate subscriptionUpdate)
{
try
@ -63,24 +62,24 @@ public class InviteUsersValidator(
organization: await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId),
plan: request.InviteOrganization.Plan,
autoscaling: true)
.AdjustSeats(GetSecretManagerSeatAdjustment(
occupiedSeats: request.OccupiedSmSeats,
organization: request.InviteOrganization,
invitesToSend: request.Invites.Count(x => x.AccessSecretsManager)));
.AdjustSeats(GetSecretManagerSeatAdjustment(request));
await secretsManagerSubscriptionCommand.ValidateUpdateAsync(smSubscriptionUpdate);
return new Valid<InviteUserOrganizationValidationRequest>(new InviteUserOrganizationValidationRequest(
return new Valid<InviteOrganizationUsersValidationRequest>(new InviteOrganizationUsersValidationRequest(
request,
subscriptionUpdate,
smSubscriptionUpdate));
}
catch (Exception ex)
{
return new Invalid<InviteUserOrganizationValidationRequest>(new Error<InviteUserOrganizationValidationRequest>(ex.Message, request));
return new Invalid<InviteOrganizationUsersValidationRequest>(new Error<InviteOrganizationUsersValidationRequest>(ex.Message, request));
}
int GetSecretManagerSeatAdjustment(int occupiedSeats, InviteOrganization organization, int invitesToSend) =>
organization.SmSeats - (occupiedSeats + invitesToSend) ?? 0;
int GetSecretManagerSeatAdjustment(InviteOrganizationUsersValidationRequest request) =>
Math.Abs(
request.InviteOrganization.SmSeats -
request.OccupiedSmSeats -
request.Invites.Count(x => x.AccessSecretsManager) ?? 0);
}
}

View File

@ -77,13 +77,13 @@ public class PasswordManagerSubscriptionUpdate
inviteOrganization: inviteOrganization)
{ }
public PasswordManagerSubscriptionUpdate(InviteUserOrganizationValidationRequest validationRequest) :
public PasswordManagerSubscriptionUpdate(InviteOrganizationUsersValidationRequest usersValidationRequest) :
this(
organizationSeats: validationRequest.InviteOrganization.Seats,
organizationAutoScaleSeatLimit: validationRequest.InviteOrganization.MaxAutoScaleSeats,
currentSeats: validationRequest.OccupiedPmSeats,
newUsersToAdd: validationRequest.Invites.Length,
plan: validationRequest.InviteOrganization.Plan.PasswordManager,
inviteOrganization: validationRequest.InviteOrganization)
organizationSeats: usersValidationRequest.InviteOrganization.Seats,
organizationAutoScaleSeatLimit: usersValidationRequest.InviteOrganization.MaxAutoScaleSeats,
currentSeats: usersValidationRequest.OccupiedPmSeats,
newUsersToAdd: usersValidationRequest.Invites.Length,
plan: usersValidationRequest.InviteOrganization.Plan.PasswordManager,
inviteOrganization: usersValidationRequest.InviteOrganization)
{ }
}

View File

@ -183,7 +183,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IInviteOrganizationUsersCommand, InviteOrganizationUsersCommand>();
services.AddScoped<ISendOrganizationInvitesCommand, SendOrganizationInvitesCommand>();
services.AddScoped<IInviteUsersValidator, InviteUsersValidator>();
services.AddScoped<IInviteUsersValidator, InviteOrganizationUsersValidator>();
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();
services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>();
services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>();

View File

@ -8,7 +8,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.Invi
public static class InviteUserOrganizationValidationRequestHelpers
{
public static InviteUserOrganizationValidationRequest GetInviteValidationRequestMock(InviteOrganizationUsersRequest request,
public static InviteOrganizationUsersValidationRequest GetInviteValidationRequestMock(InviteOrganizationUsersRequest request,
InviteOrganization inviteOrganization, Organization organization) =>
new()
{
@ -23,7 +23,7 @@ public static class InviteUserOrganizationValidationRequestHelpers
.AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager))
};
public static InviteUserOrganizationValidationRequest WithPasswordManagerUpdate(this InviteUserOrganizationValidationRequest request, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate) =>
public static InviteOrganizationUsersValidationRequest WithPasswordManagerUpdate(this InviteOrganizationUsersValidationRequest request, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate) =>
new()
{
Invites = request.Invites,
@ -36,7 +36,7 @@ public static class InviteUserOrganizationValidationRequestHelpers
SecretsManagerSubscriptionUpdate = request.SecretsManagerSubscriptionUpdate
};
public static InviteUserOrganizationValidationRequest WithSecretsManagerUpdate(this InviteUserOrganizationValidationRequest request, SecretsManagerSubscriptionUpdate secretsManagerSubscriptionUpdate) =>
public static InviteOrganizationUsersValidationRequest WithSecretsManagerUpdate(this InviteOrganizationUsersValidationRequest request, SecretsManagerSubscriptionUpdate secretsManagerSubscriptionUpdate) =>
new()
{
Invites = request.Invites,

View File

@ -72,8 +72,8 @@ public class InviteOrganizationUserCommandTests
.Returns([user.Email]);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));
// Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -134,8 +134,8 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));
// Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -198,9 +198,9 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Invalid<InviteUserOrganizationValidationRequest>(
new Error<InviteUserOrganizationValidationRequest>(errorMessage, validationRequest)));
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Invalid<InviteOrganizationUsersValidationRequest>(
new Error<InviteOrganizationUsersValidationRequest>(errorMessage, validationRequest)));
// Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
@ -268,8 +268,8 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithPasswordManagerUpdate(new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
// Act
@ -338,8 +338,8 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithPasswordManagerUpdate(passwordManagerUpdate)));
// Act
@ -414,8 +414,8 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));
// Act
@ -486,8 +486,8 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)
.WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));
@ -581,8 +581,8 @@ public class InviteOrganizationUserCommandTests
.Returns(organization);
sutProvider.GetDependency<IInviteUsersValidator>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Valid<InviteUserOrganizationValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
.WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)
.WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));

View File

@ -0,0 +1,66 @@
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.Billing.Models.StaticStore.Plans;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
[SutProviderCustomize]
public class InviteOrganizationUsersValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_WhenOrganizationHasSecretsManagerInvites_ThenShouldCorrectlyCalculateSeatsToAdd(
Organization organization,
SutProvider<InviteOrganizationUsersValidator> sutProvider
)
{
organization.Seats = null;
organization.SmSeats = 10;
organization.UseSecretsManager = true;
var request = new InviteOrganizationUsersValidationRequest
{
Invites =
[
new OrganizationUserInvite(
email: "test@email.com",
externalId: "test-external-id"),
new OrganizationUserInvite(
email: "test2@email.com",
externalId: "test-external-id2"),
new OrganizationUserInvite(
email: "test3@email.com",
externalId: "test-external-id3")
],
InviteOrganization = new InviteOrganization(organization, new Enterprise2023Plan(true)),
OccupiedPmSeats = 0,
OccupiedSmSeats = 9
};
sutProvider.GetDependency<IPaymentService>()
.HasSecretsManagerStandalone(request.InviteOrganization)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
_ = await sutProvider.Sut.ValidateAsync(request);
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()
.Received(1)
.ValidateUpdateAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(x =>
x.SmSeatsChanged == true && x.SmSeats == 12));
}
}