1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-16 01:51:21 +01:00

[PM-12488] Migrating Cloud Org Sign Up to Command (#5078)

This commit is contained in:
Jared McCannon 2024-12-05 21:18:02 -06:00 committed by GitHub
parent 6a9b7ece2b
commit 2333a934a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 630 additions and 363 deletions

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
@ -52,11 +53,11 @@ public class OrganizationsController : Controller
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IFeatureService _featureService;
private readonly GlobalSettings _globalSettings;
private readonly IPushNotificationService _pushNotificationService;
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -73,11 +74,11 @@ public class OrganizationsController : Controller
IOrganizationApiKeyRepository organizationApiKeyRepository,
IFeatureService featureService,
GlobalSettings globalSettings,
IPushNotificationService pushNotificationService,
IProviderRepository providerRepository,
IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -93,11 +94,11 @@ public class OrganizationsController : Controller
_organizationApiKeyRepository = organizationApiKeyRepository;
_featureService = featureService;
_globalSettings = globalSettings;
_pushNotificationService = pushNotificationService;
_providerRepository = providerRepository;
_providerBillingService = providerBillingService;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
}
[HttpGet("{id}")]
@ -175,8 +176,8 @@ public class OrganizationsController : Controller
}
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _organizationService.SignUpAsync(organizationSignup);
return new OrganizationResponseModel(result.Item1);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization);
}
[HttpPost("create-without-payment")]
@ -190,8 +191,8 @@ public class OrganizationsController : Controller
}
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _organizationService.SignUpAsync(organizationSignup);
return new OrganizationResponseModel(result.Item1);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization);
}
[HttpPut("{id}")]

View File

@ -0,0 +1,368 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public record SignUpOrganizationResponse(
Organization Organization,
OrganizationUser OrganizationUser,
Collection DefaultCollection);
public interface ICloudOrganizationSignUpCommand
{
Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup);
}
public class CloudOrganizationSignUpCommand(
IOrganizationUserRepository organizationUserRepository,
IFeatureService featureService,
IOrganizationBillingService organizationBillingService,
IPaymentService paymentService,
IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IApplicationCacheService applicationCacheService,
IPushRegistrationService pushRegistrationService,
IPushNotificationService pushNotificationService,
ICollectionRepository collectionRepository,
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand
{
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
{
var plan = StaticStore.GetPlan(signup.Plan);
ValidatePasswordManagerPlan(plan, signup);
if (signup.UseSecretsManager)
{
if (signup.IsFromProvider)
{
throw new BadRequestException(
"Organizations with a Managed Service Provider do not support Secrets Manager.");
}
ValidateSecretsManagerPlan(plan, signup);
}
if (!signup.IsFromProvider)
{
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
}
var organization = new Organization
{
// Pre-generate the org id so that we can save it with the Stripe subscription
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
BusinessName = signup.BusinessName,
PlanType = plan!.Type,
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ?
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
UseCustomPermissions = plan.HasCustomPermissions,
UseScim = plan.HasScim,
Plan = plan.Name,
Gateway = null,
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
UseSecretsManager = signup.UseSecretsManager
};
if (signup.UseSecretsManager)
{
organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +
signup.AdditionalServiceAccounts.GetValueOrDefault();
}
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
{
var adminCount =
await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
if (adminCount > 0)
{
throw new BadRequestException("You can only be an admin of one free organization.");
}
}
else if (plan.Type != PlanType.Free)
{
if (featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
var sale = OrganizationSale.From(organization, signup);
await organizationBillingService.Finalize(sale);
}
else
{
if (signup.PaymentMethodType != null)
{
await paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
else
{
await paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
}
}
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
await referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Item1.Seats,
SignupInitiationPath = signup.InitiationPath,
Storage = returnValue.Item1.MaxStorageGb,
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
});
return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection);
}
public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade)
{
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)
{
throw new BadRequestException($"You do not have any Password Manager seats!");
}
if (upgrade.AdditionalSeats < 0)
{
throw new BadRequestException($"You can't subtract Password Manager seats!");
}
if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
{
throw new BadRequestException("Plan does not allow additional storage.");
}
if (upgrade.AdditionalStorageGb < 0)
{
throw new BadRequestException("You can't subtract storage!");
}
if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
{
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
}
if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
{
throw new BadRequestException("Plan does not allow additional users.");
}
if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue &&
upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
{
throw new BadRequestException($"Selected plan allows a maximum of " +
$"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
}
public void ValidateSecretsManagerPlan(Plan plan, OrganizationUpgrade upgrade)
{
if (plan.SupportsSecretsManager == false)
{
throw new BadRequestException("Invalid Secrets Manager plan selected.");
}
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");
if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0)
{
throw new BadRequestException($"You do not have any Secrets Manager seats!");
}
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
{
throw new BadRequestException("Plan does not allow additional Machine Accounts.");
}
if ((plan.ProductTier == ProductTierType.TeamsStarter &&
upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||
(plan.ProductTier != ProductTierType.TeamsStarter &&
upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))
{
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
}
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
{
throw new BadRequestException("You can't subtract Machine Accounts!");
}
switch (plan.SecretsManager.HasAdditionalSeatsOption)
{
case false when upgrade.AdditionalSmSeats > 0:
throw new BadRequestException("Plan does not allow additional users.");
case true when plan.SecretsManager.MaxAdditionalSeats.HasValue &&
upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value:
throw new BadRequestException($"Selected plan allows a maximum of " +
$"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
}
private static void ValidatePlan(Plan plan, int additionalSeats, string productType)
{
if (plan is null)
{
throw new BadRequestException($"{productType} Plan was null.");
}
if (plan.Disabled)
{
throw new BadRequestException($"{productType} Plan not found.");
}
if (additionalSeats < 0)
{
throw new BadRequestException($"You can't subtract {productType} seats!");
}
}
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{
throw new BadRequestException("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}
}
private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization,
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
{
try
{
await organizationRepository.CreateAsync(organization);
await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
{
OrganizationId = organization.Id,
ApiKey = CoreHelpers.SecureRandomString(30),
Type = OrganizationApiKeyType.Default,
RevisionDate = DateTime.UtcNow,
});
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
// ownerId == default if the org is created by a provider - in this case it's created without an
// owner and the first owner is immediately invited afterwards
OrganizationUser orgUser = null;
if (ownerId != default)
{
orgUser = new OrganizationUser
{
OrganizationId = organization.Id,
UserId = ownerId,
Key = ownerKey,
AccessSecretsManager = organization.UseSecretsManager,
Type = OrganizationUserType.Owner,
Status = OrganizationUserStatusType.Confirmed,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
orgUser.SetNewId();
await organizationUserRepository.CreateAsync(orgUser);
var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, organization.Id.ToString());
await pushNotificationService.PushSyncOrgKeysAsync(ownerId);
}
Collection defaultCollection = null;
if (!string.IsNullOrWhiteSpace(collectionName))
{
defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
// Give the owner Can Manage access over the default collection
List<CollectionAccessSelection> defaultOwnerAccess = null;
if (orgUser != null)
{
defaultOwnerAccess =
[new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
}
await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
}
return (organization, orgUser, defaultCollection);
}
catch
{
if (withPayment)
{
await paymentService.CancelAndRecoverChargesAsync(organization);
}
if (organization.Id != default(Guid))
{
await organizationRepository.DeleteAsync(organization);
await applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
throw;
}
}
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
{
var devices = await deviceRepository.GetManyByUserIdAsync(userId);
return devices
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => d.Id.ToString());
}
}

View File

@ -20,13 +20,7 @@ public interface IOrganizationService
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
/// <summary>
/// Create a new organization in a cloud environment
/// </summary>
/// <returns>A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any)</returns>
#nullable enable
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup);
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
#nullable disable
/// <summary>

View File

@ -16,7 +16,6 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -502,129 +501,6 @@ public class OrganizationService : IOrganizationService
return returnValue;
}
/// <summary>
/// Create a new organization in a cloud environment
/// </summary>
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup)
{
var plan = StaticStore.GetPlan(signup.Plan);
ValidatePasswordManagerPlan(plan, signup);
if (signup.UseSecretsManager)
{
if (signup.IsFromProvider)
{
throw new BadRequestException(
"Organizations with a Managed Service Provider do not support Secrets Manager.");
}
ValidateSecretsManagerPlan(plan, signup);
}
if (!signup.IsFromProvider)
{
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
}
var organization = new Organization
{
// Pre-generate the org id so that we can save it with the Stripe subscription..
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
BusinessName = signup.BusinessName,
PlanType = plan!.Type,
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ?
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
UseCustomPermissions = plan.HasCustomPermissions,
UseScim = plan.HasScim,
Plan = plan.Name,
Gateway = null,
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
UseSecretsManager = signup.UseSecretsManager
};
if (signup.UseSecretsManager)
{
organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +
signup.AdditionalServiceAccounts.GetValueOrDefault();
}
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
{
var adminCount =
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
if (adminCount > 0)
{
throw new BadRequestException("You can only be an admin of one free organization.");
}
}
else if (plan.Type != PlanType.Free)
{
var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI);
if (deprecateStripeSourcesAPI)
{
var sale = OrganizationSale.From(organization, signup);
await _organizationBillingService.Finalize(sale);
}
else
{
if (signup.PaymentMethodType != null)
{
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
else
{
await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
}
}
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Item1.Seats,
SignupInitiationPath = signup.InitiationPath,
Storage = returnValue.Item1.MaxStorageGb,
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
});
return returnValue;
}
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
@ -50,12 +51,16 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationSignUpCommands();
services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries();
services.AddBaseOrganizationSubscriptionCommandsQueries();
}
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
{
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();

View File

@ -1,13 +1,13 @@
using System.Diagnostics;
using Bit.Api.IntegrationTest.Factories;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.IntegrationTestCommon.Factories;
namespace Bit.Api.IntegrationTest.Helpers;
@ -24,11 +24,11 @@ public static class OrganizationTestHelpers
PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class
{
var userRepository = factory.GetService<IUserRepository>();
var organizationService = factory.GetService<IOrganizationService>();
var organizationSignUpCommand = factory.GetService<ICloudOrganizationSignUpCommand>();
var owner = await userRepository.GetByEmailAsync(ownerEmail);
var signUpResult = await organizationService.SignUpAsync(new OrganizationSignup
var signUpResult = await organizationSignUpCommand.SignUpOrganizationAsync(new OrganizationSignup
{
Name = name,
BillingEmail = billingEmail,
@ -39,9 +39,9 @@ public static class OrganizationTestHelpers
PaymentMethodType = paymentMethod
});
Debug.Assert(signUpResult.organizationUser is not null);
Debug.Assert(signUpResult.OrganizationUser is not null);
return new Tuple<Organization, OrganizationUser>(signUpResult.organization, signUpResult.organizationUser);
return new Tuple<Organization, OrganizationUser>(signUpResult.Organization, signUpResult.OrganizationUser);
}
/// <summary>

View File

@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
@ -46,11 +47,11 @@ public class OrganizationsControllerTests : IDisposable
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly IFeatureService _featureService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly OrganizationsController _sut;
public OrganizationsControllerTests()
@ -69,11 +70,11 @@ public class OrganizationsControllerTests : IDisposable
_userService = Substitute.For<IUserService>();
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
_featureService = Substitute.For<IFeatureService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_providerRepository = Substitute.For<IProviderRepository>();
_providerBillingService = Substitute.For<IProviderBillingService>();
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
_sut = new OrganizationsController(
_organizationRepository,
@ -90,11 +91,11 @@ public class OrganizationsControllerTests : IDisposable
_organizationApiKeyRepository,
_featureService,
_globalSettings,
_pushNotificationService,
_providerRepository,
_providerBillingService,
_orgDeleteTokenDataFactory,
_removeOrganizationUserCommand);
_removeOrganizationUserCommand,
_cloudOrganizationSignUpCommand);
}
public void Dispose()

View File

@ -0,0 +1,238 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;
[SutProviderCustomize]
public class CloudICloudOrganizationSignUpCommandTests
{
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == null
&& o.SmServiceAccounts == null));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Organization.Seats &&
referenceEvent.Storage == result.Organization.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Organization);
Assert.NotNull(result.OrganizationUser);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
plan,
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.UseSecretsManager
);
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_AssignsOwnerToDefaultCollection
(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
// Extract orgUserId when created
Guid? orgUserId = null;
await sutProvider.GetDependency<IOrganizationUserRepository>()
.CreateAsync(Arg.Do<OrganizationUser>(ou => orgUserId = ou.Id));
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
// Assert: created a Can Manage association for the default collection
Assert.NotNull(orgUserId);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).CreateAsync(
Arg.Any<Collection>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas => cas == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas =>
cas.Count() == 1 &&
cas.All(c =>
c.Id == orgUserId &&
!c.ReadOnly &&
!c.HidePasswords &&
c.Manage)));
Assert.NotNull(result.Organization);
Assert.NotNull(result.OrganizationUser);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats
&& o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Organization.Seats &&
referenceEvent.Storage == result.Organization.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Organization);
Assert.NotNull(result.OrganizationUser);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
Arg.Is<Plan>(plan),
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.IsFromSecretsManagerTrial
);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromProvider = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.AdditionalSmSeats = 0;
signup.AdditionalSeats = 0;
signup.Plan = PlanType.Free;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
signup.AdditionalStorageGb = 0;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.AdditionalSmSeats = 100;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.AdditionalSmSeats = 10;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = -10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
}

View File

@ -20,7 +20,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Mail;
using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -200,221 +199,6 @@ public class OrganizationServiceTests
referenceEvent.Users == expectedNewUsersCount));
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
signup.IsFromSecretsManagerTrial = false;
var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan);
var result = await sutProvider.Sut.SignUpAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == null
&& o.SmServiceAccounts == null));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Item1.Seats &&
referenceEvent.Storage == result.Item1.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
plan,
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.UseSecretsManager
);
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_AssignsOwnerToDefaultCollection
(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
// Extract orgUserId when created
Guid? orgUserId = null;
await sutProvider.GetDependency<IOrganizationUserRepository>()
.CreateAsync(Arg.Do<OrganizationUser>(ou => orgUserId = ou.Id));
var result = await sutProvider.Sut.SignUpAsync(signup);
// Assert: created a Can Manage association for the default collection
Assert.NotNull(orgUserId);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).CreateAsync(
Arg.Any<Collection>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas => cas == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas =>
cas.Count() == 1 &&
cas.All(c =>
c.Id == orgUserId &&
!c.ReadOnly &&
!c.HidePasswords &&
c.Manage)));
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats
&& o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Item1.Seats &&
referenceEvent.Storage == result.Item1.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
Arg.Is<Plan>(plan),
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.IsFromSecretsManagerTrial
);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromProvider = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 0;
signup.AdditionalSeats = 0;
signup.Plan = PlanType.Free;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
signup.AdditionalStorageGb = 0;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 100;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 10;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = -10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
[Theory, BitAutoData]
public async Task SignupClientAsync_Succeeds(
OrganizationSignup signup,