diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index abb625ce0d..702502ec58 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -25,6 +25,7 @@ using Bit.Core.Models.Api; using Bit.Core.Utilities; using System.Text.Json; using Bit.Core.Models.Data; +using Bit.Core.Settings; namespace Bit.Sso.Controllers { @@ -37,6 +38,7 @@ namespace Bit.Sso.Controllers private readonly ILogger _logger; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationService _organizationService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoUserRepository _ssoUserRepository; private readonly IUserRepository _userRepository; @@ -44,6 +46,7 @@ namespace Bit.Sso.Controllers private readonly IUserService _userService; private readonly II18nService _i18nService; private readonly UserManager _userManager; + private readonly IGlobalSettings _globalSettings; private readonly Core.Services.IEventService _eventService; public AccountController( @@ -53,6 +56,7 @@ namespace Bit.Sso.Controllers ILogger logger, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, + IOrganizationService organizationService, ISsoConfigRepository ssoConfigRepository, ISsoUserRepository ssoUserRepository, IUserRepository userRepository, @@ -60,6 +64,7 @@ namespace Bit.Sso.Controllers IUserService userService, II18nService i18nService, UserManager userManager, + IGlobalSettings globalSettings, Core.Services.IEventService eventService) { _schemeProvider = schemeProvider; @@ -68,6 +73,7 @@ namespace Bit.Sso.Controllers _logger = logger; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; + _organizationService = organizationService; _userRepository = userRepository; _ssoConfigRepository = ssoConfigRepository; _ssoUserRepository = ssoUserRepository; @@ -76,6 +82,7 @@ namespace Bit.Sso.Controllers _i18nService = i18nService; _userManager = userManager; _eventService = eventService; + _globalSettings = globalSettings; } [HttpGet] @@ -469,10 +476,34 @@ namespace Bit.Sso.Controllers if (orgUser == null && organization.Seats.HasValue) { var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId); - var availableSeats = organization.Seats.Value - userCount; + var initialSeatCount = organization.Seats.Value; + var availableSeats = initialSeatCount - userCount; + var prorationDate = DateTime.UtcNow; if (availableSeats < 1) { - throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name)); + try + { + if (_globalSettings.SelfHosted) + { + throw new Exception("Cannot autoscale on self-hosted instance."); + } + + var paymentIntentClientSecret = await _organizationService.AdjustSeatsAsync(orgId, 1, prorationDate); + organization = await _organizationRepository.GetByIdAsync(orgId); + if (!string.IsNullOrEmpty(paymentIntentClientSecret)) + { + throw new Exception("Stripe payment required client-side confirmation."); + } + } + catch (Exception e) + { + if (organization.Seats.Value != initialSeatCount) + { + await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate); + } + _logger.LogInformation(e, "SSO auto provisioning failed"); + throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name)); + } } } diff --git a/bitwarden_license/src/Sso/Startup.cs b/bitwarden_license/src/Sso/Startup.cs index 5678dace21..4a824e8f9f 100644 --- a/bitwarden_license/src/Sso/Startup.cs +++ b/bitwarden_license/src/Sso/Startup.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Logging; +using Stripe; namespace Bit.Sso { @@ -34,6 +35,9 @@ namespace Bit.Sso // Settings var globalSettings = services.AddGlobalSettingsServices(Configuration); + // Stripe Billing + StripeConfiguration.ApiKey = globalSettings.StripeApiKey; + // Data Protection services.AddCustomDataProtectionServices(Environment, globalSettings); diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 9b93af393b..5f4a5ecc36 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Bit.Core.Models.Table; using Bit.Core.Models.Business; using Bit.Core.Enums; @@ -15,8 +16,8 @@ namespace Bit.Core.Services short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, short additionalStorageGb, TaxInfo taxInfo); - Task AdjustSeatsAsync(Organization organization, Models.StaticStore.Plan plan, int additionalSeats); - Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); + Task AdjustSeatsAsync(Organization organization, Models.StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null); + Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, bool skipInAppPurchaseCheck = false); Task ReinstateSubscriptionAsync(ISubscriber subscriber); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 77994eaf46..7da7115395 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -477,7 +477,7 @@ namespace Bit.Core.Services } } - var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats); + var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats, prorationDate); await _referenceEventService.RaiseEventAsync( new ReferenceEvent(ReferenceEventType.AdjustSeats, organization) { diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index a29ad599ee..690391d78b 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -679,7 +679,7 @@ namespace Bit.Core.Services } private async Task FinalizeSubscriptionChangeAsync(IStorableSubscriber storableSubscriber, - SubscriptionUpdate subscriptionUpdate) + SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate) { var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId); if (sub == null) @@ -687,7 +687,7 @@ namespace Bit.Core.Services throw new GatewayException("Subscription not found."); } - var prorationDate = DateTime.UtcNow; + prorationDate ??= DateTime.UtcNow; var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; @@ -793,15 +793,15 @@ namespace Bit.Core.Services return paymentIntentClientSecret; } - public Task AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) + public Task AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null) { - return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats)); + return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate); } public Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, - string storagePlanId) + string storagePlanId, DateTime? prorationDate = null) { - return FinalizeSubscriptionChangeAsync(storableSubscriber, new StorageSubscriptionUpdate(storagePlanId, additionalStorage)); + return FinalizeSubscriptionChangeAsync(storableSubscriber, new StorageSubscriptionUpdate(storagePlanId, additionalStorage), prorationDate); } public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)