mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +01: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:
parent
0b5c21acca
commit
a60180230d
@ -6,7 +6,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
|
|||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Auth.Models.Request.Organizations;
|
using Bit.Api.Auth.Models.Request.Organizations;
|
||||||
using Bit.Api.Auth.Models.Response.Organizations;
|
using Bit.Api.Auth.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Request;
|
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@ -21,20 +20,12 @@ using Bit.Core.Auth.Repositories;
|
|||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
|
||||||
using Bit.Core.Billing.Queries;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
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.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
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 Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -50,7 +41,6 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly ISsoConfigService _ssoConfigService;
|
private readonly ISsoConfigService _ssoConfigService;
|
||||||
@ -58,17 +48,9 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
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 IPushNotificationService _pushNotificationService;
|
||||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
|
||||||
private readonly ISubscriberQueries _subscriberQueries;
|
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
@ -79,7 +61,6 @@ public class OrganizationsController : Controller
|
|||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPaymentService paymentService,
|
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
ISsoConfigService ssoConfigService,
|
ISsoConfigService ssoConfigService,
|
||||||
@ -87,17 +68,9 @@ public class OrganizationsController : Controller
|
|||||||
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
||||||
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILicensingService licensingService,
|
|
||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
|
||||||
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
|
||||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
|
||||||
ISubscriberQueries subscriberQueries,
|
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IScaleSeatsCommand scaleSeatsCommand)
|
IScaleSeatsCommand scaleSeatsCommand)
|
||||||
@ -107,7 +80,6 @@ public class OrganizationsController : Controller
|
|||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_paymentService = paymentService;
|
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_ssoConfigService = ssoConfigService;
|
_ssoConfigService = ssoConfigService;
|
||||||
@ -115,17 +87,9 @@ public class OrganizationsController : Controller
|
|||||||
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
||||||
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_licensingService = licensingService;
|
|
||||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
|
||||||
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
|
|
||||||
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
|
||||||
_subscriberQueries = subscriberQueries;
|
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_scaleSeatsCommand = scaleSeatsCommand;
|
_scaleSeatsCommand = scaleSeatsCommand;
|
||||||
@ -149,83 +113,6 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationResponseModel(organization);
|
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("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()
|
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()
|
||||||
{
|
{
|
||||||
@ -268,21 +155,6 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
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("")]
|
[HttpPost("")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
|
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
|
||||||
@ -326,124 +198,6 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationResponseModel(organization);
|
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")]
|
[HttpPost("{id}/storage")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)
|
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 };
|
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")]
|
[HttpPost("{id}/leave")]
|
||||||
public async Task Leave(string id)
|
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")]
|
[HttpGet("{id}/public-key")]
|
||||||
public async Task<OrganizationPublicKeyResponseModel> GetPublicKey(string id)
|
public async Task<OrganizationPublicKeyResponseModel> GetPublicKey(string id)
|
||||||
{
|
{
|
||||||
@ -912,15 +556,4 @@ public class OrganizationsController : Controller
|
|||||||
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
||||||
.Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value)));
|
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.Billing.Queries;
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -8,7 +14,10 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("organizations/{organizationId:guid}/billing")]
|
[Route("organizations/{organizationId:guid}/billing")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class OrganizationBillingController(
|
public class OrganizationBillingController(
|
||||||
IOrganizationBillingQueries organizationBillingQueries) : Controller
|
IOrganizationBillingQueries organizationBillingQueries,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IPaymentService paymentService) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("metadata")]
|
[HttpGet("metadata")]
|
||||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||||
@ -24,4 +33,23 @@ public class OrganizationBillingController(
|
|||||||
|
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<BillingResponseModel> GetBilling(Guid organizationId)
|
||||||
|
{
|
||||||
|
if (!await currentContext.ViewBillingHistory(organizationId))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var billingInfo = await paymentService.GetBillingAsync(organization);
|
||||||
|
return new BillingResponseModel(billingInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
385
src/Api/Billing/Controllers/OrganizationsController.cs
Normal file
385
src/Api/Billing/Controllers/OrganizationsController.cs
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.AdminConsole.Models.Response;
|
||||||
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
|
using Bit.Api.Models.Request;
|
||||||
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
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;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Route("organizations")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class OrganizationsController(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
IUserService userService,
|
||||||
|
IPaymentService paymentService,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
ILicensingService licensingService,
|
||||||
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
|
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
||||||
|
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||||
|
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||||
|
ISubscriberQueries subscriberQueries,
|
||||||
|
IReferenceEventService referenceEventService)
|
||||||
|
: Controller
|
||||||
|
{
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/subscription")]
|
||||||
|
public async Task<OrganizationSubscriptionResponseModel> GetSubscription(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.ViewSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
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(id);
|
||||||
|
|
||||||
|
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:guid}/license")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<OrganizationLicense> GetLicense(Guid id, [FromQuery] Guid installationId)
|
||||||
|
{
|
||||||
|
if (!await currentContext.OrganizationOwner(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var org = await organizationRepository.GetByIdAsync(id);
|
||||||
|
var license = await cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
|
||||||
|
if (license == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return license;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/payment")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditPaymentMethods(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.ReplacePaymentMethodAsync(id, 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:guid}/upgrade")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (success, paymentIntentClientSecret) = await upgradeOrganizationPlanCommand.UpgradePlanAsync(id, model.ToOrganizationUpgrade());
|
||||||
|
|
||||||
|
if (model.UseSecretsManager && success)
|
||||||
|
{
|
||||||
|
var userId = userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
await TryGrantOwnerAccessToSecretsManagerAsync(id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret };
|
||||||
|
}
|
||||||
|
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
|
||||||
|
organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model);
|
||||||
|
|
||||||
|
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization);
|
||||||
|
|
||||||
|
await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/subscription")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostSubscription(Guid id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
await organizationService.UpdateSubscription(id, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/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:guid}/seat")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<PaymentResponseModel> PostSeat(Guid id, [FromBody] OrganizationSeatRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await organizationService.AdjustSeatsAsync(id, model.SeatAdjustment.Value);
|
||||||
|
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/verify-bank")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostVerifyBank(Guid id, [FromBody] OrganizationVerifyBankRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.VerifyBankAsync(id, 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:guid}/reinstate")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostReinstate(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.ReinstateSubscriptionAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/tax")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<TaxInfoResponseModel> GetTaxInfo(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.OrganizationOwner(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxInfo = await paymentService.GetTaxInfoAsync(organization);
|
||||||
|
return new TaxInfoResponseModel(taxInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/tax")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PutTaxInfo(Guid id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.OrganizationOwner(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to grant owner access to the Secrets Manager for the organization
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts the organization seats for the Secrets Manager trial to match the new seat count for secrets manager
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="organization"></param>
|
||||||
|
/// <param name="model"></param>
|
||||||
|
private async Task<Organization> AdjustOrganizationSeatsForSmTrialAsync(Guid id, Organization organization,
|
||||||
|
SecretsManagerSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) ||
|
||||||
|
string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId) ||
|
||||||
|
model.SeatAdjustment == 0)
|
||||||
|
{
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
|
||||||
|
if (subscriptionInfo?.CustomerDiscount?.Id != StripeConstants.CouponIDs.SecretsManagerStandalone)
|
||||||
|
{
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.UpdateSubscription(id, model.SeatAdjustment, null);
|
||||||
|
|
||||||
|
return await organizationRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using AutoFixture.Xunit2;
|
using AutoFixture.Xunit2;
|
||||||
using Bit.Api.AdminConsole.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -16,21 +14,14 @@ using Bit.Core.Auth.Models.Data;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Queries;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
|
||||||
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.Tools.Services;
|
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
@ -43,7 +34,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly ISsoConfigService _ssoConfigService;
|
private readonly ISsoConfigService _ssoConfigService;
|
||||||
@ -51,17 +41,9 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;
|
private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;
|
||||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
|
||||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ILicensingService _licensingService;
|
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
|
||||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
|
||||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
|
||||||
private readonly ISubscriberQueries _subscriberQueries;
|
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
@ -75,7 +57,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
_organizationService = Substitute.For<IOrganizationService>();
|
_organizationService = Substitute.For<IOrganizationService>();
|
||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_paymentService = Substitute.For<IPaymentService>();
|
|
||||||
_policyRepository = Substitute.For<IPolicyRepository>();
|
_policyRepository = Substitute.For<IPolicyRepository>();
|
||||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||||
_ssoConfigService = Substitute.For<ISsoConfigService>();
|
_ssoConfigService = Substitute.For<ISsoConfigService>();
|
||||||
@ -83,17 +64,9 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_rotateOrganizationApiKeyCommand = Substitute.For<IRotateOrganizationApiKeyCommand>();
|
_rotateOrganizationApiKeyCommand = Substitute.For<IRotateOrganizationApiKeyCommand>();
|
||||||
_organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();
|
_organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();
|
||||||
_userService = Substitute.For<IUserService>();
|
_userService = Substitute.For<IUserService>();
|
||||||
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
|
|
||||||
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
||||||
_featureService = Substitute.For<IFeatureService>();
|
_featureService = Substitute.For<IFeatureService>();
|
||||||
_licensingService = Substitute.For<ILicensingService>();
|
|
||||||
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
|
||||||
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
|
|
||||||
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
|
|
||||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||||
_cancelSubscriptionCommand = Substitute.For<ICancelSubscriptionCommand>();
|
|
||||||
_subscriberQueries = Substitute.For<ISubscriberQueries>();
|
|
||||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
|
||||||
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
|
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
|
||||||
_providerRepository = Substitute.For<IProviderRepository>();
|
_providerRepository = Substitute.For<IProviderRepository>();
|
||||||
_scaleSeatsCommand = Substitute.For<IScaleSeatsCommand>();
|
_scaleSeatsCommand = Substitute.For<IScaleSeatsCommand>();
|
||||||
@ -104,7 +77,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_policyRepository,
|
_policyRepository,
|
||||||
_organizationService,
|
_organizationService,
|
||||||
_userService,
|
_userService,
|
||||||
_paymentService,
|
|
||||||
_currentContext,
|
_currentContext,
|
||||||
_ssoConfigRepository,
|
_ssoConfigRepository,
|
||||||
_ssoConfigService,
|
_ssoConfigService,
|
||||||
@ -112,17 +84,9 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_rotateOrganizationApiKeyCommand,
|
_rotateOrganizationApiKeyCommand,
|
||||||
_createOrganizationApiKeyCommand,
|
_createOrganizationApiKeyCommand,
|
||||||
_organizationApiKeyRepository,
|
_organizationApiKeyRepository,
|
||||||
_cloudGetOrganizationLicenseQuery,
|
|
||||||
_featureService,
|
_featureService,
|
||||||
_globalSettings,
|
_globalSettings,
|
||||||
_licensingService,
|
|
||||||
_updateSecretsManagerSubscriptionCommand,
|
|
||||||
_upgradeOrganizationPlanCommand,
|
|
||||||
_addSecretsManagerSubscriptionCommand,
|
|
||||||
_pushNotificationService,
|
_pushNotificationService,
|
||||||
_cancelSubscriptionCommand,
|
|
||||||
_subscriberQueries,
|
|
||||||
_referenceEventService,
|
|
||||||
_organizationEnableCollectionEnhancementsCommand,
|
_organizationEnableCollectionEnhancementsCommand,
|
||||||
_providerRepository,
|
_providerRepository,
|
||||||
_scaleSeatsCommand);
|
_scaleSeatsCommand);
|
||||||
@ -193,196 +157,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
|
|
||||||
Guid organizationId,
|
|
||||||
OrganizationUpgradeRequestModel model)
|
|
||||||
{
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(false);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId.ToString(), model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
OrganizationUpgradeRequestModel model,
|
|
||||||
bool success,
|
|
||||||
string paymentIntentClientSecret)
|
|
||||||
{
|
|
||||||
model.UseSecretsManager = false;
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
|
||||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
|
||||||
|
|
||||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
|
||||||
|
|
||||||
Assert.Equal(success, response.Success);
|
|
||||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUpgradeRequestModel model,
|
|
||||||
bool success,
|
|
||||||
string paymentIntentClientSecret,
|
|
||||||
OrganizationUser organizationUser)
|
|
||||||
{
|
|
||||||
model.UseSecretsManager = true;
|
|
||||||
organizationUser.AccessSecretsManager = false;
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
|
||||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);
|
|
||||||
|
|
||||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
|
||||||
|
|
||||||
Assert.Equal(success, response.Success);
|
|
||||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
|
||||||
|
|
||||||
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
|
||||||
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUpgradeRequestModel model,
|
|
||||||
bool success,
|
|
||||||
string paymentIntentClientSecret)
|
|
||||||
{
|
|
||||||
model.UseSecretsManager = true;
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
|
||||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();
|
|
||||||
|
|
||||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
|
||||||
|
|
||||||
Assert.Equal(success, response.Success);
|
|
||||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
|
||||||
|
|
||||||
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model)
|
|
||||||
{
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).ReturnsNull();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model,
|
|
||||||
Organization organization)
|
|
||||||
{
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(false);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model,
|
|
||||||
Organization organization,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUser organizationUser,
|
|
||||||
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
|
||||||
{
|
|
||||||
organizationUser.AccessSecretsManager = false;
|
|
||||||
|
|
||||||
var ssoConfigurationData = new SsoConfigurationData
|
|
||||||
{
|
|
||||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
|
||||||
KeyConnectorUrl = "https://example.com"
|
|
||||||
};
|
|
||||||
|
|
||||||
organizationUserOrganizationDetails.Permissions = string.Empty;
|
|
||||||
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
|
||||||
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
|
||||||
.Returns(organizationUserOrganizationDetails);
|
|
||||||
|
|
||||||
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
|
||||||
|
|
||||||
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
|
||||||
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
|
||||||
|
|
||||||
await _addSecretsManagerSubscriptionCommand.Received(1)
|
|
||||||
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
|
||||||
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
|
||||||
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model,
|
|
||||||
Organization organization,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
|
||||||
{
|
|
||||||
var ssoConfigurationData = new SsoConfigurationData
|
|
||||||
{
|
|
||||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
|
||||||
KeyConnectorUrl = "https://example.com"
|
|
||||||
};
|
|
||||||
|
|
||||||
organizationUserOrganizationDetails.Permissions = string.Empty;
|
|
||||||
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
|
||||||
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();
|
|
||||||
|
|
||||||
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
|
||||||
.Returns(organizationUserOrganizationDetails);
|
|
||||||
|
|
||||||
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
|
||||||
|
|
||||||
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
|
||||||
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
|
||||||
|
|
||||||
await _addSecretsManagerSubscriptionCommand.Received(1)
|
|
||||||
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
|
||||||
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
[Theory, AutoData]
|
||||||
public async Task EnableCollectionEnhancements_Success(Organization organization)
|
public async Task EnableCollectionEnhancements_Success(Organization organization)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,317 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using AutoFixture.Xunit2;
|
||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Billing.Controllers;
|
||||||
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Auth.Services;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Xunit;
|
||||||
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Billing.Controllers;
|
||||||
|
|
||||||
|
public class OrganizationsControllerTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||||
|
private readonly ILicensingService _licensingService;
|
||||||
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||||
|
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||||
|
private readonly ISubscriberQueries _subscriberQueries;
|
||||||
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
|
|
||||||
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
|
public OrganizationsControllerTests()
|
||||||
|
{
|
||||||
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
|
_globalSettings = Substitute.For<GlobalSettings>();
|
||||||
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
|
_organizationService = Substitute.For<IOrganizationService>();
|
||||||
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
|
_paymentService = Substitute.For<IPaymentService>();
|
||||||
|
Substitute.For<IPolicyRepository>();
|
||||||
|
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||||
|
Substitute.For<ISsoConfigService>();
|
||||||
|
_userService = Substitute.For<IUserService>();
|
||||||
|
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
|
||||||
|
_licensingService = Substitute.For<ILicensingService>();
|
||||||
|
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
||||||
|
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
|
||||||
|
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
|
||||||
|
_cancelSubscriptionCommand = Substitute.For<ICancelSubscriptionCommand>();
|
||||||
|
_subscriberQueries = Substitute.For<ISubscriberQueries>();
|
||||||
|
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||||
|
|
||||||
|
_sut = new OrganizationsController(
|
||||||
|
_organizationRepository,
|
||||||
|
_organizationUserRepository,
|
||||||
|
_organizationService,
|
||||||
|
_userService,
|
||||||
|
_paymentService,
|
||||||
|
_currentContext,
|
||||||
|
_cloudGetOrganizationLicenseQuery,
|
||||||
|
_globalSettings,
|
||||||
|
_licensingService,
|
||||||
|
_updateSecretsManagerSubscriptionCommand,
|
||||||
|
_upgradeOrganizationPlanCommand,
|
||||||
|
_addSecretsManagerSubscriptionCommand,
|
||||||
|
_cancelSubscriptionCommand,
|
||||||
|
_subscriberQueries,
|
||||||
|
_referenceEventService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_sut?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineAutoData(true, false)]
|
||||||
|
[InlineAutoData(false, true)]
|
||||||
|
[InlineAutoData(false, false)]
|
||||||
|
public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector(
|
||||||
|
bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user)
|
||||||
|
{
|
||||||
|
var ssoConfig = new SsoConfig
|
||||||
|
{
|
||||||
|
Id = default,
|
||||||
|
Data = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = keyConnectorEnabled
|
||||||
|
? MemberDecryptionType.KeyConnector
|
||||||
|
: MemberDecryptionType.MasterPassword
|
||||||
|
}.Serialize(),
|
||||||
|
Enabled = true,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
};
|
||||||
|
|
||||||
|
user.UsesKeyConnector = userUsesKeyConnector;
|
||||||
|
|
||||||
|
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||||
|
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
|
|
||||||
|
await _organizationService.DeleteUserAsync(orgId, user.Id);
|
||||||
|
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUpgradeRequestModel model)
|
||||||
|
{
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = false;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = true;
|
||||||
|
organizationUser.AccessSecretsManager = false;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
|
||||||
|
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = true;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
|
||||||
|
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model)
|
||||||
|
{
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser,
|
||||||
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||||
|
{
|
||||||
|
organizationUser.AccessSecretsManager = false;
|
||||||
|
|
||||||
|
var ssoConfigurationData = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||||
|
KeyConnectorUrl = "https://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||||
|
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(organizationUserOrganizationDetails);
|
||||||
|
|
||||||
|
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||||
|
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||||
|
|
||||||
|
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||||
|
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||||
|
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||||
|
{
|
||||||
|
var ssoConfigurationData = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||||
|
KeyConnectorUrl = "https://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||||
|
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();
|
||||||
|
|
||||||
|
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(organizationUserOrganizationDetails);
|
||||||
|
|
||||||
|
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||||
|
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||||
|
|
||||||
|
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||||
|
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||||
|
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user