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

Refactored to use new ValidationResult pattern. added mapping method.

This commit is contained in:
jrmccannon 2025-03-17 13:27:37 -05:00
parent 1620fecc70
commit 59b579f071
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
29 changed files with 226 additions and 108 deletions

View File

@ -1,3 +1,8 @@
namespace Bit.Core.AdminConsole.Errors; namespace Bit.Core.AdminConsole.Errors;
public record Error<T>(string Message, T ErroredValue); public record Error<T>(string Message, T ErroredValue);
public static class ErrorMappers
{
public static Error<B> ToError<A, B>(this Error<A> errorA, B erroredValue) => new(errorA.Message, erroredValue);
}

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;

View File

@ -0,0 +1,9 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
public record CannotAutoScaleOnSelfHostError(IGlobalSettings InvalidSettings) : Error<IGlobalSettings>(Code, InvalidSettings)
{
public const string Code = "CannotAutoScaleOnSelfHost";
}

View File

@ -1,20 +1,21 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.InviteUserValidationErrorMessages; using SecretsManagerSubscriptionUpdate = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager.SecretsManagerSubscriptionUpdate;
using SecretsManagerSubscriptionUpdate = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models.SecretsManagerSubscriptionUpdate;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
// TODO move into own file ... and change name to validator public interface IInviteUsersValidation : IValidator<InviteUserOrganizationValidationRequest>;
public interface IInviteUsersValidation
{
Task<ValidationResult<InviteUserOrganizationValidationRequest>> ValidateAsync(InviteUserOrganizationValidationRequest request);
}
public class InviteUsersValidation( public class InviteUsersValidation(
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
@ -26,14 +27,14 @@ public class InviteUsersValidation(
{ {
if (ValidateEnvironment(globalSettings) is Invalid<IGlobalSettings> invalidEnvironment) if (ValidateEnvironment(globalSettings) is Invalid<IGlobalSettings> invalidEnvironment)
{ {
return new Invalid<InviteUserOrganizationValidationRequest>(invalidEnvironment.ErrorMessageString); return invalidEnvironment.Map(request);
} }
var organizationValidationResult = InvitingUserOrganizationValidation.Validate(request.InviteOrganization); var organizationValidationResult = InvitingUserOrganizationValidation.Validate(request.InviteOrganization);
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation) if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
{ {
return new Invalid<InviteUserOrganizationValidationRequest>(organizationValidation.ErrorMessageString); return organizationValidation.Map(request);
} }
var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(request); var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(request);
@ -41,7 +42,7 @@ public class InviteUsersValidation(
if (passwordManagerValidationResult is Invalid<PasswordManagerSubscriptionUpdate> invalidSubscriptionUpdate) if (passwordManagerValidationResult is Invalid<PasswordManagerSubscriptionUpdate> invalidSubscriptionUpdate)
{ {
return new Invalid<InviteUserOrganizationValidationRequest>(invalidSubscriptionUpdate.ErrorMessageString); return invalidSubscriptionUpdate.Map(request);
} }
var smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(request, subscriptionUpdate); var smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(request, subscriptionUpdate);
@ -49,7 +50,7 @@ public class InviteUsersValidation(
if (secretsManagerValidationResult is Invalid<SecretsManagerSubscriptionUpdate> invalidSmSubscriptionUpdate) if (secretsManagerValidationResult is Invalid<SecretsManagerSubscriptionUpdate> invalidSmSubscriptionUpdate)
{ {
return new Invalid<InviteUserOrganizationValidationRequest>(invalidSmSubscriptionUpdate.ErrorMessageString); return invalidSmSubscriptionUpdate.Map(request);
} }
var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId); var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId);
@ -59,16 +60,19 @@ public class InviteUsersValidation(
if (providerValidationResult is Invalid<ProviderDto> invalidProviderValidation) if (providerValidationResult is Invalid<ProviderDto> invalidProviderValidation)
{ {
return new Invalid<InviteUserOrganizationValidationRequest>(invalidProviderValidation.ErrorMessageString); return invalidProviderValidation.Map(request);
} }
} }
var paymentSubscription = await paymentService.GetSubscriptionAsync(await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId)); var paymentSubscription = await paymentService.GetSubscriptionAsync(
var paymentValidationResult = InviteUserPaymentValidation.Validate(new PaymentsSubscription(paymentSubscription, request.InviteOrganization)); await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId));
var paymentValidationResult = InviteUserPaymentValidation.Validate(
new PaymentsSubscription(paymentSubscription, request.InviteOrganization));
if (paymentValidationResult is Invalid<PaymentsSubscription> invalidPaymentValidation) if (paymentValidationResult is Invalid<PaymentsSubscription> invalidPaymentValidation)
{ {
return new Invalid<InviteUserOrganizationValidationRequest>(invalidPaymentValidation.ErrorMessageString); return invalidPaymentValidation.Map(request);
} }
return new Valid<InviteUserOrganizationValidationRequest>(new InviteUserOrganizationValidationRequest( return new Valid<InviteUserOrganizationValidationRequest>(new InviteUserOrganizationValidationRequest(
@ -79,6 +83,6 @@ public class InviteUsersValidation(
public static ValidationResult<IGlobalSettings> ValidateEnvironment(IGlobalSettings globalSettings) => public static ValidationResult<IGlobalSettings> ValidateEnvironment(IGlobalSettings globalSettings) =>
globalSettings.SelfHosted globalSettings.SelfHosted
? new Invalid<IGlobalSettings>(CannotAutoScaleOnSelfHostedError) ? new Invalid<IGlobalSettings>(new CannotAutoScaleOnSelfHostError(globalSettings))
: new Valid<IGlobalSettings>(globalSettings); : new Valid<IGlobalSettings>(globalSettings);
} }

View File

@ -1,22 +0,0 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
public static class InviteUserValidationErrorMessages
{
public const string CannotAutoScaleOnSelfHostedError = "Cannot autoscale on self-hosted instance.";
public const string ProviderBillableSeatLimitError = "Seat limit has been reached. Please contact your provider to add more seats.";
public const string ProviderResellerSeatLimitError = "Seat limit has been reached. Contact your provider to purchase additional seats.";
public const string CancelledSubscriptionError = "Cannot autoscale with a canceled subscription.";
public const string NoPaymentMethodFoundError = "No payment method found.";
public const string NoSubscriptionFoundError = "No subscription found.";
// Password Manger Invite Users Error Messages
public const string SeatLimitHasBeenReachedError = "Seat limit has been reached.";
public const string PlanDoesNotAllowAdditionalSeats = "Plan does not allow additional seats.";
public const string PlanOnlyAllowsMaxAdditionalSeats = "Organization plan allows a maximum of {0} additional seats.";
// Secrets Manager Invite Users Error Messages
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

@ -0,0 +1,16 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Models.Business;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
public record OrganizationNoPaymentMethodFoundError(InviteOrganization InvalidRequest)
: Error<InviteOrganization>(Code, InvalidRequest)
{
public const string Code = "No payment method found.";
}
public record OrganizationNoSubscriptionFoundError(InviteOrganization InvalidRequest)
: Error<InviteOrganization>(Code, InvalidRequest)
{
public const string Code = "No subscription found.";
}

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.InviteUserValidationErrorMessages; using Bit.Core.AdminConsole.Shared.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
public static class InvitingUserOrganizationValidation public static class InvitingUserOrganizationValidation
{ {
@ -14,12 +14,12 @@ public static class InvitingUserOrganizationValidation
if (string.IsNullOrWhiteSpace(inviteOrganization.GatewayCustomerId)) if (string.IsNullOrWhiteSpace(inviteOrganization.GatewayCustomerId))
{ {
return new Invalid<InviteOrganization>(NoPaymentMethodFoundError); return new Invalid<InviteOrganization>(new OrganizationNoPaymentMethodFoundError(inviteOrganization));
} }
if (string.IsNullOrWhiteSpace(inviteOrganization.GatewaySubscriptionId)) if (string.IsNullOrWhiteSpace(inviteOrganization.GatewaySubscriptionId))
{ {
return new Invalid<InviteOrganization>(NoSubscriptionFoundError); return new Invalid<InviteOrganization>(new OrganizationNoSubscriptionFoundError(inviteOrganization));
} }
return new Valid<InviteOrganization>(inviteOrganization); return new Valid<InviteOrganization>(inviteOrganization);

View File

@ -0,0 +1,24 @@
using Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
public record PasswordManagerSeatLimitHasBeenReachedError(PasswordManagerSubscriptionUpdate InvalidRequest)
: Error<PasswordManagerSubscriptionUpdate>(Code, InvalidRequest)
{
public const string Code = "Seat limit has been reached.";
}
public record PasswordManagerPlanDoesNotAllowAdditionalSeatsError(PasswordManagerSubscriptionUpdate InvalidRequest)
: Error<PasswordManagerSubscriptionUpdate>(Code, InvalidRequest)
{
public const string Code = "Plan does not allow additional seats.";
}
public record PasswordManagerPlanOnlyAllowsMaxAdditionalSeatsError(PasswordManagerSubscriptionUpdate InvalidRequest)
: Error<PasswordManagerSubscriptionUpdate>(GetErrorMessage(InvalidRequest), InvalidRequest)
{
private static string GetErrorMessage(PasswordManagerSubscriptionUpdate invalidRequest) =>
string.Format(Code, invalidRequest.PasswordManagerPlan.MaxAdditionalSeats);
public const string Code = "Organization plan allows a maximum of {0} additional seats.";
}

View File

@ -1,12 +1,10 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.Shared.Validation;
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.InviteUserValidationErrorMessages;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
public static class PasswordManagerInviteUserValidation public static class PasswordManagerInviteUserValidation
{ {
// NOTE This is only for validating adding users to an organization, not removing // NOTE This is only for validating adding users to an organization, not removing
public static ValidationResult<PasswordManagerSubscriptionUpdate> Validate(PasswordManagerSubscriptionUpdate subscriptionUpdate) public static ValidationResult<PasswordManagerSubscriptionUpdate> Validate(PasswordManagerSubscriptionUpdate subscriptionUpdate)
{ {
if (subscriptionUpdate.Seats is null) if (subscriptionUpdate.Seats is null)
@ -22,19 +20,21 @@ public static class PasswordManagerInviteUserValidation
if (subscriptionUpdate.UpdatedSeatTotal is not null && subscriptionUpdate.MaxAutoScaleSeats is not null && if (subscriptionUpdate.UpdatedSeatTotal is not null && subscriptionUpdate.MaxAutoScaleSeats is not null &&
subscriptionUpdate.UpdatedSeatTotal > subscriptionUpdate.MaxAutoScaleSeats) subscriptionUpdate.UpdatedSeatTotal > subscriptionUpdate.MaxAutoScaleSeats)
{ {
return new Invalid<PasswordManagerSubscriptionUpdate>(SeatLimitHasBeenReachedError); return new Invalid<PasswordManagerSubscriptionUpdate>(
new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate));
} }
if (subscriptionUpdate.PasswordManagerPlan.HasAdditionalSeatsOption is false) if (subscriptionUpdate.PasswordManagerPlan.HasAdditionalSeatsOption is false)
{ {
return new Invalid<PasswordManagerSubscriptionUpdate>(PlanDoesNotAllowAdditionalSeats); return new Invalid<PasswordManagerSubscriptionUpdate>(
new PasswordManagerPlanDoesNotAllowAdditionalSeatsError(subscriptionUpdate));
} }
// Apparently MaxAdditionalSeats is never set. Can probably be removed. // Apparently MaxAdditionalSeats is never set. Can probably be removed.
if (subscriptionUpdate.AdditionalSeats > subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats) if (subscriptionUpdate.AdditionalSeats > subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats)
{ {
return new Invalid<PasswordManagerSubscriptionUpdate>(string.Format(PlanOnlyAllowsMaxAdditionalSeats, return new Invalid<PasswordManagerSubscriptionUpdate>(
subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats)); new PasswordManagerPlanOnlyAllowsMaxAdditionalSeatsError(subscriptionUpdate));
} }
return new Valid<PasswordManagerSubscriptionUpdate>(subscriptionUpdate); return new Valid<PasswordManagerSubscriptionUpdate>(subscriptionUpdate);

View File

@ -2,7 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
public class PasswordManagerSubscriptionUpdate public class PasswordManagerSubscriptionUpdate
{ {

View File

@ -0,0 +1,10 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
public record PaymentCancelledSubscriptionError(PaymentsSubscription InvalidRequest)
: Error<PaymentsSubscription>(Code, InvalidRequest)
{
public const string Code = "Cannot autoscale with a canceled subscription.";
}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
@ -15,7 +17,7 @@ public static class InviteUserPaymentValidation
if (subscription.SubscriptionStatus == StripeConstants.SubscriptionStatus.Canceled) if (subscription.SubscriptionStatus == StripeConstants.SubscriptionStatus.Canceled)
{ {
return new Invalid<PaymentsSubscription>(InviteUserValidationErrorMessages.CancelledSubscriptionError); return new Invalid<PaymentsSubscription>(new PaymentCancelledSubscriptionError(subscription));
} }
return new Valid<PaymentsSubscription>(subscription); return new Valid<PaymentsSubscription>(subscription);

View File

@ -0,0 +1,13 @@
using Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
public record ProviderBillableSeatLimitError(ProviderDto InvalidRequest) : Error<ProviderDto>(Code, InvalidRequest)
{
public const string Code = "Seat limit has been reached. Please contact your provider to add more seats.";
}
public record ProviderResellerSeatLimitError(ProviderDto InvalidRequest) : Error<ProviderDto>(Code, InvalidRequest)
{
public const string Code = "Seat limit has been reached. Contact your provider to purchase additional seats.";
}

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
public static class InvitingUserOrganizationProviderValidation public static class InvitingUserOrganizationProviderValidation
{ {
@ -12,12 +12,12 @@ public static class InvitingUserOrganizationProviderValidation
{ {
if (provider.IsBillable()) if (provider.IsBillable())
{ {
return new Invalid<ProviderDto>(InviteUserValidationErrorMessages.ProviderBillableSeatLimitError); return new Invalid<ProviderDto>(new ProviderBillableSeatLimitError(provider));
} }
if (provider.Type == ProviderType.Reseller) if (provider.Type == ProviderType.Reseller)
{ {
return new Invalid<ProviderDto>(InviteUserValidationErrorMessages.ProviderResellerSeatLimitError); return new Invalid<ProviderDto>(new ProviderResellerSeatLimitError(provider));
} }
} }

View File

@ -1,7 +1,6 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
public class ProviderDto public class ProviderDto
{ {
@ -10,7 +9,7 @@ public class ProviderDto
public ProviderStatusType Status { get; init; } public ProviderStatusType Status { get; init; }
public bool Enabled { get; init; } public bool Enabled { get; init; }
public static ProviderDto FromProviderEntity(Provider provider) public static ProviderDto FromProviderEntity(Entities.Provider.Provider provider)
{ {
return new ProviderDto { ProviderId = provider.Id, Type = provider.Type, Status = provider.Status, Enabled = provider.Enabled }; return new ProviderDto { ProviderId = provider.Id, Type = provider.Type, Status = provider.Status, Enabled = provider.Enabled };
} }

View File

@ -0,0 +1,32 @@
using Bit.Core.AdminConsole.Errors;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
public record OrganizationNoSecretsManagerError(SecretsManagerSubscriptionUpdate InvalidRequest)
: Error<SecretsManagerSubscriptionUpdate>(Code, InvalidRequest)
{
public const string Code = "Organization has no access to Secrets Manager";
}
public record SecretsManagerAdditionalSeatLimitReachedError(SecretsManagerSubscriptionUpdate InvalidRequest)
: Error<SecretsManagerSubscriptionUpdate>(GetErrorMessage(InvalidRequest), InvalidRequest)
{
public const string Code = "You have reached the maximum number of Secrets Manager seats ({0}) for this plan.";
public static string GetErrorMessage(SecretsManagerSubscriptionUpdate invalidRequest) =>
string.Format(Code,
invalidRequest.SecretsManagerPlan.BaseSeats +
invalidRequest.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault());
}
public record SecretsManagerSeatLimitReachedError(SecretsManagerSubscriptionUpdate InvalidRequest)
: Error<SecretsManagerSubscriptionUpdate>(Code, InvalidRequest)
{
public const string Code = "Secrets Manager seat limit has been reached.";
}
public record SecretsManagerCannotExceedPasswordManagerError(SecretsManagerSubscriptionUpdate InvalidRequest)
: Error<SecretsManagerSubscriptionUpdate>(Code, InvalidRequest)
{
public const string Code = "You cannot have more Secrets Manager seats than Password Manager seats.";
}

View File

@ -1,7 +1,6 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.Shared.Validation;
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.InviteUserValidationErrorMessages;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
public static class SecretsManagerInviteUserValidation public static class SecretsManagerInviteUserValidation
{ {
@ -10,33 +9,34 @@ public static class SecretsManagerInviteUserValidation
subscriptionUpdate switch subscriptionUpdate switch
{ {
{ UseSecretsManger: false, AdditionalSeats: > 0 } => { UseSecretsManger: false, AdditionalSeats: > 0 } =>
new Invalid<SecretsManagerSubscriptionUpdate>(OrganizationNoSecretsManager), new Invalid<SecretsManagerSubscriptionUpdate>(
new OrganizationNoSecretsManagerError(subscriptionUpdate)),
{ UseSecretsManger: false, AdditionalSeats: 0 } or { UseSecretsManger: true, Seats: null } => { UseSecretsManger: false, AdditionalSeats: 0 } or { UseSecretsManger: true, Seats: null } =>
new Valid<SecretsManagerSubscriptionUpdate>(subscriptionUpdate), new Valid<SecretsManagerSubscriptionUpdate>(subscriptionUpdate),
{ UseSecretsManger: true, SecretsManagerPlan.HasAdditionalSeatsOption: false } => { UseSecretsManger: true, SecretsManagerPlan.HasAdditionalSeatsOption: false } =>
new Invalid<SecretsManagerSubscriptionUpdate>( new Invalid<SecretsManagerSubscriptionUpdate>(
string.Format(SecretsManagerAdditionalSeatLimitReached, new SecretsManagerAdditionalSeatLimitReachedError(subscriptionUpdate)),
subscriptionUpdate.SecretsManagerPlan.BaseSeats +
subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault())),
{ UseSecretsManger: true, SecretsManagerPlan.MaxAdditionalSeats: var planMaxSeats } { UseSecretsManger: true, SecretsManagerPlan.MaxAdditionalSeats: var planMaxSeats }
when planMaxSeats < subscriptionUpdate.AdditionalSeats => when planMaxSeats < subscriptionUpdate.AdditionalSeats =>
new Invalid<SecretsManagerSubscriptionUpdate>( new Invalid<SecretsManagerSubscriptionUpdate>(
string.Format(SecretsManagerAdditionalSeatLimitReached, new SecretsManagerAdditionalSeatLimitReachedError(subscriptionUpdate)),
subscriptionUpdate.SecretsManagerPlan.BaseSeats +
subscriptionUpdate.SecretsManagerPlan.MaxAdditionalSeats.GetValueOrDefault())),
{ UseSecretsManger: true, UpdatedSeatTotal: var updateSeatTotal, MaxAutoScaleSeats: var maxAutoScaleSeats } { UseSecretsManger: true, UpdatedSeatTotal: var updateSeatTotal, MaxAutoScaleSeats: var maxAutoScaleSeats }
when updateSeatTotal > maxAutoScaleSeats => when updateSeatTotal > maxAutoScaleSeats =>
new Invalid<SecretsManagerSubscriptionUpdate>(SecretsManagerSeatLimitReached), new Invalid<SecretsManagerSubscriptionUpdate>(
new SecretsManagerSeatLimitReachedError(subscriptionUpdate)),
{ {
UseSecretsManger: true,
PasswordManagerUpdatedSeatTotal: var passwordManagerUpdatedSeatTotal, PasswordManagerUpdatedSeatTotal: var passwordManagerUpdatedSeatTotal,
UpdatedSeatTotal: var secretsManagerUpdatedSeatTotal UpdatedSeatTotal: var secretsManagerUpdatedSeatTotal
} when passwordManagerUpdatedSeatTotal < secretsManagerUpdatedSeatTotal => }
new Invalid<SecretsManagerSubscriptionUpdate>(SecretsManagerCannotExceedPasswordManager), when passwordManagerUpdatedSeatTotal < secretsManagerUpdatedSeatTotal =>
new Invalid<SecretsManagerSubscriptionUpdate>(
new SecretsManagerCannotExceedPasswordManagerError(subscriptionUpdate)),
_ => new Valid<SecretsManagerSubscriptionUpdate>(subscriptionUpdate) _ => new Valid<SecretsManagerSubscriptionUpdate>(subscriptionUpdate)
}; };

View File

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
public class SecretsManagerSubscriptionUpdate public class SecretsManagerSubscriptionUpdate
{ {

View File

@ -1,15 +0,0 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
public abstract record ValidationResult<T>(T Value, IEnumerable<string> Errors)
{
public bool IsValid => !Errors.Any();
public string ErrorMessageString => string.Join(" ", Errors);
}
public record Valid<T>(T Value) : ValidationResult<T>(Value, []);
public record Invalid<T>(IEnumerable<string> Errors) : ValidationResult<T>(default, Errors)
{
public Invalid(string error) : this([error]) { }
}

View File

@ -6,10 +6,39 @@ public abstract record ValidationResult<T>;
public record Valid<T> : ValidationResult<T> public record Valid<T> : ValidationResult<T>
{ {
public Valid() { }
public Valid(T Value)
{
this.Value = Value;
}
public T Value { get; init; } public T Value { get; init; }
} }
public record Invalid<T> : ValidationResult<T> public record Invalid<T> : ValidationResult<T>
{ {
public IEnumerable<Error<T>> Errors { get; init; } public IEnumerable<Error<T>> Errors { get; init; } = [];
public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message));
public Invalid() { }
public Invalid(Error<T> error) : this([error]) { }
public Invalid(IEnumerable<Error<T>> errors)
{
Errors = errors;
}
}
public static class ValidationResultMappers
{
public static ValidationResult<B> Map<A, B>(this ValidationResult<A> validationResult, B invalidValue) =>
validationResult switch
{
Valid<A> => new Valid<B>(invalidValue),
Invalid<A> invalid => new Invalid<B>(invalid.Errors.Select(x => x.ToError(invalidValue))),
_ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type")
};
} }

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers;

View File

@ -1,10 +1,13 @@
using System.Net.Mail; using System.Net.Mail;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Commands; using Bit.Core.Models.Commands;
@ -158,7 +161,7 @@ public class InviteOrganizationUserCommandTests
sutProvider.GetDependency<IInviteUsersValidation>() sutProvider.GetDependency<IInviteUsersValidation>()
.ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>()) .ValidateAsync(Arg.Any<InviteUserOrganizationValidationRequest>())
.Returns(new Invalid<InviteUserOrganizationValidationRequest>(errorMessage)); .Returns(new Invalid<InviteUserOrganizationValidationRequest>(new Error<InviteUserOrganizationValidationRequest>(errorMessage, new InviteUserOrganizationValidationRequest())));
// Act // Act
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
@ -27,7 +28,8 @@ public class InviteUserOrganizationValidationTests
var result = InvitingUserOrganizationValidation.Validate(new InviteOrganization(organization)); var result = InvitingUserOrganizationValidation.Validate(new InviteOrganization(organization));
Assert.IsType<Invalid<InviteOrganization>>(result); Assert.IsType<Invalid<InviteOrganization>>(result);
Assert.Equal(InviteUserValidationErrorMessages.NoPaymentMethodFoundError, result.ErrorMessageString);
Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid<InviteOrganization>).ErrorMessageString);
} }
[Theory] [Theory]
@ -40,6 +42,6 @@ public class InviteUserOrganizationValidationTests
var result = InvitingUserOrganizationValidation.Validate(new InviteOrganization(organization)); var result = InvitingUserOrganizationValidation.Validate(new InviteOrganization(organization));
Assert.IsType<Invalid<InviteOrganization>>(result); Assert.IsType<Invalid<InviteOrganization>>(result);
Assert.Equal(InviteUserValidationErrorMessages.NoSubscriptionFoundError, result.ErrorMessageString); Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid<InviteOrganization>).ErrorMessageString);
} }
} }

View File

@ -2,6 +2,8 @@
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -36,7 +38,7 @@ public class InviteUserPaymentValidationTests
}); });
Assert.IsType<Invalid<PaymentsSubscription>>(result); Assert.IsType<Invalid<PaymentsSubscription>>(result);
Assert.Equal(InviteUserValidationErrorMessages.CancelledSubscriptionError, result.ErrorMessageString); Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid<PaymentsSubscription>).ErrorMessageString);
} }
[Fact] [Fact]

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
@ -61,7 +61,7 @@ public class PasswordManagerInviteUserValidationTests
var result = PasswordManagerInviteUserValidation.Validate(subscriptionUpdate); var result = PasswordManagerInviteUserValidation.Validate(subscriptionUpdate);
Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result); Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.SeatLimitHasBeenReachedError, result.ErrorMessageString); Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid<PasswordManagerSubscriptionUpdate>).ErrorMessageString);
} }
[Theory] [Theory]
@ -80,6 +80,6 @@ public class PasswordManagerInviteUserValidationTests
var result = PasswordManagerInviteUserValidation.Validate(subscriptionUpdate); var result = PasswordManagerInviteUserValidation.Validate(subscriptionUpdate);
Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result); Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.PlanDoesNotAllowAdditionalSeats, result.ErrorMessageString); Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid<PasswordManagerSubscriptionUpdate>).ErrorMessageString);
} }
} }

View File

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.SecretsManager;
using Bit.Core.AdminConsole.Shared.Validation;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -68,7 +69,7 @@ public class SecretsManagerInviteUserValidationTests
var result = SecretsManagerInviteUserValidation.Validate(update); var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result); Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.OrganizationNoSecretsManager, result.ErrorMessageString); Assert.Equal(OrganizationNoSecretsManagerError.Code, (result as Invalid<SecretsManagerSubscriptionUpdate>).ErrorMessageString);
} }
[Theory] [Theory]
@ -127,7 +128,7 @@ public class SecretsManagerInviteUserValidationTests
var result = SecretsManagerInviteUserValidation.Validate(update); var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result); Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.SecretsManagerSeatLimitReached, result.ErrorMessageString); Assert.Equal(SecretsManagerSeatLimitReachedError.Code, (result as Invalid<SecretsManagerSubscriptionUpdate>).ErrorMessageString);
} }
[Theory] [Theory]
@ -158,6 +159,6 @@ public class SecretsManagerInviteUserValidationTests
var result = SecretsManagerInviteUserValidation.Validate(update); var result = SecretsManagerInviteUserValidation.Validate(update);
Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result); Assert.IsType<Invalid<SecretsManagerSubscriptionUpdate>>(result);
Assert.Equal(InviteUserValidationErrorMessages.SecretsManagerCannotExceedPasswordManager, result.ErrorMessageString); Assert.Equal(SecretsManagerCannotExceedPasswordManagerError.Code, (result as Invalid<SecretsManagerSubscriptionUpdate>).ErrorMessageString);
} }
} }