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:
parent
cd3f16948b
commit
077d0fa6d7
@ -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);
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user