From a5efec301e848c1beb2f1b6b03e5e513df4d74cf Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 11 Jul 2023 19:01:20 +1000 Subject: [PATCH] [AC-1495] Extract UpgradePlanAsync into a command (#3081) * This is a pure lift & shift with no refactors * Only register subscription commands in Api --------- Co-authored-by: cyprain-okeke --- bitwarden_license/src/Scim/Startup.cs | 2 - src/Admin/Startup.cs | 3 - .../Controllers/OrganizationsController.cs | 9 +- src/Api/Startup.cs | 2 + ...OrganizationServiceCollectionExtensions.cs | 8 - ...UpdateSecretsManagerSubscriptionCommand.cs | 2 +- .../IUpgradeOrganizationPlanCommand.cs | 6 + ...SubscriptionServiceCollectionExtensions.cs | 13 + ...UpdateSecretsManagerSubscriptionCommand.cs | 4 +- .../UpgradeOrganizationPlanCommand.cs | 339 ++++++++++++++++++ src/Core/Services/IOrganizationService.cs | 4 +- .../Implementations/OrganizationService.cs | 288 +-------------- src/Identity/Startup.cs | 5 - .../OrganizationsControllerTests.cs | 6 +- ...eSecretsManagerSubscriptionCommandTests.cs | 2 +- .../UpgradeOrganizationPlanCommandTests.cs | 113 ++++++ .../Services/OrganizationServiceTests.cs | 93 ----- 17 files changed, 493 insertions(+), 406 deletions(-) rename src/Core/OrganizationFeatures/{OrganizationSubscriptionUpdate => OrganizationSubscriptions}/Interface/IUpdateSecretsManagerSubscriptionCommand.cs (93%) create mode 100644 src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpgradeOrganizationPlanCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs rename src/Core/OrganizationFeatures/{OrganizationSubscriptionUpdate => OrganizationSubscriptions}/UpdateSecretsManagerSubscriptionCommand.cs (99%) create mode 100644 src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index 0d87f48ff7..4ef46459c3 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -42,8 +42,6 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); - services.AddOosServices(); - // Context services.AddScoped(); services.AddScoped(); diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 81c789c9c0..9482be011a 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -9,8 +9,6 @@ using Stripe; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; -using Bit.Core.Repositories.Noop; -using Bit.Core.SecretsManager.Repositories; #if !OSS using Bit.Commercial.Core.Utilities; @@ -88,7 +86,6 @@ public class Startup services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.AddScoped(); - services.AddScoped(); #if OSS services.AddOosServices(); diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index d0fe7eaf70..8ceb1f038b 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -18,7 +18,7 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; -using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -52,6 +52,7 @@ public class OrganizationsController : Controller private readonly GlobalSettings _globalSettings; private readonly ILicensingService _licensingService; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; + private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -73,7 +74,8 @@ public class OrganizationsController : Controller IFeatureService featureService, GlobalSettings globalSettings, ILicensingService licensingService, - IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand) + IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, + IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -95,6 +97,7 @@ public class OrganizationsController : Controller _globalSettings = globalSettings; _licensingService = licensingService; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; + _upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand; } [HttpGet("{id}")] @@ -310,7 +313,7 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - var result = await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); + var result = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 }; } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 6070a91b29..2580e06df9 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -15,6 +15,7 @@ using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Core.Auth.Identity; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; #if !OSS using Bit.Commercial.Core.SecretsManager; @@ -133,6 +134,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddOrganizationSubscriptionServices(); services.AddCoreLocalizationServices(); //health check diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 7bbe1680b8..f302aef90b 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -17,8 +17,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted; -using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate; -using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface; using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager; using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces; using Bit.Core.Services; @@ -45,7 +43,6 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationGroupCommands(); services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); - services.AddOrganizationSubscriptionUpdateCommandsQueries(); services.AddOrganizationAuthCommands(); } @@ -116,11 +113,6 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } - private static void AddOrganizationSubscriptionUpdateCommandsQueries(this IServiceCollection services) - { - services.AddScoped(); - } - private static void AddOrganizationAuthCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptionUpdate/Interface/IUpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs similarity index 93% rename from src/Core/OrganizationFeatures/OrganizationSubscriptionUpdate/Interface/IUpdateSecretsManagerSubscriptionCommand.cs rename to src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs index de5271639e..78244d8e3d 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptionUpdate/Interface/IUpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs @@ -1,6 +1,6 @@ using Bit.Core.Models.Business; -namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface; +namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; public interface IUpdateSecretsManagerSubscriptionCommand { diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpgradeOrganizationPlanCommand.cs new file mode 100644 index 0000000000..59525242bb --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpgradeOrganizationPlanCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; + +public interface IUpgradeOrganizationPlanCommand +{ + Task> UpgradePlanAsync(Guid organizationId, Models.Business.OrganizationUpgrade upgrade); +} diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs new file mode 100644 index 0000000000..1b8d67211b --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; + +public static class OrganizationSubscriptionServiceCollectionExtensions +{ + public static void AddOrganizationSubscriptionServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs similarity index 99% rename from src/Core/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommand.cs rename to src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs index 16d0af84e5..9237253f79 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs @@ -4,14 +4,14 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; -using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; -namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate; +namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand { diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs new file mode 100644 index 0000000000..62ee67442f --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -0,0 +1,339 @@ +using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.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.OrganizationFeatures.OrganizationSubscriptions; + +public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand +{ + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly IGroupRepository _groupRepository; + private readonly IPaymentService _paymentService; + private readonly IPolicyRepository _policyRepository; + private readonly ISsoConfigRepository _ssoConfigRepository; + private readonly IReferenceEventService _referenceEventService; + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + private readonly ICurrentContext _currentContext; + private readonly IServiceAccountRepository _serviceAccountRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationService _organizationService; + + public UpgradeOrganizationPlanCommand( + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository, + IGroupRepository groupRepository, + IPaymentService paymentService, + IPolicyRepository policyRepository, + ISsoConfigRepository ssoConfigRepository, + IReferenceEventService referenceEventService, + IOrganizationConnectionRepository organizationConnectionRepository, + ICurrentContext currentContext, + IServiceAccountRepository serviceAccountRepository, + IOrganizationRepository organizationRepository, + IOrganizationService organizationService) + { + _organizationUserRepository = organizationUserRepository; + _collectionRepository = collectionRepository; + _groupRepository = groupRepository; + _paymentService = paymentService; + _policyRepository = policyRepository; + _ssoConfigRepository = ssoConfigRepository; + _referenceEventService = referenceEventService; + _organizationConnectionRepository = organizationConnectionRepository; + _currentContext = currentContext; + _serviceAccountRepository = serviceAccountRepository; + _organizationRepository = organizationRepository; + _organizationService = organizationService; + } + + public async Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) + { + var organization = await GetOrgById(organizationId); + if (organization == null) + { + throw new NotFoundException(); + } + + if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) + { + throw new BadRequestException("Your account has no payment method available."); + } + + var existingPasswordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); + if (existingPasswordManagerPlan == null) + { + throw new BadRequestException("Existing plan not found."); + } + + var newPasswordManagerPlan = + StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); + if (newPasswordManagerPlan == null) + { + throw new BadRequestException("Plan not found."); + } + + if (existingPasswordManagerPlan.Type == newPasswordManagerPlan.Type) + { + throw new BadRequestException("Organization is already on this plan."); + } + + if (existingPasswordManagerPlan.UpgradeSortOrder >= newPasswordManagerPlan.UpgradeSortOrder) + { + throw new BadRequestException("You cannot upgrade to this plan."); + } + + if (existingPasswordManagerPlan.Type != PlanType.Free) + { + throw new BadRequestException("You can only upgrade from the free plan. Contact support."); + } + + _organizationService.ValidatePasswordManagerPlan(newPasswordManagerPlan, upgrade); + var newSecretsManagerPlan = + StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); + if (upgrade.UseSecretsManager) + { + _organizationService.ValidateSecretsManagerPlan(newSecretsManagerPlan, upgrade); + } + + var newPasswordManagerPlanSeats = (short)(newPasswordManagerPlan.BaseSeats + + (newPasswordManagerPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0)); + if (!organization.Seats.HasValue || organization.Seats.Value > newPasswordManagerPlanSeats) + { + var occupiedSeats = + await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + if (occupiedSeats > newPasswordManagerPlanSeats) + { + throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " + + $"Your new plan only has ({newPasswordManagerPlanSeats}) seats. Remove some users."); + } + } + + if (newPasswordManagerPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue || + organization.MaxCollections.Value > + newPasswordManagerPlan.MaxCollections.Value)) + { + var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id); + if (collectionCount > newPasswordManagerPlan.MaxCollections.Value) + { + throw new BadRequestException($"Your organization currently has {collectionCount} collections. " + + $"Your new plan allows for a maximum of ({newPasswordManagerPlan.MaxCollections.Value}) collections. " + + "Remove some collections."); + } + } + + if (!newPasswordManagerPlan.HasGroups && organization.UseGroups) + { + var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id); + if (groups.Any()) + { + throw new BadRequestException($"Your new plan does not allow the groups feature. " + + $"Remove your groups."); + } + } + + if (!newPasswordManagerPlan.HasPolicies && organization.UsePolicies) + { + var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id); + if (policies.Any(p => p.Enabled)) + { + throw new BadRequestException($"Your new plan does not allow the policies feature. " + + $"Disable your policies."); + } + } + + if (!newPasswordManagerPlan.HasSso && organization.UseSso) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); + if (ssoConfig != null && ssoConfig.Enabled) + { + throw new BadRequestException($"Your new plan does not allow the SSO feature. " + + $"Disable your SSO configuration."); + } + } + + if (!newPasswordManagerPlan.HasKeyConnector && organization.UseKeyConnector) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); + if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector) + { + throw new BadRequestException("Your new plan does not allow the Key Connector feature. " + + "Disable your Key Connector."); + } + } + + if (!newPasswordManagerPlan.HasResetPassword && organization.UseResetPassword) + { + var resetPasswordPolicy = + await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword); + if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled) + { + throw new BadRequestException("Your new plan does not allow the Password Reset feature. " + + "Disable your Password Reset policy."); + } + } + + if (!newPasswordManagerPlan.HasScim && organization.UseScim) + { + var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id, + OrganizationConnectionType.Scim); + if (scimConnections != null && scimConnections.Any(c => c.GetConfig()?.Enabled == true)) + { + throw new BadRequestException("Your new plan does not allow the SCIM feature. " + + "Disable your SCIM configuration."); + } + } + + if (!newPasswordManagerPlan.HasCustomPermissions && organization.UseCustomPermissions) + { + var organizationCustomUsers = + await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, + OrganizationUserType.Custom); + if (organizationCustomUsers.Any()) + { + throw new BadRequestException("Your new plan does not allow the Custom Permissions feature. " + + "Disable your Custom Permissions configuration."); + } + } + + if (upgrade.UseSecretsManager && newSecretsManagerPlan != null) + { + await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newSecretsManagerPlan); + } + + // TODO: Check storage? + string paymentIntentClientSecret = null; + var success = true; + + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + var organizationUpgradePlan = upgrade.UseSecretsManager + ? StaticStore.Plans.Where(p => p.Type == upgrade.Plan).ToList() + : StaticStore.Plans.Where(p => p.Type == upgrade.Plan && p.BitwardenProduct == BitwardenProductType.PasswordManager).ToList(); + + paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, + organizationUpgradePlan, + upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo + , upgrade.AdditionalSmSeats.GetValueOrDefault(), upgrade.AdditionalServiceAccounts.GetValueOrDefault()); + success = string.IsNullOrWhiteSpace(paymentIntentClientSecret); + } + else + { + // TODO: Update existing sub + throw new BadRequestException("You can only upgrade from the free plan. Contact support."); + } + + organization.BusinessName = upgrade.BusinessName; + organization.PlanType = newPasswordManagerPlan.Type; + organization.Seats = (short)(newPasswordManagerPlan.BaseSeats + upgrade.AdditionalSeats); + organization.MaxCollections = newPasswordManagerPlan.MaxCollections; + organization.UseGroups = newPasswordManagerPlan.HasGroups; + organization.UseDirectory = newPasswordManagerPlan.HasDirectory; + organization.UseEvents = newPasswordManagerPlan.HasEvents; + organization.UseTotp = newPasswordManagerPlan.HasTotp; + organization.Use2fa = newPasswordManagerPlan.Has2fa; + organization.UseApi = newPasswordManagerPlan.HasApi; + organization.SelfHost = newPasswordManagerPlan.HasSelfHost; + organization.UsePolicies = newPasswordManagerPlan.HasPolicies; + organization.MaxStorageGb = !newPasswordManagerPlan.BaseStorageGb.HasValue + ? (short?)null + : (short)(newPasswordManagerPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb); + organization.UseGroups = newPasswordManagerPlan.HasGroups; + organization.UseDirectory = newPasswordManagerPlan.HasDirectory; + organization.UseEvents = newPasswordManagerPlan.HasEvents; + organization.UseTotp = newPasswordManagerPlan.HasTotp; + organization.Use2fa = newPasswordManagerPlan.Has2fa; + organization.UseApi = newPasswordManagerPlan.HasApi; + organization.UseSso = newPasswordManagerPlan.HasSso; + organization.UseKeyConnector = newPasswordManagerPlan.HasKeyConnector; + organization.UseScim = newPasswordManagerPlan.HasScim; + organization.UseResetPassword = newPasswordManagerPlan.HasResetPassword; + organization.SelfHost = newPasswordManagerPlan.HasSelfHost; + organization.UsersGetPremium = newPasswordManagerPlan.UsersGetPremium || upgrade.PremiumAccessAddon; + organization.UseCustomPermissions = newPasswordManagerPlan.HasCustomPermissions; + organization.Plan = newPasswordManagerPlan.Name; + organization.Enabled = success; + organization.PublicKey = upgrade.PublicKey; + organization.PrivateKey = upgrade.PrivateKey; + organization.UsePasswordManager = true; + organization.SmSeats = (short)(newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault()); + organization.SmServiceAccounts = upgrade.AdditionalServiceAccounts.GetValueOrDefault(); + organization.UseSecretsManager = upgrade.UseSecretsManager; + + await _organizationService.ReplaceAndUpdateCacheAsync(organization); + + if (success) + { + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext) + { + PlanName = newPasswordManagerPlan.Name, + PlanType = newPasswordManagerPlan.Type, + OldPlanName = existingPasswordManagerPlan.Name, + OldPlanType = existingPasswordManagerPlan.Type, + Seats = organization.Seats, + Storage = organization.MaxStorageGb, + SmSeats = organization.SmSeats, + ServiceAccounts = organization.SmServiceAccounts, + UseSecretsManager = organization.UseSecretsManager + }); + } + + return new Tuple(success, paymentIntentClientSecret); + } + + private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization, + Models.StaticStore.Plan newSecretsManagerPlan) + { + var newPlanSmSeats = (short)(newSecretsManagerPlan.BaseSeats + + (newSecretsManagerPlan.HasAdditionalSeatsOption + ? upgrade.AdditionalSmSeats + : 0)); + var occupiedSmSeats = + await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); + + if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSmSeats) + { + if (occupiedSmSeats > newPlanSmSeats) + { + throw new BadRequestException( + $"Your organization currently has {occupiedSmSeats} Secrets Manager seats filled. " + + $"Your new plan only has ({newPlanSmSeats}) seats. Remove some users."); + } + } + + if (newSecretsManagerPlan.BaseServiceAccount != null) + { + if (!organization.SmServiceAccounts.HasValue || + organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccounts) + { + var currentServiceAccounts = + await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id); + if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccounts) + { + throw new BadRequestException( + $"Your organization currently has {currentServiceAccounts} service account seats filled. " + + $"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccounts}) service accounts. Remove some service accounts."); + } + } + } + } + + private async Task GetOrgById(Guid id) + { + return await _organizationRepository.GetByIdAsync(id); + } +} diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index b912bd9214..05fa0775a5 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -13,7 +13,6 @@ public interface IOrganizationService TaxInfo taxInfo); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task ReinstateSubscriptionAsync(Guid organizationId); - Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade); Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats); Task AutoAddSeatsAsync(Organization organization, int seatsToAdd, DateTime? prorationDate = null); @@ -79,4 +78,7 @@ public interface IOrganizationService /// Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); + + void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); + void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index c00d9f9835..98506d43de 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1,6 +1,5 @@ using System.Security.Claims; using System.Text.Json; -using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Business; using Bit.Core.Auth.Repositories; @@ -13,7 +12,6 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Repositories; -using Bit.Core.SecretsManager.Repositories; using Bit.Core.Settings; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; @@ -39,7 +37,6 @@ public class OrganizationService : IOrganizationService private readonly IDeviceRepository _deviceRepository; private readonly ILicensingService _licensingService; private readonly IEventService _eventService; - private readonly IInstallationRepository _installationRepository; private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; @@ -49,12 +46,10 @@ public class OrganizationService : IOrganizationService private readonly IReferenceEventService _referenceEventService; private readonly IGlobalSettings _globalSettings; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; - private readonly IOrganizationConnectionRepository _organizationConnectionRepository; private readonly ICurrentContext _currentContext; private readonly ILogger _logger; private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderUserRepository _providerUserRepository; - private readonly IServiceAccountRepository _serviceAccountRepository; public OrganizationService( IOrganizationRepository organizationRepository, @@ -69,7 +64,6 @@ public class OrganizationService : IOrganizationService IDeviceRepository deviceRepository, ILicensingService licensingService, IEventService eventService, - IInstallationRepository installationRepository, IApplicationCacheService applicationCacheService, IPaymentService paymentService, IPolicyRepository policyRepository, @@ -79,12 +73,10 @@ public class OrganizationService : IOrganizationService IReferenceEventService referenceEventService, IGlobalSettings globalSettings, IOrganizationApiKeyRepository organizationApiKeyRepository, - IOrganizationConnectionRepository organizationConnectionRepository, ICurrentContext currentContext, ILogger logger, IProviderOrganizationRepository providerOrganizationRepository, - IProviderUserRepository providerUserRepository, - IServiceAccountRepository serviceAccountRepository) + IProviderUserRepository providerUserRepository) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -98,7 +90,6 @@ public class OrganizationService : IOrganizationService _deviceRepository = deviceRepository; _licensingService = licensingService; _eventService = eventService; - _installationRepository = installationRepository; _applicationCacheService = applicationCacheService; _paymentService = paymentService; _policyRepository = policyRepository; @@ -108,12 +99,10 @@ public class OrganizationService : IOrganizationService _referenceEventService = referenceEventService; _globalSettings = globalSettings; _organizationApiKeyRepository = organizationApiKeyRepository; - _organizationConnectionRepository = organizationConnectionRepository; _currentContext = currentContext; _logger = logger; _providerOrganizationRepository = providerOrganizationRepository; _providerUserRepository = providerUserRepository; - _serviceAccountRepository = serviceAccountRepository; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -170,277 +159,6 @@ public class OrganizationService : IOrganizationService new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext)); } - public async Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) - { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) - { - throw new BadRequestException("Your account has no payment method available."); - } - - var existingPasswordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); - if (existingPasswordManagerPlan == null) - { - throw new BadRequestException("Existing plan not found."); - } - - var newPasswordManagerPlan = - StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); - if (newPasswordManagerPlan == null) - { - throw new BadRequestException("Plan not found."); - } - - if (existingPasswordManagerPlan.Type == newPasswordManagerPlan.Type) - { - throw new BadRequestException("Organization is already on this plan."); - } - - if (existingPasswordManagerPlan.UpgradeSortOrder >= newPasswordManagerPlan.UpgradeSortOrder) - { - throw new BadRequestException("You cannot upgrade to this plan."); - } - - if (existingPasswordManagerPlan.Type != PlanType.Free) - { - throw new BadRequestException("You can only upgrade from the free plan. Contact support."); - } - - ValidatePasswordManagerPlan(newPasswordManagerPlan, upgrade); - var newSecretsManagerPlan = - StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); - if (upgrade.UseSecretsManager) - { - ValidateSecretsManagerPlan(newSecretsManagerPlan, upgrade); - } - - var newPasswordManagerPlanSeats = (short)(newPasswordManagerPlan.BaseSeats + - (newPasswordManagerPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0)); - if (!organization.Seats.HasValue || organization.Seats.Value > newPasswordManagerPlanSeats) - { - var occupiedSeats = - await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); - if (occupiedSeats > newPasswordManagerPlanSeats) - { - throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " + - $"Your new plan only has ({newPasswordManagerPlanSeats}) seats. Remove some users."); - } - } - - if (newPasswordManagerPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue || - organization.MaxCollections.Value > - newPasswordManagerPlan.MaxCollections.Value)) - { - var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id); - if (collectionCount > newPasswordManagerPlan.MaxCollections.Value) - { - throw new BadRequestException($"Your organization currently has {collectionCount} collections. " + - $"Your new plan allows for a maximum of ({newPasswordManagerPlan.MaxCollections.Value}) collections. " + - "Remove some collections."); - } - } - - if (!newPasswordManagerPlan.HasGroups && organization.UseGroups) - { - var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id); - if (groups.Any()) - { - throw new BadRequestException($"Your new plan does not allow the groups feature. " + - $"Remove your groups."); - } - } - - if (!newPasswordManagerPlan.HasPolicies && organization.UsePolicies) - { - var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id); - if (policies.Any(p => p.Enabled)) - { - throw new BadRequestException($"Your new plan does not allow the policies feature. " + - $"Disable your policies."); - } - } - - if (!newPasswordManagerPlan.HasSso && organization.UseSso) - { - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); - if (ssoConfig != null && ssoConfig.Enabled) - { - throw new BadRequestException($"Your new plan does not allow the SSO feature. " + - $"Disable your SSO configuration."); - } - } - - if (!newPasswordManagerPlan.HasKeyConnector && organization.UseKeyConnector) - { - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); - if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector) - { - throw new BadRequestException("Your new plan does not allow the Key Connector feature. " + - "Disable your Key Connector."); - } - } - - if (!newPasswordManagerPlan.HasResetPassword && organization.UseResetPassword) - { - var resetPasswordPolicy = - await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword); - if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled) - { - throw new BadRequestException("Your new plan does not allow the Password Reset feature. " + - "Disable your Password Reset policy."); - } - } - - if (!newPasswordManagerPlan.HasScim && organization.UseScim) - { - var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id, - OrganizationConnectionType.Scim); - if (scimConnections != null && scimConnections.Any(c => c.GetConfig()?.Enabled == true)) - { - throw new BadRequestException("Your new plan does not allow the SCIM feature. " + - "Disable your SCIM configuration."); - } - } - - if (!newPasswordManagerPlan.HasCustomPermissions && organization.UseCustomPermissions) - { - var organizationCustomUsers = - await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, - OrganizationUserType.Custom); - if (organizationCustomUsers.Any()) - { - throw new BadRequestException("Your new plan does not allow the Custom Permissions feature. " + - "Disable your Custom Permissions configuration."); - } - } - - if (upgrade.UseSecretsManager && newSecretsManagerPlan != null) - { - await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newSecretsManagerPlan); - } - - // TODO: Check storage? - string paymentIntentClientSecret = null; - var success = true; - - if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) - { - var organizationUpgradePlan = upgrade.UseSecretsManager - ? StaticStore.Plans.Where(p => p.Type == upgrade.Plan).ToList() - : StaticStore.Plans.Where(p => p.Type == upgrade.Plan && p.BitwardenProduct == BitwardenProductType.PasswordManager).ToList(); - - paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, - organizationUpgradePlan, - upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo - , upgrade.AdditionalSmSeats.GetValueOrDefault(), upgrade.AdditionalServiceAccounts.GetValueOrDefault()); - success = string.IsNullOrWhiteSpace(paymentIntentClientSecret); - } - else - { - // TODO: Update existing sub - throw new BadRequestException("You can only upgrade from the free plan. Contact support."); - } - - organization.BusinessName = upgrade.BusinessName; - organization.PlanType = newPasswordManagerPlan.Type; - organization.Seats = (short)(newPasswordManagerPlan.BaseSeats + upgrade.AdditionalSeats); - organization.MaxCollections = newPasswordManagerPlan.MaxCollections; - organization.UseGroups = newPasswordManagerPlan.HasGroups; - organization.UseDirectory = newPasswordManagerPlan.HasDirectory; - organization.UseEvents = newPasswordManagerPlan.HasEvents; - organization.UseTotp = newPasswordManagerPlan.HasTotp; - organization.Use2fa = newPasswordManagerPlan.Has2fa; - organization.UseApi = newPasswordManagerPlan.HasApi; - organization.SelfHost = newPasswordManagerPlan.HasSelfHost; - organization.UsePolicies = newPasswordManagerPlan.HasPolicies; - organization.MaxStorageGb = !newPasswordManagerPlan.BaseStorageGb.HasValue - ? (short?)null - : (short)(newPasswordManagerPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb); - organization.UseGroups = newPasswordManagerPlan.HasGroups; - organization.UseDirectory = newPasswordManagerPlan.HasDirectory; - organization.UseEvents = newPasswordManagerPlan.HasEvents; - organization.UseTotp = newPasswordManagerPlan.HasTotp; - organization.Use2fa = newPasswordManagerPlan.Has2fa; - organization.UseApi = newPasswordManagerPlan.HasApi; - organization.UseSso = newPasswordManagerPlan.HasSso; - organization.UseKeyConnector = newPasswordManagerPlan.HasKeyConnector; - organization.UseScim = newPasswordManagerPlan.HasScim; - organization.UseResetPassword = newPasswordManagerPlan.HasResetPassword; - organization.SelfHost = newPasswordManagerPlan.HasSelfHost; - organization.UsersGetPremium = newPasswordManagerPlan.UsersGetPremium || upgrade.PremiumAccessAddon; - organization.UseCustomPermissions = newPasswordManagerPlan.HasCustomPermissions; - organization.Plan = newPasswordManagerPlan.Name; - organization.Enabled = success; - organization.PublicKey = upgrade.PublicKey; - organization.PrivateKey = upgrade.PrivateKey; - organization.UsePasswordManager = true; - organization.SmSeats = (short)(newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault()); - organization.SmServiceAccounts = upgrade.AdditionalServiceAccounts.GetValueOrDefault(); - organization.UseSecretsManager = upgrade.UseSecretsManager; - - await ReplaceAndUpdateCacheAsync(organization); - if (success) - { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext) - { - PlanName = newPasswordManagerPlan.Name, - PlanType = newPasswordManagerPlan.Type, - OldPlanName = existingPasswordManagerPlan.Name, - OldPlanType = existingPasswordManagerPlan.Type, - Seats = organization.Seats, - Storage = organization.MaxStorageGb, - SmSeats = organization.SmSeats, - ServiceAccounts = organization.SmServiceAccounts, - UseSecretsManager = organization.UseSecretsManager - }); - } - - return new Tuple(success, paymentIntentClientSecret); - } - - private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization, - Models.StaticStore.Plan newSecretsManagerPlan) - { - var newPlanSmSeats = (short)(newSecretsManagerPlan.BaseSeats + - (newSecretsManagerPlan.HasAdditionalSeatsOption - ? upgrade.AdditionalSmSeats - : 0)); - var occupiedSmSeats = - await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); - - if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSmSeats) - { - if (occupiedSmSeats > newPlanSmSeats) - { - throw new BadRequestException( - $"Your organization currently has {occupiedSmSeats} Secrets Manager seats filled. " + - $"Your new plan only has ({newPlanSmSeats}) seats. Remove some users."); - } - } - - if (newSecretsManagerPlan.BaseServiceAccount != null) - { - if (!organization.SmServiceAccounts.HasValue || - organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccounts) - { - var currentServiceAccounts = - await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id); - if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccounts) - { - throw new BadRequestException( - $"Your organization currently has {currentServiceAccounts} service account seats filled. " + - $"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccounts}) service accounts. Remove some service accounts."); - } - } - } - } - public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) { var organization = await GetOrgById(organizationId); @@ -2162,7 +1880,7 @@ public class OrganizationService : IOrganizationService } } - private static void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade) + public void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade) { ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager"); @@ -2204,7 +1922,7 @@ public class OrganizationService : IOrganizationService } } - private static void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade) + public void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade) { ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager"); diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 5029212e6d..6a0f274ce8 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -4,8 +4,6 @@ using AspNetCoreRateLimit; using Bit.Core; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; -using Bit.Core.Repositories.Noop; -using Bit.Core.SecretsManager.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Identity.Utilities; @@ -48,8 +46,6 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); - services.AddScoped(); - // Context services.AddScoped(); services.TryAddSingleton(); @@ -155,7 +151,6 @@ public class Startup }); } - public void Configure( IApplicationBuilder app, IWebHostEnvironment env, diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs index aa34ba5e44..bfed60a9a4 100644 --- a/test/Api.Test/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs @@ -11,7 +11,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; -using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -42,6 +42,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IFeatureService _featureService; private readonly ILicensingService _licensingService; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; + private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand; private readonly OrganizationsController _sut; @@ -67,13 +68,14 @@ public class OrganizationsControllerTests : IDisposable _featureService = Substitute.For(); _licensingService = Substitute.For(); _updateSecretsManagerSubscriptionCommand = Substitute.For(); + _upgradeOrganizationPlanCommand = Substitute.For(); _sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext, _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand, _createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand, _cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService, - _updateSecretsManagerSubscriptionCommand); + _updateSecretsManagerSubscriptionCommand, _upgradeOrganizationPlanCommand); } public void Dispose() diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs index 1a3ffdfc08..9210140bc6 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs @@ -3,7 +3,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; -using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs new file mode 100644 index 0000000000..e71e3052ff --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -0,0 +1,113 @@ +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +using Organization = Bit.Core.Entities.Organization; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate; + +[SutProviderCustomize] +public class UpgradeOrganizationPlanCommandTests +{ + [Theory, BitAutoData] + public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(Task.FromResult(null)); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade)); + } + + [Theory, BitAutoData] + public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + organization.GatewayCustomerId = string.Empty; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); + Assert.Contains("no payment method", exception.Message); + } + + [Theory, BitAutoData] + public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + upgrade.Plan = organization.PlanType; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); + Assert.Contains("already on this plan", exception.Message); + } + + [Theory, BitAutoData] + public async Task UpgradePlan_SM_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + upgrade.Plan = organization.PlanType; + upgrade.UseSecretsManager = true; + upgrade.AdditionalSmSeats = 10; + upgrade.AdditionalServiceAccounts = 10; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); + Assert.Contains("already on this plan", exception.Message); + } + + [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData] + public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); + Assert.Contains("can only upgrade", exception.Message); + } + + [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData] + public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + upgrade.UseSecretsManager = true; + upgrade.AdditionalSmSeats = 10; + upgrade.AdditionalServiceAccounts = 10; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); + Assert.Contains("can only upgrade", exception.Message); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + upgrade.AdditionalSmSeats = 10; + upgrade.AdditionalSeats = 10; + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + await sutProvider.GetDependency().Received(1).ReplaceAndUpdateCacheAsync(organization); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + upgrade.AdditionalSmSeats = 10; + upgrade.AdditionalSeats = 10; + var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + await sutProvider.GetDependency().Received(1).ReplaceAndUpdateCacheAsync(organization); + Assert.True(result.Item1); + Assert.NotNull(result.Item2); + } + +} diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs index b2c73c24fc..f3c88663e1 100644 --- a/test/Core.Test/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/Services/OrganizationServiceTests.cs @@ -145,85 +145,6 @@ public class OrganizationServiceTests referenceEvent.Users == expectedNewUsersCount)); } - [Theory, BitAutoData] - public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(Task.FromResult(null)); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade)); - } - - [Theory, BitAutoData] - public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - organization.GatewayCustomerId = string.Empty; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); - Assert.Contains("no payment method", exception.Message); - } - - [Theory, BitAutoData] - public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - upgrade.Plan = organization.PlanType; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); - Assert.Contains("already on this plan", exception.Message); - } - - [Theory, BitAutoData] - public async Task UpgradePlan_SM_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - upgrade.Plan = organization.PlanType; - upgrade.UseSecretsManager = true; - upgrade.AdditionalSmSeats = 10; - upgrade.AdditionalServiceAccounts = 10; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); - Assert.Contains("already on this plan", exception.Message); - } - - [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData] - public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); - Assert.Contains("can only upgrade", exception.Message); - } - - [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData] - public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - upgrade.UseSecretsManager = true; - upgrade.AdditionalSmSeats = 10; - upgrade.AdditionalServiceAccounts = 10; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); - Assert.Contains("can only upgrade", exception.Message); - } - - [Theory] - [FreeOrganizationUpgradeCustomize, BitAutoData] - public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - upgrade.AdditionalSmSeats = 10; - upgrade.AdditionalSeats = 10; - await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); - await sutProvider.GetDependency().Received(1).ReplaceAsync(organization); - } [Theory] [BitAutoData] @@ -327,20 +248,6 @@ public class OrganizationServiceTests Assert.Contains("You can't subtract Service Accounts!", exception.Message); } - [Theory] - [FreeOrganizationUpgradeCustomize, BitAutoData] - public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - upgrade.AdditionalSmSeats = 10; - upgrade.AdditionalSeats = 10; - var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); - await sutProvider.GetDependency().Received(1).ReplaceAsync(organization); - Assert.True(result.Item1); - Assert.NotNull(result.Item2); - } - [Theory] [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Owner), BitAutoData]