mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[AC-1486] Feature: SM Billing (#3073)
* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem (#3037) * [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem * [AC-1423] Add helper to StaticStore.cs to find a Plan by StripePlanId * [AC-1423] Use the helper method to set SubscriptionInfo.BitwardenProduct * Add SecretsManagerBilling feature flag to Constants * [AC 1409] Secrets Manager Subscription Stripe Integration (#3019) * [AC-1418] Add missing SecretsManagerPlan property to OrganizationResponseModel (#3055) * [AC 1460] Update Stripe Configuration (#3070) * [AC 1410] Secrets Manager subscription adjustment back-end changes (#3036) * Create UpgradeSecretsManagerSubscription command * [AC-1495] Extract UpgradePlanAsync into a command (#3081) * This is a pure lift & shift with no refactors * [AC-1503] Fix Stripe integration on organization upgrade (#3084) * Fix SM parameters not being passed to Stripe * [AC-1504] Allow SM max autoscale limits to be disabled (#3085) * [AC-1488] Changed SM Signup and Upgrade paths to set SmServiceAccounts to include the plan BaseServiceAccount (#3086) * [AC-1510] Enable access to Secrets Manager to Organization owner for new Subscription (#3089) * Revert changes to ReferenceEvent code (#3091) This will be done in AC-1481 * Add UsePasswordManager to sync data (#3114) * [AC-1522] Fix service account check on upgrading (#3111) * [AC-1521] Address checkmarx security feedback (#3124) * Reinstate target attribute but add noopener noreferrer * Update date on migration script --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com> Co-authored-by: Rui Tome <rtome@bitwarden.com>
This commit is contained in:
@ -18,6 +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.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -50,6 +51,8 @@ public class OrganizationsController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -70,7 +73,9 @@ public class OrganizationsController : Controller
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings,
|
||||
ILicensingService licensingService)
|
||||
ILicensingService licensingService,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -91,6 +96,8 @@ public class OrganizationsController : Controller
|
||||
_featureService = featureService;
|
||||
_globalSettings = globalSettings;
|
||||
_licensingService = licensingService;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -306,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 };
|
||||
}
|
||||
|
||||
@ -319,10 +326,34 @@ public class OrganizationsController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.UpdateSubscription(orgIdGuid, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/sm-subscription")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostSmSubscription(Guid id, [FromBody] SecretsManagerSubscriptionUpdateRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!await _currentContext.EditSubscription(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secretsManagerPlan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
|
||||
if (secretsManagerPlan == null)
|
||||
{
|
||||
throw new NotFoundException("Invalid Secrets Manager plan.");
|
||||
}
|
||||
|
||||
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, secretsManagerPlan);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSecretsManagerSubscription(organizationUpdate);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/seat")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
||||
|
@ -40,6 +40,12 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
[StringLength(2)]
|
||||
public string BillingAddressCountry { get; set; }
|
||||
public int? MaxAutoscaleSeats { get; set; }
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? AdditionalSmSeats { get; set; }
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? AdditionalServiceAccounts { get; set; }
|
||||
[Required]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
|
||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||
{
|
||||
@ -58,6 +64,9 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
BillingEmail = BillingEmail,
|
||||
BusinessName = BusinessName,
|
||||
CollectionName = CollectionName,
|
||||
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),
|
||||
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),
|
||||
UseSecretsManager = UseSecretsManager,
|
||||
TaxInfo = new TaxInfo
|
||||
{
|
||||
TaxIdNumber = TaxIdNumber,
|
||||
|
@ -13,6 +13,12 @@ public class OrganizationUpgradeRequestModel
|
||||
public int AdditionalSeats { get; set; }
|
||||
[Range(0, 99)]
|
||||
public short? AdditionalStorageGb { get; set; }
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? AdditionalSmSeats { get; set; }
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? AdditionalServiceAccounts { get; set; }
|
||||
[Required]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool PremiumAccessAddon { get; set; }
|
||||
public string BillingAddressCountry { get; set; }
|
||||
public string BillingAddressPostalCode { get; set; }
|
||||
@ -24,6 +30,9 @@ public class OrganizationUpgradeRequestModel
|
||||
{
|
||||
AdditionalSeats = AdditionalSeats,
|
||||
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(),
|
||||
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(0),
|
||||
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(0),
|
||||
UseSecretsManager = UseSecretsManager,
|
||||
BusinessName = BusinessName,
|
||||
Plan = PlanType,
|
||||
PremiumAccessAddon = PremiumAccessAddon,
|
||||
|
@ -0,0 +1,45 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Api.Models.Request.Organizations;
|
||||
|
||||
public class SecretsManagerSubscriptionUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
public int SeatAdjustment { get; set; }
|
||||
public int? MaxAutoscaleSeats { get; set; }
|
||||
public int ServiceAccountAdjustment { get; set; }
|
||||
public int? MaxAutoscaleServiceAccounts { get; set; }
|
||||
|
||||
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
|
||||
{
|
||||
var newTotalSeats = organization.SmSeats.GetValueOrDefault() + SeatAdjustment;
|
||||
var newTotalServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + ServiceAccountAdjustment;
|
||||
|
||||
var orgUpdate = new SecretsManagerSubscriptionUpdate
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
|
||||
SmSeatsAdjustment = SeatAdjustment,
|
||||
SmSeats = newTotalSeats,
|
||||
SmSeatsExcludingBase = newTotalSeats - plan.BaseSeats,
|
||||
|
||||
MaxAutoscaleSmSeats = MaxAutoscaleSeats,
|
||||
|
||||
SmServiceAccountsAdjustment = ServiceAccountAdjustment,
|
||||
SmServiceAccounts = newTotalServiceAccounts,
|
||||
SmServiceAccountsExcludingBase = newTotalServiceAccounts - plan.BaseServiceAccount.GetValueOrDefault(),
|
||||
|
||||
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts,
|
||||
|
||||
MaxAutoscaleSmSeatsChanged =
|
||||
MaxAutoscaleSeats.GetValueOrDefault() != organization.MaxAutoscaleSmSeats.GetValueOrDefault(),
|
||||
MaxAutoscaleSmServiceAccountsChanged =
|
||||
MaxAutoscaleServiceAccounts.GetValueOrDefault() != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
|
||||
};
|
||||
|
||||
return orgUpdate;
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
BusinessTaxNumber = organization.BusinessTaxNumber;
|
||||
BillingEmail = organization.BillingEmail;
|
||||
Plan = new PlanResponseModel(StaticStore.PasswordManagerPlans.FirstOrDefault(plan => plan.Type == organization.PlanType));
|
||||
SecretsManagerPlan = new PlanResponseModel(StaticStore.SecretManagerPlans.FirstOrDefault(plan => plan.Type == organization.PlanType));
|
||||
PlanType = organization.PlanType;
|
||||
Seats = organization.Seats;
|
||||
MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
|
||||
@ -65,6 +66,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public string BusinessTaxNumber { get; set; }
|
||||
public string BillingEmail { get; set; }
|
||||
public PlanResponseModel Plan { get; set; }
|
||||
public PlanResponseModel SecretsManagerPlan { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public int? MaxAutoscaleSeats { get; set; } = null;
|
||||
|
@ -55,10 +55,12 @@ public class PlanResponseModel : ResponseModel
|
||||
|
||||
AdditionalPricePerServiceAccount = plan.AdditionalPricePerServiceAccount;
|
||||
BaseServiceAccount = plan.BaseServiceAccount;
|
||||
MaxServiceAccount = plan.MaxServiceAccount;
|
||||
MaxServiceAccounts = plan.MaxServiceAccounts;
|
||||
MaxAdditionalServiceAccounts = plan.MaxAdditionalServiceAccount;
|
||||
HasAdditionalServiceAccountOption = plan.HasAdditionalServiceAccountOption;
|
||||
MaxProjects = plan.MaxProjects;
|
||||
BitwardenProduct = plan.BitwardenProduct;
|
||||
StripeServiceAccountPlanId = plan.StripeServiceAccountPlanId;
|
||||
}
|
||||
|
||||
public PlanType Type { get; set; }
|
||||
@ -105,10 +107,11 @@ public class PlanResponseModel : ResponseModel
|
||||
public decimal SeatPrice { get; set; }
|
||||
public decimal AdditionalStoragePricePerGb { get; set; }
|
||||
public decimal PremiumAccessOptionPrice { get; set; }
|
||||
|
||||
public string StripeServiceAccountPlanId { get; set; }
|
||||
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
||||
public short? BaseServiceAccount { get; set; }
|
||||
public short? MaxServiceAccount { get; set; }
|
||||
public short? MaxServiceAccounts { get; set; }
|
||||
public short? MaxAdditionalServiceAccounts { get; set; }
|
||||
public bool HasAdditionalServiceAccountOption { get; set; }
|
||||
public short? MaxProjects { get; set; }
|
||||
public BitwardenProductType BitwardenProduct { get; set; }
|
||||
|
@ -29,6 +29,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
UseApi = organization.UseApi;
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseSecretsManager = organization.UseSecretsManager;
|
||||
UsePasswordManager = organization.UsePasswordManager;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UseActivateAutofillPolicy = organization.PlanType == PlanType.EnterpriseAnnually ||
|
||||
@ -82,6 +83,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UseActivateAutofillPolicy { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Utilities;
|
||||
@ -82,6 +83,8 @@ public class BillingSubscription
|
||||
Interval = item.Interval;
|
||||
Quantity = item.Quantity;
|
||||
SponsoredSubscriptionItem = item.SponsoredSubscriptionItem;
|
||||
AddonSubscriptionItem = item.AddonSubscriptionItem;
|
||||
BitwardenProduct = item.BitwardenProduct;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
@ -89,6 +92,8 @@ public class BillingSubscription
|
||||
public int Quantity { get; set; }
|
||||
public string Interval { get; set; }
|
||||
public bool SponsoredSubscriptionItem { get; set; }
|
||||
public bool AddonSubscriptionItem { get; set; }
|
||||
public BitwardenProductType BitwardenProduct { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user