1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 12:43:14 +00:00

[PM-22692] Fix Secrets Manager Seat and ServiceAccount Limit Bug (#6138)

* test: add new test harnesses

* feat: update autoscale limit logic for SM Subscription Command

* fix: remove redundant helper methods

* fix: add periods to second sentence of templates
This commit is contained in:
Stephon Brown
2025-08-01 14:40:43 -04:00
committed by GitHub
parent 5485c12445
commit 2908ddb759
5 changed files with 193 additions and 33 deletions

View File

@@ -6,7 +6,7 @@
<td class="content-block"
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
valign="top">
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created.
</td>
</tr>
<tr

View File

@@ -1,5 +1,5 @@
{{#>BasicTextLayout}}
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created.
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
{{/BasicTextLayout}}

View File

@@ -50,11 +50,7 @@ public class SecretsManagerSubscriptionUpdate
public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats;
public bool MaxAutoscaleSmServiceAccountsChanged =>
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats;
public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue &&
MaxAutoscaleSmServiceAccounts.HasValue &&
SmServiceAccounts == MaxAutoscaleSmServiceAccounts;
public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling)
{

View File

@@ -55,15 +55,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
await FinalizeSubscriptionAdjustmentAsync(update);
if (update.SmSeatAutoscaleLimitReached)
{
await SendSeatLimitEmailAsync(update.Organization);
}
if (update.SmServiceAccountAutoscaleLimitReached)
{
await SendServiceAccountLimitEmailAsync(update.Organization);
}
await ValidateAutoScaleLimitsAsync(update);
}
private async Task FinalizeSubscriptionAdjustmentAsync(SecretsManagerSubscriptionUpdate update)
@@ -100,7 +92,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
OrganizationUserType.Owner))
.Select(u => u.Email).Distinct();
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value, ownerEmails);
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats!.Value, ownerEmails);
}
catch (Exception e)
@@ -117,7 +109,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
OrganizationUserType.Owner))
.Select(u => u.Email).Distinct();
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, ownerEmails);
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts!.Value, ownerEmails);
}
catch (Exception e)
@@ -197,7 +189,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
throw new BadRequestException("Organization has no Secrets Manager seat limit, no need to adjust seats");
}
if (update.Autoscaling && update.SmSeats.Value < organization.SmSeats.Value)
if (update.Autoscaling && update.SmSeats!.Value < organization.SmSeats.Value)
{
throw new BadRequestException("Cannot use autoscaling to subtract seats.");
}
@@ -211,7 +203,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
// Check autoscale maximum seats
if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats.Value > update.MaxAutoscaleSmSeats.Value)
if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats!.Value > update.MaxAutoscaleSmSeats.Value)
{
var message = update.Autoscaling
? "Secrets Manager seat limit has been reached."
@@ -220,7 +212,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
// Check minimum seats included with plan
if (plan.SecretsManager.BaseSeats > update.SmSeats.Value)
if (plan.SecretsManager.BaseSeats > update.SmSeats!.Value)
{
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseSeats} Secrets Manager seats.");
}
@@ -260,7 +252,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
throw new BadRequestException("Organization has no machine accounts limit, no need to adjust machine accounts");
}
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
if (update.Autoscaling && update.SmServiceAccounts!.Value < organization.SmServiceAccounts.Value)
{
throw new BadRequestException("Cannot use autoscaling to subtract machine accounts.");
}
@@ -276,7 +268,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
// Check autoscale maximum service accounts
if (update.MaxAutoscaleSmServiceAccounts.HasValue &&
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
update.SmServiceAccounts!.Value > update.MaxAutoscaleSmServiceAccounts.Value)
{
var message = update.Autoscaling
? "Secrets Manager machine account limit has been reached."
@@ -285,7 +277,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
// Check minimum service accounts included with plan
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value)
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts!.Value)
{
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} machine accounts.");
}
@@ -379,4 +371,55 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);
}
}
private async Task ValidateAutoScaleLimitsAsync(SecretsManagerSubscriptionUpdate update)
{
var (smSeatAutoScaleLimitReached, smServiceAccountsLimitReached) = await AreAutoscaleLimitsReachedAsync(update);
if (smSeatAutoScaleLimitReached)
{
await SendSeatLimitEmailAsync(update.Organization);
}
if (smServiceAccountsLimitReached)
{
await SendServiceAccountLimitEmailAsync(update.Organization);
}
}
private async Task<(bool, bool)> AreAutoscaleLimitsReachedAsync(SecretsManagerSubscriptionUpdate update)
{
var smSeatAutoScaleLimitReached = false;
var smServiceAccountsLimitReached = false;
var (occupiedSmSeats, occupiedSmServiceAccounts) = await GetOccupiedSmSeatsAndServiceAccountsAsync(update.Organization.Id);
if (occupiedSmSeats > 0
&& update.MaxAutoscaleSmSeats is not null
&& occupiedSmSeats == update.MaxAutoscaleSmSeats!.Value)
{
smSeatAutoScaleLimitReached = true;
}
if (occupiedSmServiceAccounts > 0
&& update.MaxAutoscaleSmServiceAccounts is not null
&& occupiedSmServiceAccounts == update.MaxAutoscaleSmServiceAccounts!.Value)
{
smServiceAccountsLimitReached = true;
}
return (smSeatAutoScaleLimitReached, smServiceAccountsLimitReached);
}
/// <summary>
/// Requests the number of Secret Manager seats and service accounts are currently used by the organization
/// </summary>
/// <param name="organizationId"> The id of the organization</param>
/// <returns > A tuple containing the occupied seats and the occupied service account counts</returns>
private async Task<(int, int)> GetOccupiedSmSeatsAndServiceAccountsAsync(Guid organizationId)
{
var occupiedSmSeatsTask = _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organizationId);
var occupiedServiceAccountsTask = _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organizationId);
return (await occupiedSmSeatsTask, await occupiedServiceAccountsTask);
}
}