1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-05 03:32:21 -05:00

Resolved an issue where autoscaling always happened (#5765)

This commit is contained in:
Conner Turnbull 2025-05-02 12:53:06 -04:00 committed by GitHub
parent cd3f16948b
commit 077d0fa6d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 145 additions and 4 deletions

View File

@ -49,6 +49,7 @@ public interface IOrganizationService
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId); IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);

View File

@ -1058,7 +1058,7 @@ public class OrganizationService : IOrganizationService
organization: organization, organization: organization,
initOrganization: initOrganization)); initOrganization: initOrganization));
internal async Task<(bool canScale, string failureReason)> CanScaleAsync( public async Task<(bool canScale, string failureReason)> CanScaleAsync(
Organization organization, Organization organization,
int seatsToAdd) int seatsToAdd)
{ {

View File

@ -15,7 +15,8 @@ public class CreateSponsorshipCommand(
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IUserService userService, IUserService userService,
IOrganizationService organizationService) : ICreateSponsorshipCommand IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand
{ {
public async Task<OrganizationSponsorship> CreateSponsorshipAsync( public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
Organization sponsoringOrganization, Organization sponsoringOrganization,
@ -82,14 +83,26 @@ public class CreateSponsorshipCommand(
if (existingOrgSponsorship != null) if (existingOrgSponsorship != null)
{ {
// Replace existing invalid offer with our new sponsorship offer
sponsorship.Id = existingOrgSponsorship.Id; sponsorship.Id = existingOrgSponsorship.Id;
} }
} }
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue) if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
{ {
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1); var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats;
if (availableSeats <= 0)
{
var newSeatsRequired = 1;
var (canScale, failureReason) = await organizationService.CanScaleAsync(sponsoringOrganization, newSeatsRequired);
if (!canScale)
{
throw new BadRequestException(failureReason);
}
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, newSeatsRequired);
}
} }
try try

View File

@ -168,6 +168,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
}); });
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);
// Setup for checking available seats
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
.Returns(0);
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null);
@ -293,6 +298,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
{ {
sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;
sponsoringOrg.UseAdminSponsoredFamilies = true; sponsoringOrg.UseAdminSponsoredFamilies = true;
sponsoringOrg.Seats = 10;
sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);
@ -311,6 +317,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
} }
]); ]);
// Setup for checking available seats - organization has plenty of seats
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
.Returns(5);
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
@ -331,5 +342,121 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1) await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
.CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship))); .CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
// Verify we didn't need to add seats
await sutProvider.GetDependency<IOrganizationService>().DidNotReceive()
.AutoAddSeatsAsync(Arg.Any<Organization>(), Arg.Any<int>());
}
[Theory]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_AutoscalesWhenNeeded(
OrganizationUserType organizationUserType,
Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,
string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider<CreateSponsorshipCommand> sutProvider)
{
sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;
sponsoringOrg.UseAdminSponsoredFamilies = true;
sponsoringOrg.Seats = 10;
sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>
{
var sponsorship = callInfo.Arg<OrganizationSponsorship>();
sponsorship.Id = sponsorshipId;
});
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([
new()
{
Id = sponsoringOrg.Id,
Permissions = new Permissions { ManageUsers = true },
Type = organizationUserType
}
]);
// Setup for checking available seats - organization has no available seats
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
.Returns(10);
// Setup for checking if can scale
sutProvider.GetDependency<IOrganizationService>()
.CanScaleAsync(sponsoringOrg, 1)
.Returns((true, ""));
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
var expectedSponsorship = new OrganizationSponsorship
{
Id = sponsorshipId,
SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = sponsoringOrgUser.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail,
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
IsAdminInitiated = true,
Notes = notes
};
Assert.True(SponsorshipValidator(expectedSponsorship, actual));
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
.CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
// Verify we needed to add seats
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.AutoAddSeatsAsync(sponsoringOrg, 1);
}
[Theory]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_ThrowsWhenCannotAutoscale(
OrganizationUserType organizationUserType,
Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,
string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider<CreateSponsorshipCommand> sutProvider)
{
sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;
sponsoringOrg.UseAdminSponsoredFamilies = true;
sponsoringOrg.Seats = 10;
sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>
{
var sponsorship = callInfo.Arg<OrganizationSponsorship>();
sponsorship.Id = sponsorshipId;
});
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([
new()
{
Id = sponsoringOrg.Id,
Permissions = new Permissions { ManageUsers = true },
Type = organizationUserType
}
]);
// Setup for checking available seats - organization has no available seats
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
.Returns(10);
// Setup for checking if can scale - cannot scale
var failureReason = "Seat limit has been reached.";
sutProvider.GetDependency<IOrganizationService>()
.CanScaleAsync(sponsoringOrg, 1)
.Returns((false, failureReason));
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes));
Assert.Equal(failureReason, exception.Message);
} }
} }