1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-14 09:38:16 -05:00

Added Secrets Manager Validations and Tests.

This commit is contained in:
jrmccannon 2025-02-13 16:23:00 -06:00
parent bf8d6fb3ba
commit fcaa449f83
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
7 changed files with 190 additions and 74 deletions

View File

@ -18,4 +18,5 @@ public static class InviteUserValidationErrorMessages
public const string OrganizationNoSecretsManager = "Organization has no access to Secrets Manager";
public const string SecretsManagerSeatLimitReached = "Secrets Manager seat limit has been reached.";
public const string SecretsManagerCannotExceedPasswordManager = "You cannot have more Secrets Manager seats than Password Manager seats.";
public const string SecretsManagerAdditionalSeatLimitReached = "You have reached the maximum number of Secrets Manager seats ({0}) for this plan.";
}

View File

@ -23,25 +23,34 @@ public class PasswordManagerSubscriptionUpdate
public int? UpdatedSeatTotal => Seats + SeatsRequiredToAdd;
public Plan Plan { get; }
public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; }
private PasswordManagerSubscriptionUpdate(int? organizationSeats, int? organizationAutoScaleSeatLimit, int currentSeats, int seatsToAdd, Plan plan)
private PasswordManagerSubscriptionUpdate(int? organizationSeats,
int? organizationAutoScaleSeatLimit,
int currentSeats,
int seatsToAdd,
Plan.PasswordManagerPlanFeatures plan)
{
Seats = organizationSeats;
MaxAutoScaleSeats = organizationAutoScaleSeatLimit;
OccupiedSeats = currentSeats;
AdditionalSeats = seatsToAdd;
Plan = plan;
PasswordManagerPlan = plan;
}
public static PasswordManagerSubscriptionUpdate Create(OrganizationDto organizationDto, int occupiedSeats, int seatsToAdd)
{
return new PasswordManagerSubscriptionUpdate(organizationDto.Seats, organizationDto.MaxAutoScaleSeats, occupiedSeats, seatsToAdd, organizationDto.Plan);
return new PasswordManagerSubscriptionUpdate(
organizationDto.Seats,
organizationDto.MaxAutoScaleSeats,
occupiedSeats,
seatsToAdd,
organizationDto.Plan.PasswordManager);
}
public static PasswordManagerSubscriptionUpdate Create(InviteUserOrganizationValidationRequest refined)
{
return new PasswordManagerSubscriptionUpdate(refined.Organization.Seats, refined.Organization.MaxAutoScaleSeats,
refined.OccupiedPmSeats, refined.Invites.Length, refined.Organization.Plan);
refined.OccupiedPmSeats, refined.Invites.Length, refined.Organization.Plan.PasswordManager);
}
}

View File

@ -1,35 +1,46 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Models.StaticStore;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
public class SecretsManagerSubscriptionUpdate
{
public bool UseSecretsManger { get; private init; }
public int? Seats { get; private init; }
public int? MaxAutoScaleSeats { get; private init; }
public int OccupiedSeats { get; private init; }
public int AdditionalSeats { get; private init; }
public PasswordManagerSubscriptionUpdate PasswordManagerSubscriptionUpdate { get; private init; }
public bool UseSecretsManger { get; }
public int? Seats { get; }
public int? MaxAutoScaleSeats { get; }
public int OccupiedSeats { get; }
public int AdditionalSeats { get; }
public int? PasswordManagerUpdatedSeatTotal { get; }
public Plan.SecretsManagerPlanFeatures SecretsManagerPlan { get; }
public int? AvailableSeats => Seats - OccupiedSeats;
public int SeatsRequiredToAdd => AdditionalSeats - AvailableSeats ?? 0;
public int? UpdatedSeatTotal => Seats + SeatsRequiredToAdd;
private SecretsManagerSubscriptionUpdate(bool useSecretsManger, int? organizationSeats,
int? organizationAutoScaleSeatLimit, int currentSeats, int seatsToAdd, PasswordManagerSubscriptionUpdate passwordManagerSeats)
private SecretsManagerSubscriptionUpdate(bool useSecretsManger,
int? organizationSeats,
int? organizationAutoScaleSeatLimit,
int currentSeats,
int seatsToAdd,
int? passwordManagerUpdatedSeatTotal,
Plan.SecretsManagerPlanFeatures plan)
{
UseSecretsManger = useSecretsManger;
Seats = organizationSeats;
MaxAutoScaleSeats = organizationAutoScaleSeatLimit;
OccupiedSeats = currentSeats;
AdditionalSeats = seatsToAdd;
PasswordManagerSubscriptionUpdate = passwordManagerSeats;
PasswordManagerUpdatedSeatTotal = passwordManagerUpdatedSeatTotal;
SecretsManagerPlan = plan;
}
public static SecretsManagerSubscriptionUpdate Create(InviteUserOrganizationValidationRequest refined, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate)
{
return new SecretsManagerSubscriptionUpdate(refined.Organization.UseSecretsManager,
refined.Organization.SmSeats, refined.Organization.SmMaxAutoScaleSeats,
refined.OccupiedPmSeats, refined.Invites.Count(x => x.AccessSecretsManager),
passwordManagerSubscriptionUpdate);
refined.Organization.SmSeats,
refined.Organization.SmMaxAutoScaleSeats,
refined.OccupiedSmSeats,
refined.Invites.Count(x => x.AccessSecretsManager),
passwordManagerSubscriptionUpdate.UpdatedSeatTotal,
refined.Organization.Plan.SecretsManager);
}
}

View File

@ -25,16 +25,16 @@ public static class PasswordManagerInviteUserValidation
return new Invalid<PasswordManagerSubscriptionUpdate>(SeatLimitHasBeenReachedError);
}
if (subscriptionUpdate.Plan.PasswordManager.HasAdditionalSeatsOption is false)
if (subscriptionUpdate.PasswordManagerPlan.HasAdditionalSeatsOption is false)
{
return new Invalid<PasswordManagerSubscriptionUpdate>(PlanDoesNotAllowAdditionalSeats);
}
// Apparently MaxAdditionalSeats is never set. Can probably be removed.
if (subscriptionUpdate.AdditionalSeats > subscriptionUpdate.Plan.PasswordManager.MaxAdditionalSeats)
if (subscriptionUpdate.AdditionalSeats > subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats)
{
return new Invalid<PasswordManagerSubscriptionUpdate>(string.Format(PlanOnlyAllowsMaxAdditionalSeats,
subscriptionUpdate.Plan.PasswordManager.MaxAdditionalSeats));
subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats));
}
return new Valid<PasswordManagerSubscriptionUpdate>(subscriptionUpdate);

View File

@ -3,12 +3,12 @@ using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Invite
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
public class SecretsManagerInviteUserValidation
public static class SecretsManagerInviteUserValidation
{
// Do we need to check if they are attempting to subtract seats? (no I don't think so because this is for inviting a User)
// NOTE This is only validating adding new users
public static ValidationResult<SecretsManagerSubscriptionUpdate> Validate(SecretsManagerSubscriptionUpdate subscriptionUpdate)
{
if (subscriptionUpdate.UseSecretsManger)
if (subscriptionUpdate.UseSecretsManger is false)
{
return new Invalid<SecretsManagerSubscriptionUpdate>(OrganizationNoSecretsManager);
}
@ -18,63 +18,24 @@ public class SecretsManagerInviteUserValidation
return new Valid<SecretsManagerSubscriptionUpdate>(subscriptionUpdate); // no need to adjust seats...continue on
}
// if (update.Autoscaling && update.SmSeats.Value < organization.SmSeats.Value)
// {
// throw new BadRequestException("Cannot use autoscaling to subtract seats.");
// }
// 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<SecretsManagerSubscriptionUpdate>(
string.Format(SecretsManagerAdditionalSeatLimitReached,
subscriptionUpdate.SecretsManagerPlan.BaseSeats +
subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault()));
}
// Might need to check plan
// Check plan maximum seats
// if (!plan.SecretsManager.HasAdditionalSeatsOption ||
// (plan.SecretsManager.MaxAdditionalSeats.HasValue && update.SmSeatsExcludingBase > plan.SecretsManager.MaxAdditionalSeats.Value))
// {
// var planMaxSeats = plan.SecretsManager.BaseSeats + plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault();
// throw new BadRequestException($"You have reached the maximum number of Secrets Manager seats ({planMaxSeats}) for this plan.");
// }
// Check autoscale maximum seats
if (subscriptionUpdate.UpdatedSeatTotal is not null && subscriptionUpdate.MaxAutoScaleSeats is not null &&
subscriptionUpdate.UpdatedSeatTotal > subscriptionUpdate.MaxAutoScaleSeats)
{
return new Invalid<SecretsManagerSubscriptionUpdate>(SecretsManagerSeatLimitReached);
}
// if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats.Value > update.MaxAutoscaleSmSeats.Value)
// {
// var message = update.Autoscaling
// ? "Secrets Manager seat limit has been reached."
// : "Cannot set max seat autoscaling below seat count.";
// throw new BadRequestException(message);
// }
// Inviting a user... this shouldn't matter
//
// Check minimum seats included with plan
// if (plan.SecretsManager.BaseSeats > update.SmSeats.Value)
// {
// throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseSeats} Secrets Manager seats.");
// }
// Check minimum seats required by business logic
// if (update.SmSeats.Value <= 0)
// {
// throw new BadRequestException("You must have at least 1 Secrets Manager seat.");
// }
// Check minimum seats currently in use by the organization
// if (organization.SmSeats.Value > update.SmSeats.Value)
// {
// var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
// if (occupiedSeats > update.SmSeats.Value)
// {
// throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " +
// "You cannot decrease your subscription below your current occupied seat count.");
// }
// }
// Check that SM seats aren't greater than password manager seats
if (subscriptionUpdate.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal < subscriptionUpdate.UpdatedSeatTotal)
if (subscriptionUpdate.PasswordManagerUpdatedSeatTotal < subscriptionUpdate.UpdatedSeatTotal)
{
return new Invalid<SecretsManagerSubscriptionUpdate>(SecretsManagerCannotExceedPasswordManager);
}

View File

@ -50,6 +50,7 @@ public class PasswordManagerInviteUserValidationTests
{
organization.Seats = 4;
organization.MaxAutoscaleSeats = 4;
organization.PlanType = PlanType.EnterpriseAnnually;
var seatsOccupiedByUsers = 4;
var additionalSeats = 1;
@ -81,5 +82,4 @@ public class PasswordManagerInviteUserValidationTests
Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.PlanDoesNotAllowAdditionalSeats, result.ErrorMessageString);
}
}

View File

@ -0,0 +1,134 @@
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.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
[SutProviderCustomize]
public class SecretsManagerInviteUserValidationTests
{
[Theory]
[BitAutoData]
public void Validate_GivenOrganizationDoesNotHaveSecretsManager_ThenShouldNotBeAllowedToAddSecretsManagerUsers(
Organization organization)
{
organization.UseSecretsManager = false;
var organizationDto = OrganizationDto.FromOrganization(organization);
var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0);
var request = new InviteUserOrganizationValidationRequest
{
Invites = [new OrganizationUserInviteDto()],
Organization = organizationDto,
PerformedBy = Guid.Empty,
PerformedAt = default,
OccupiedPmSeats = 0,
OccupiedSmSeats = 0
};
var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate);
var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.OrganizationNoSecretsManager, result.ErrorMessageString);
}
[Theory]
[BitAutoData]
public void Validate_GivenOrganizationHasSecretsManagerWithoutASeatLimit_ThenShouldBeAllowedToAddSecretsManagerUsers(
Organization organization)
{
organization.SmSeats = null;
organization.UseSecretsManager = true;
var organizationDto = OrganizationDto.FromOrganization(organization);
var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0);
var request = new InviteUserOrganizationValidationRequest
{
Invites = [new OrganizationUserInviteDto()],
Organization = organizationDto,
PerformedBy = Guid.Empty,
PerformedAt = default,
OccupiedPmSeats = 0,
OccupiedSmSeats = 0
};
var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate);
var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Valid<SecretsManagerSubscriptionUpdate>>(result);
}
[Theory]
[BitAutoData]
public void Validate_GivenOrganizationPlanDoesNotAllowAdditionalSeats_ThenShouldNotBeAllowedToAddSecretsManagerUsers(
Organization organization)
{
organization.SmSeats = 4;
organization.MaxAutoscaleSmSeats = 4;
organization.UseSecretsManager = true;
organization.PlanType = PlanType.EnterpriseAnnually;
var organizationDto = OrganizationDto.FromOrganization(organization);
var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0);
var request = new InviteUserOrganizationValidationRequest
{
Invites = [OrganizationUserInviteDto.Create("email@test.com", OrganizationUserInvite.Create(["email@test.com"], [], OrganizationUserType.User, new Permissions(), string.Empty, true))],
Organization = organizationDto,
PerformedBy = Guid.Empty,
PerformedAt = default,
OccupiedPmSeats = 0,
OccupiedSmSeats = 4
};
var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate);
var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.SecretsManagerSeatLimitReached, result.ErrorMessageString);
}
[Theory]
[BitAutoData]
public void Validate_GivenPasswordManagerSeatsAreTheSameAsSecretsManagerSeats_WhenAttemptingToAddASecretManagerSeatOnly_ThenShouldNotBeAllowedToAddSecretsManagerUsers(
Organization organization)
{
organization.SmSeats = 4;
organization.MaxAutoscaleSmSeats = 5;
organization.UseSecretsManager = true;
organization.PlanType = PlanType.EnterpriseAnnually;
var organizationDto = OrganizationDto.FromOrganization(organization);
var subscriptionUpdate = PasswordManagerSubscriptionUpdate.Create(organizationDto, 0, 0);
var request = new InviteUserOrganizationValidationRequest
{
Invites = [OrganizationUserInviteDto.Create("email@test.com", OrganizationUserInvite.Create(["email@test.com"], [], OrganizationUserType.User, new Permissions(), string.Empty, true))],
Organization = organizationDto,
PerformedBy = Guid.Empty,
PerformedAt = default,
OccupiedPmSeats = 0,
OccupiedSmSeats = 4
};
var update = SecretsManagerSubscriptionUpdate.Create(request, subscriptionUpdate);
var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.SecretsManagerCannotExceedPasswordManager, result.ErrorMessageString);
}
}