mirror of
https://github.com/bitwarden/server
synced 2025-12-16 08:13:33 +00:00
[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 <cokeke@bitwarden.com>
This commit is contained in:
@@ -42,8 +42,6 @@ public class Startup
|
|||||||
// Repositories
|
// Repositories
|
||||||
services.AddDatabaseRepositories(globalSettings);
|
services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
services.AddOosServices();
|
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
services.AddScoped<ICurrentContext, CurrentContext>();
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
services.AddScoped<IScimContext, ScimContext>();
|
services.AddScoped<IScimContext, ScimContext>();
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ using Stripe;
|
|||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Core.Repositories.Noop;
|
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.Utilities;
|
using Bit.Commercial.Core.Utilities;
|
||||||
@@ -88,7 +86,6 @@ public class Startup
|
|||||||
services.AddBaseServices(globalSettings);
|
services.AddBaseServices(globalSettings);
|
||||||
services.AddDefaultServices(globalSettings);
|
services.AddDefaultServices(globalSettings);
|
||||||
services.AddScoped<IAccessControlService, AccessControlService>();
|
services.AddScoped<IAccessControlService, AccessControlService>();
|
||||||
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
|
||||||
|
|
||||||
#if OSS
|
#if OSS
|
||||||
services.AddOosServices();
|
services.AddOosServices();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ using Bit.Core.Models.Business;
|
|||||||
using Bit.Core.Models.Data.Organizations.Policies;
|
using Bit.Core.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.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.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -52,6 +52,7 @@ public class OrganizationsController : Controller
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ILicensingService _licensingService;
|
private readonly ILicensingService _licensingService;
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -73,7 +74,8 @@ public class OrganizationsController : Controller
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILicensingService licensingService,
|
ILicensingService licensingService,
|
||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
|
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@@ -95,6 +97,7 @@ public class OrganizationsController : Controller
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_licensingService = licensingService;
|
_licensingService = licensingService;
|
||||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||||
|
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@@ -310,7 +313,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new NotFoundException();
|
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 };
|
return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using Bit.SharedWeb.Utilities;
|
|||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Core.Auth.Identity;
|
using Bit.Core.Auth.Identity;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.SecretsManager;
|
using Bit.Commercial.Core.SecretsManager;
|
||||||
@@ -133,6 +134,7 @@ public class Startup
|
|||||||
// Services
|
// Services
|
||||||
services.AddBaseServices(globalSettings);
|
services.AddBaseServices(globalSettings);
|
||||||
services.AddDefaultServices(globalSettings);
|
services.AddDefaultServices(globalSettings);
|
||||||
|
services.AddOrganizationSubscriptionServices();
|
||||||
services.AddCoreLocalizationServices();
|
services.AddCoreLocalizationServices();
|
||||||
|
|
||||||
//health check
|
//health check
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
|
|||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
|
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;
|
||||||
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
|
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -45,7 +43,6 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddOrganizationGroupCommands();
|
services.AddOrganizationGroupCommands();
|
||||||
services.AddOrganizationLicenseCommandsQueries();
|
services.AddOrganizationLicenseCommandsQueries();
|
||||||
services.AddOrganizationDomainCommandsQueries();
|
services.AddOrganizationDomainCommandsQueries();
|
||||||
services.AddOrganizationSubscriptionUpdateCommandsQueries();
|
|
||||||
services.AddOrganizationAuthCommands();
|
services.AddOrganizationAuthCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,11 +113,6 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationSubscriptionUpdateCommandsQueries(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddOrganizationAuthCommands(this IServiceCollection services)
|
private static void AddOrganizationAuthCommands(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>();
|
services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
|
|
||||||
public interface IUpdateSecretsManagerSubscriptionCommand
|
public interface IUpdateSecretsManagerSubscriptionCommand
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
|
|
||||||
|
public interface IUpgradeOrganizationPlanCommand
|
||||||
|
{
|
||||||
|
Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, Models.Business.OrganizationUpgrade upgrade);
|
||||||
|
}
|
||||||
@@ -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<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
|
||||||
|
services.AddScoped<IUpgradeOrganizationPlanCommand, UpgradeOrganizationPlanCommand>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.StaticStore;
|
using Bit.Core.Models.StaticStore;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
|
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||||
|
|
||||||
public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand
|
public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand
|
||||||
{
|
{
|
||||||
@@ -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<Tuple<bool, string>> 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<ScimConfig>()?.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<bool, string>(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<Organization> GetOrgById(Guid id)
|
||||||
|
{
|
||||||
|
return await _organizationRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ public interface IOrganizationService
|
|||||||
TaxInfo taxInfo);
|
TaxInfo taxInfo);
|
||||||
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
||||||
Task ReinstateSubscriptionAsync(Guid organizationId);
|
Task ReinstateSubscriptionAsync(Guid organizationId);
|
||||||
Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade);
|
|
||||||
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
||||||
Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats);
|
Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats);
|
||||||
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd, DateTime? prorationDate = null);
|
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd, DateTime? prorationDate = null);
|
||||||
@@ -79,4 +78,7 @@ public interface IOrganizationService
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName);
|
Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName);
|
||||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||||
|
|
||||||
|
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
||||||
|
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Business;
|
using Bit.Core.Auth.Models.Business;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@@ -13,7 +12,6 @@ using Bit.Core.Models.Business;
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.Policies;
|
using Bit.Core.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
@@ -39,7 +37,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly ILicensingService _licensingService;
|
private readonly ILicensingService _licensingService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IInstallationRepository _installationRepository;
|
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
@@ -49,12 +46,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ILogger<OrganizationService> _logger;
|
private readonly ILogger<OrganizationService> _logger;
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -69,7 +64,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
ILicensingService licensingService,
|
ILicensingService licensingService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IInstallationRepository installationRepository,
|
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
@@ -79,12 +73,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ILogger<OrganizationService> logger,
|
ILogger<OrganizationService> logger,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository)
|
||||||
IServiceAccountRepository serviceAccountRepository)
|
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@@ -98,7 +90,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
_licensingService = licensingService;
|
_licensingService = licensingService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_installationRepository = installationRepository;
|
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
@@ -108,12 +99,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
_organizationConnectionRepository = organizationConnectionRepository;
|
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_serviceAccountRepository = serviceAccountRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||||
@@ -170,277 +159,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext));
|
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Tuple<bool, string>> 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<ScimConfig>()?.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<bool, string>(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<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
||||||
{
|
{
|
||||||
var organization = await GetOrgById(organizationId);
|
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");
|
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");
|
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ using AspNetCoreRateLimit;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Repositories.Noop;
|
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Utilities;
|
using Bit.Identity.Utilities;
|
||||||
@@ -48,8 +46,6 @@ public class Startup
|
|||||||
// Repositories
|
// Repositories
|
||||||
services.AddDatabaseRepositories(globalSettings);
|
services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
services.AddScoped<ICurrentContext, CurrentContext>();
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
@@ -155,7 +151,6 @@ public class Startup
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
IApplicationBuilder app,
|
IApplicationBuilder app,
|
||||||
IWebHostEnvironment env,
|
IWebHostEnvironment env,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.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.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -42,6 +42,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ILicensingService _licensingService;
|
private readonly ILicensingService _licensingService;
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||||
|
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
@@ -67,13 +68,14 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_featureService = Substitute.For<IFeatureService>();
|
_featureService = Substitute.For<IFeatureService>();
|
||||||
_licensingService = Substitute.For<ILicensingService>();
|
_licensingService = Substitute.For<ILicensingService>();
|
||||||
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
||||||
|
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
||||||
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
|
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
|
||||||
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
||||||
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
||||||
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService,
|
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService,
|
||||||
_updateSecretsManagerSubscriptionCommand);
|
_updateSecretsManagerSubscriptionCommand, _upgradeOrganizationPlanCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.StaticStore;
|
using Bit.Core.Models.StaticStore;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|||||||
@@ -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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));
|
||||||
|
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organization.GatewayCustomerId = string.Empty;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => 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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
upgrade.Plan = organization.PlanType;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => 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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
upgrade.Plan = organization.PlanType;
|
||||||
|
upgrade.UseSecretsManager = true;
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalServiceAccounts = 10;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => 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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => 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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
upgrade.UseSecretsManager = true;
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalServiceAccounts = 10;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => 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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalSeats = 10;
|
||||||
|
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||||
|
public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalSeats = 10;
|
||||||
|
var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
|
||||||
|
Assert.True(result.Item1);
|
||||||
|
Assert.NotNull(result.Item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -145,85 +145,6 @@ public class OrganizationServiceTests
|
|||||||
referenceEvent.Users == expectedNewUsersCount));
|
referenceEvent.Users == expectedNewUsersCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));
|
|
||||||
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
|
||||||
() => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organization.GatewayCustomerId = string.Empty;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => 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<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
upgrade.Plan = organization.PlanType;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => 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<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
upgrade.Plan = organization.PlanType;
|
|
||||||
upgrade.UseSecretsManager = true;
|
|
||||||
upgrade.AdditionalSmSeats = 10;
|
|
||||||
upgrade.AdditionalServiceAccounts = 10;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => 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<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => 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<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
upgrade.UseSecretsManager = true;
|
|
||||||
upgrade.AdditionalSmSeats = 10;
|
|
||||||
upgrade.AdditionalServiceAccounts = 10;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => 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<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
upgrade.AdditionalSmSeats = 10;
|
|
||||||
upgrade.AdditionalSeats = 10;
|
|
||||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
|
||||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
@@ -327,20 +248,6 @@ public class OrganizationServiceTests
|
|||||||
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
|
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
|
||||||
public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
upgrade.AdditionalSmSeats = 10;
|
|
||||||
upgrade.AdditionalSeats = 10;
|
|
||||||
var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
|
||||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
|
|
||||||
Assert.True(result.Item1);
|
|
||||||
Assert.NotNull(result.Item2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
||||||
InvitorUserType = OrganizationUserType.Owner), BitAutoData]
|
InvitorUserType = OrganizationUserType.Owner), BitAutoData]
|
||||||
|
|||||||
Reference in New Issue
Block a user