1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00
Conner Turnbull 721d2969d4
[PM-8830] Billing Enums Rename (#4180)
* Renamed ProductType to ProductTierType

* Renamed Product properties to ProductTier

* Moved ProductTierType to Bit.Core.Billing.Enums namespace from Bit.Core.Enums

* Moved PlanType enum to Bit.Core.Billing.Enums

* Moved StaticStore to Bit.Core.Billing.Models.StaticStore namespace

* Added ProductType enum

* dotnet format
2024-06-14 15:34:47 -04:00

368 lines
17 KiB
C#

using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Context;
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 existingPlan = StaticStore.GetPlan(organization.PlanType);
if (existingPlan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (newPlan == null)
{
throw new BadRequestException("Plan not found.");
}
if (existingPlan.Type == newPlan.Type)
{
throw new BadRequestException("Organization is already on this plan.");
}
if (existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder)
{
throw new BadRequestException("You cannot upgrade to this plan.");
}
_organizationService.ValidatePasswordManagerPlan(newPlan, upgrade);
if (upgrade.UseSecretsManager)
{
_organizationService.ValidateSecretsManagerPlan(newPlan, upgrade);
}
var updatedPasswordManagerSeats = (short)(newPlan.PasswordManager.BaseSeats +
(newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)
{
var occupiedSeats =
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > updatedPasswordManagerSeats)
{
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.");
}
}
if (newPlan.PasswordManager.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
organization.MaxCollections.Value >
newPlan.PasswordManager.MaxCollections.Value))
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if (collectionCount > newPlan.PasswordManager.MaxCollections.Value)
{
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
$"Your new plan allows for a maximum of ({newPlan.PasswordManager.MaxCollections.Value}) collections. " +
"Remove some collections.");
}
}
if (!newPlan.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 (!newPlan.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 (!newPlan.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 (!newPlan.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 (!newPlan.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 (!newPlan.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 (!newPlan.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)
{
await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newPlan);
}
// TODO: Check storage?
string paymentIntentClientSecret = null;
var success = true;
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
newPlan, upgrade);
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
}
else
{
paymentIntentClientSecret = await _paymentService.AdjustSubscription(
organization,
newPlan,
upgrade.AdditionalSeats,
upgrade.UseSecretsManager,
upgrade.AdditionalSmSeats,
upgrade.AdditionalServiceAccounts,
upgrade.AdditionalStorageGb);
success = string.IsNullOrEmpty(paymentIntentClientSecret);
}
organization.BusinessName = upgrade.BusinessName;
organization.PlanType = newPlan.Type;
organization.Seats = (short)(newPlan.PasswordManager.BaseSeats + upgrade.AdditionalSeats);
organization.MaxCollections = newPlan.PasswordManager.MaxCollections;
organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents;
organization.UseTotp = newPlan.HasTotp;
organization.Use2fa = newPlan.Has2fa;
organization.UseApi = newPlan.HasApi;
organization.SelfHost = newPlan.HasSelfHost;
organization.UsePolicies = newPlan.HasPolicies;
organization.MaxStorageGb = !newPlan.PasswordManager.BaseStorageGb.HasValue
? (short?)null
: (short)(newPlan.PasswordManager.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents;
organization.UseTotp = newPlan.HasTotp;
organization.Use2fa = newPlan.Has2fa;
organization.UseApi = newPlan.HasApi;
organization.UseSso = newPlan.HasSso;
organization.UseKeyConnector = newPlan.HasKeyConnector;
organization.UseScim = newPlan.HasScim;
organization.UseResetPassword = newPlan.HasResetPassword;
organization.SelfHost = newPlan.HasSelfHost;
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
organization.UseCustomPermissions = newPlan.HasCustomPermissions;
organization.Plan = newPlan.Name;
organization.Enabled = success;
organization.PublicKey = upgrade.PublicKey;
organization.PrivateKey = upgrade.PrivateKey;
organization.UsePasswordManager = true;
organization.UseSecretsManager = upgrade.UseSecretsManager;
if (upgrade.UseSecretsManager)
{
organization.SmSeats = newPlan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault();
organization.SmServiceAccounts = newPlan.SecretsManager.BaseServiceAccount +
upgrade.AdditionalServiceAccounts.GetValueOrDefault();
}
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
if (success)
{
var upgradePath = GetUpgradePath(existingPlan.ProductTier, newPlan.ProductTier);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext)
{
PlanName = newPlan.Name,
PlanType = newPlan.Type,
OldPlanName = existingPlan.Name,
OldPlanType = existingPlan.Type,
Seats = organization.Seats,
SignupInitiationPath = "Upgrade in-product",
PlanUpgradePath = upgradePath,
Storage = organization.MaxStorageGb,
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
});
}
return new Tuple<bool, string>(success, paymentIntentClientSecret);
}
private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization,
Models.StaticStore.Plan newSecretsManagerPlan)
{
var newPlanSmSeats = (short)(newSecretsManagerPlan.SecretsManager.BaseSeats +
(newSecretsManagerPlan.SecretsManager.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 or increase your subscription.");
}
}
var additionalServiceAccounts = newSecretsManagerPlan.SecretsManager.HasAdditionalServiceAccountOption
? upgrade.AdditionalServiceAccounts
: 0;
var newPlanServiceAccounts = newSecretsManagerPlan.SecretsManager.BaseServiceAccount + additionalServiceAccounts;
if (!organization.SmServiceAccounts.HasValue || organization.SmServiceAccounts.Value > newPlanServiceAccounts)
{
var currentServiceAccounts =
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > newPlanServiceAccounts)
{
throw new BadRequestException(
$"Your organization currently has {currentServiceAccounts} machine accounts. " +
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} machine accounts. " +
"Remove some machine accounts or increase your subscription.");
}
}
}
private async Task<Organization> GetOrgById(Guid id)
{
return await _organizationRepository.GetByIdAsync(id);
}
private static string GetUpgradePath(ProductTierType oldProductTierType, ProductTierType newProductTierType)
{
var oldDescription = _upgradePath.TryGetValue(oldProductTierType, out var description)
? description
: $"{oldProductTierType:G}";
var newDescription = _upgradePath.TryGetValue(newProductTierType, out description)
? description
: $"{newProductTierType:G}";
return $"{oldDescription} → {newDescription}";
}
private static readonly Dictionary<ProductTierType, string> _upgradePath = new()
{
[ProductTierType.Free] = "2-person org",
[ProductTierType.Families] = "Families",
[ProductTierType.TeamsStarter] = "Teams Starter",
[ProductTierType.Teams] = "Teams",
[ProductTierType.Enterprise] = "Enterprise"
};
}