mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 23:52:50 -05:00
[AC-2513] Scaling PM seat count with SM seat count (#4040)
* For SM Trial orgs, now scaling PM seat count with SM seat count adjustments * Split Billing related organization endpoints into billing owned controller * Updated billing organizations controller to use a primary constructor to reduce boilerplate * Fixed error where ID couldn't be mapped to subscription endpoint guid param * Updated billing OrganizationController endpoints to not manually create the GUID from the string ID * Banished magic string back to the pit from whence it came * Resolved errors in unit tests
This commit is contained in:
@ -6,7 +6,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
@ -21,20 +20,12 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Queries;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -50,7 +41,6 @@ public class OrganizationsController : Controller
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ISsoConfigService _ssoConfigService;
|
||||
@ -58,17 +48,9 @@ public class OrganizationsController : Controller
|
||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||
private readonly ISubscriberQueries _subscriberQueries;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||
@ -79,7 +61,6 @@ public class OrganizationsController : Controller
|
||||
IPolicyRepository policyRepository,
|
||||
IOrganizationService organizationService,
|
||||
IUserService userService,
|
||||
IPaymentService paymentService,
|
||||
ICurrentContext currentContext,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ISsoConfigService ssoConfigService,
|
||||
@ -87,17 +68,9 @@ public class OrganizationsController : Controller
|
||||
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
||||
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings,
|
||||
ILicensingService licensingService,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||
ISubscriberQueries subscriberQueries,
|
||||
IReferenceEventService referenceEventService,
|
||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
||||
IProviderRepository providerRepository,
|
||||
IScaleSeatsCommand scaleSeatsCommand)
|
||||
@ -107,7 +80,6 @@ public class OrganizationsController : Controller
|
||||
_policyRepository = policyRepository;
|
||||
_organizationService = organizationService;
|
||||
_userService = userService;
|
||||
_paymentService = paymentService;
|
||||
_currentContext = currentContext;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_ssoConfigService = ssoConfigService;
|
||||
@ -115,17 +87,9 @@ public class OrganizationsController : Controller
|
||||
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
||||
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
||||
_featureService = featureService;
|
||||
_globalSettings = globalSettings;
|
||||
_licensingService = licensingService;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
|
||||
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
||||
_subscriberQueries = subscriberQueries;
|
||||
_referenceEventService = referenceEventService;
|
||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||
_providerRepository = providerRepository;
|
||||
_scaleSeatsCommand = scaleSeatsCommand;
|
||||
@ -149,83 +113,6 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationResponseModel(organization);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/billing")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<BillingResponseModel> GetBilling(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ViewBillingHistory(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
||||
return new BillingResponseModel(billingInfo);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/subscription")]
|
||||
public async Task<OrganizationSubscriptionResponseModel> GetSubscription(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.ViewSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!_globalSettings.SelfHosted && organization.Gateway != null)
|
||||
{
|
||||
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
|
||||
if (subscriptionInfo == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var hideSensitiveData = !await _currentContext.EditSubscription(orgIdGuid);
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
|
||||
}
|
||||
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
var orgLicense = await _licensingService.ReadOrganizationLicenseAsync(organization);
|
||||
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
|
||||
}
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/license")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<OrganizationLicense> GetLicense(string id, [FromQuery] Guid installationId)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
|
||||
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
|
||||
if (license == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return license;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()
|
||||
{
|
||||
@ -268,21 +155,6 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/billing-status")]
|
||||
public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
|
||||
{
|
||||
if (!await _currentContext.EditPaymentMethods(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
|
||||
|
||||
return new OrganizationBillingStatusResponseModel(organization, risksSubscriptionFailure);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
|
||||
@ -326,124 +198,6 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationResponseModel(organization);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/payment")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostPayment(string id, [FromBody] PaymentRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditPaymentMethods(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken,
|
||||
model.PaymentMethodType.Value, new TaxInfo
|
||||
{
|
||||
BillingAddressLine1 = model.Line1,
|
||||
BillingAddressLine2 = model.Line2,
|
||||
BillingAddressState = model.State,
|
||||
BillingAddressCity = model.City,
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
BillingAddressCountry = model.Country,
|
||||
TaxIdNumber = model.TaxId,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{id}/upgrade")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostUpgrade(string id, [FromBody] OrganizationUpgradeRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (success, paymentIntentClientSecret) = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
|
||||
|
||||
if (model.UseSecretsManager && success)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
await TryGrantOwnerAccessToSecretsManagerAsync(orgIdGuid, userId);
|
||||
}
|
||||
|
||||
return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret };
|
||||
}
|
||||
|
||||
[HttpPost("{id}/subscription")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostSubscription(string id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
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 organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/subscribe-secrets-manager")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<ProfileOrganizationResponseModel> PostSubscribeSecretsManagerAsync(Guid id, [FromBody] SecretsManagerSubscribeRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!await _currentContext.EditSubscription(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _addSecretsManagerSubscriptionCommand.SignUpAsync(organization, model.AdditionalSmSeats,
|
||||
model.AdditionalServiceAccounts);
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
await TryGrantOwnerAccessToSecretsManagerAsync(organization.Id, userId);
|
||||
|
||||
var organizationDetails = await _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
||||
OrganizationUserStatusType.Confirmed);
|
||||
|
||||
return new ProfileOrganizationResponseModel(organizationDetails);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/seat")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var result = await _organizationService.AdjustSeatsAsync(orgIdGuid, model.SeatAdjustment.Value);
|
||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||
}
|
||||
|
||||
[HttpPost("{id}/storage")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)
|
||||
@ -458,67 +212,6 @@ public class OrganizationsController : Controller
|
||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||
}
|
||||
|
||||
[HttpPost("{id}/verify-bank")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostVerifyBank(string id, [FromBody] OrganizationVerifyBankRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/cancel")]
|
||||
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
if (!await _currentContext.EditSubscription(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var subscription = await _subscriberQueries.GetSubscriptionOrThrow(organization);
|
||||
|
||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||
new OffboardingSurveyResponse
|
||||
{
|
||||
UserId = _currentContext.UserId!.Value,
|
||||
Reason = request.Reason,
|
||||
Feedback = request.Feedback
|
||||
},
|
||||
organization.IsExpired());
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
||||
ReferenceEventType.CancelSubscription,
|
||||
organization,
|
||||
_currentContext)
|
||||
{
|
||||
EndOfPeriod = organization.IsExpired()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reinstate")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstate(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.ReinstateSubscriptionAsync(orgIdGuid);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/leave")]
|
||||
public async Task Leave(string id)
|
||||
{
|
||||
@ -722,55 +415,6 @@ public class OrganizationsController : Controller
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{id}/tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<TaxInfoResponseModel> GetTaxInfo(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var taxInfo = await _paymentService.GetTaxInfoAsync(organization);
|
||||
return new TaxInfoResponseModel(taxInfo);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PutTaxInfo(string id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
TaxIdNumber = model.TaxId,
|
||||
BillingAddressLine1 = model.Line1,
|
||||
BillingAddressLine2 = model.Line2,
|
||||
BillingAddressCity = model.City,
|
||||
BillingAddressState = model.State,
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
BillingAddressCountry = model.Country,
|
||||
};
|
||||
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/public-key")]
|
||||
public async Task<OrganizationPublicKeyResponseModel> GetPublicKey(string id)
|
||||
{
|
||||
@ -912,15 +556,4 @@ public class OrganizationsController : Controller
|
||||
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
||||
.Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value)));
|
||||
}
|
||||
|
||||
private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||
|
||||
if (organizationUser != null)
|
||||
{
|
||||
organizationUser.AccessSecretsManager = true;
|
||||
await _organizationUserRepository.ReplaceAsync(organizationUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user