mirror of
https://github.com/bitwarden/server
synced 2025-12-19 09:43:25 +00:00
[AC 1410] Secrets Manager subscription adjustment back-end changes (#3036)
* Create UpgradeSecretsManagerSubscription command --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -9,6 +9,8 @@ using Stripe;
|
|||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
|
using Bit.Core.Repositories.Noop;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.Utilities;
|
using Bit.Commercial.Core.Utilities;
|
||||||
@@ -86,6 +88,7 @@ public class Startup
|
|||||||
services.AddBaseServices(globalSettings);
|
services.AddBaseServices(globalSettings);
|
||||||
services.AddDefaultServices(globalSettings);
|
services.AddDefaultServices(globalSettings);
|
||||||
services.AddScoped<IAccessControlService, AccessControlService>();
|
services.AddScoped<IAccessControlService, AccessControlService>();
|
||||||
|
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
||||||
|
|
||||||
#if OSS
|
#if OSS
|
||||||
services.AddOosServices();
|
services.AddOosServices();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Bit.Core.Models.Business;
|
|||||||
using Bit.Core.Models.Data.Organizations.Policies;
|
using Bit.Core.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.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;
|
||||||
@@ -50,6 +51,7 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ILicensingService _licensingService;
|
private readonly ILicensingService _licensingService;
|
||||||
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -70,7 +72,8 @@ public class OrganizationsController : Controller
|
|||||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILicensingService licensingService)
|
ILicensingService licensingService,
|
||||||
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@@ -91,6 +94,7 @@ public class OrganizationsController : Controller
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_licensingService = licensingService;
|
_licensingService = licensingService;
|
||||||
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@@ -319,10 +323,34 @@ public class OrganizationsController : Controller
|
|||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.UpdateSubscription(orgIdGuid, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
await _organizationService.UpdateSubscription(orgIdGuid, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/sm-subscription")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostSmSubscription(Guid id, [FromBody] SecretsManagerSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretsManagerPlan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
|
||||||
|
if (secretsManagerPlan == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Invalid Secrets Manager plan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, secretsManagerPlan);
|
||||||
|
await _updateSecretsManagerSubscriptionCommand.UpdateSecretsManagerSubscription(organizationUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/seat")]
|
[HttpPost("{id}/seat")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
|
||||||
|
namespace Bit.Api.Models.Request.Organizations;
|
||||||
|
|
||||||
|
public class SecretsManagerSubscriptionUpdateRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int SeatAdjustment { get; set; }
|
||||||
|
public int? MaxAutoscaleSeats { get; set; }
|
||||||
|
public int ServiceAccountAdjustment { get; set; }
|
||||||
|
public int? MaxAutoscaleServiceAccounts { get; set; }
|
||||||
|
|
||||||
|
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
|
||||||
|
{
|
||||||
|
var newTotalSeats = organization.SmSeats.GetValueOrDefault() + SeatAdjustment;
|
||||||
|
var newTotalServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + ServiceAccountAdjustment;
|
||||||
|
var autoscaleSeats = MaxAutoscaleSeats.HasValue && MaxAutoscaleSeats !=
|
||||||
|
organization.MaxAutoscaleSmSeats.GetValueOrDefault();
|
||||||
|
var autoscaleServiceAccounts = MaxAutoscaleServiceAccounts.HasValue &&
|
||||||
|
MaxAutoscaleServiceAccounts !=
|
||||||
|
organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault();
|
||||||
|
|
||||||
|
var orgUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
|
||||||
|
SmSeatsAdjustment = SeatAdjustment,
|
||||||
|
SmSeats = newTotalSeats,
|
||||||
|
SmSeatsExcludingBase = newTotalSeats - plan.BaseSeats,
|
||||||
|
|
||||||
|
MaxAutoscaleSmSeats = MaxAutoscaleSeats,
|
||||||
|
|
||||||
|
SmServiceAccountsAdjustment = ServiceAccountAdjustment,
|
||||||
|
SmServiceAccounts = newTotalServiceAccounts,
|
||||||
|
SmServiceAccountsExcludingBase = newTotalServiceAccounts - plan.BaseServiceAccount.GetValueOrDefault(),
|
||||||
|
|
||||||
|
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts,
|
||||||
|
|
||||||
|
MaxAutoscaleSmSeatsChanged = autoscaleSeats,
|
||||||
|
MaxAutoscaleSmServiceAccountsChanged = autoscaleServiceAccounts
|
||||||
|
};
|
||||||
|
|
||||||
|
return orgUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,10 +55,12 @@ public class PlanResponseModel : ResponseModel
|
|||||||
|
|
||||||
AdditionalPricePerServiceAccount = plan.AdditionalPricePerServiceAccount;
|
AdditionalPricePerServiceAccount = plan.AdditionalPricePerServiceAccount;
|
||||||
BaseServiceAccount = plan.BaseServiceAccount;
|
BaseServiceAccount = plan.BaseServiceAccount;
|
||||||
MaxServiceAccount = plan.MaxServiceAccount;
|
MaxServiceAccounts = plan.MaxServiceAccounts;
|
||||||
|
MaxAdditionalServiceAccounts = plan.MaxAdditionalServiceAccount;
|
||||||
HasAdditionalServiceAccountOption = plan.HasAdditionalServiceAccountOption;
|
HasAdditionalServiceAccountOption = plan.HasAdditionalServiceAccountOption;
|
||||||
MaxProjects = plan.MaxProjects;
|
MaxProjects = plan.MaxProjects;
|
||||||
BitwardenProduct = plan.BitwardenProduct;
|
BitwardenProduct = plan.BitwardenProduct;
|
||||||
|
StripeServiceAccountPlanId = plan.StripeServiceAccountPlanId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlanType Type { get; set; }
|
public PlanType Type { get; set; }
|
||||||
@@ -105,10 +107,11 @@ public class PlanResponseModel : ResponseModel
|
|||||||
public decimal SeatPrice { get; set; }
|
public decimal SeatPrice { get; set; }
|
||||||
public decimal AdditionalStoragePricePerGb { get; set; }
|
public decimal AdditionalStoragePricePerGb { get; set; }
|
||||||
public decimal PremiumAccessOptionPrice { get; set; }
|
public decimal PremiumAccessOptionPrice { get; set; }
|
||||||
|
public string StripeServiceAccountPlanId { get; set; }
|
||||||
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
||||||
public short? BaseServiceAccount { get; set; }
|
public short? BaseServiceAccount { get; set; }
|
||||||
public short? MaxServiceAccount { get; set; }
|
public short? MaxServiceAccounts { get; set; }
|
||||||
|
public short? MaxAdditionalServiceAccounts { get; set; }
|
||||||
public bool HasAdditionalServiceAccountOption { get; set; }
|
public bool HasAdditionalServiceAccountOption { get; set; }
|
||||||
public short? MaxProjects { get; set; }
|
public short? MaxProjects { get; set; }
|
||||||
public BitwardenProductType BitwardenProduct { get; set; }
|
public BitwardenProductType BitwardenProduct { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{{#>FullHtmlLayout}}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0"
|
||||||
|
style="margin: 0; box-sizing: border-box; ">
|
||||||
|
<tr
|
||||||
|
style="margin: 0; box-sizing: border-box; ">
|
||||||
|
<td class="content-block"
|
||||||
|
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
|
||||||
|
valign="top">
|
||||||
|
Your organization has reached the Secrets Manager seat limit of {{MaxSeatCount}} and new members cannot be invited.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
style="margin: 0; box-sizing: border-box; ">
|
||||||
|
<td class="content-block last"
|
||||||
|
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;"
|
||||||
|
valign="top">
|
||||||
|
For more information, please refer to the following help article:
|
||||||
|
<a href="https://bitwarden.com/help/managing-users" class="inline-link">
|
||||||
|
Member management
|
||||||
|
</a>
|
||||||
|
<br class="line-break" />
|
||||||
|
<br class="line-break" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="https://vault.bitwarden.com/#/organizations/{{{OrganizationId}}}/billing/subscription" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Manage subscription
|
||||||
|
</a>
|
||||||
|
<br class="line-break" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{/FullHtmlLayout}}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{#>BasicTextLayout}}
|
||||||
|
Your organization has reached the Secrets Manager seat limit of {{MaxSeatCount}} and new members cannot be invited.
|
||||||
|
|
||||||
|
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
|
||||||
|
{{/BasicTextLayout}}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{{#>FullHtmlLayout}}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0"
|
||||||
|
style="margin: 0; box-sizing: border-box; ">
|
||||||
|
<tr
|
||||||
|
style="margin: 0; box-sizing: border-box; ">
|
||||||
|
<td class="content-block"
|
||||||
|
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
|
||||||
|
valign="top">
|
||||||
|
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
style="margin: 0; box-sizing: border-box; ">
|
||||||
|
<td class="content-block last"
|
||||||
|
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;"
|
||||||
|
valign="top">
|
||||||
|
For more information, please refer to the following help article:
|
||||||
|
<a href="https://bitwarden.com/help/managing-users" class="inline-link">
|
||||||
|
Member management
|
||||||
|
</a>
|
||||||
|
<br class="line-break" />
|
||||||
|
<br class="line-break" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="https://vault.bitwarden.com/#/organizations/{{{OrganizationId}}}/billing/subscription" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Manage subscription
|
||||||
|
</a>
|
||||||
|
<br class="line-break" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{/FullHtmlLayout}}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{#>BasicTextLayout}}
|
||||||
|
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
|
||||||
|
|
||||||
|
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
|
||||||
|
{{/BasicTextLayout}}
|
||||||
56
src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs
Normal file
56
src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
public class SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The seats to be added or removed from the organization
|
||||||
|
/// </summary>
|
||||||
|
public int SmSeatsAdjustment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total seats the organization will have after the update, including any base seats included in the plan
|
||||||
|
/// </summary>
|
||||||
|
public int SmSeats { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The seats the organization will have after the update, excluding the base seats included in the plan
|
||||||
|
/// Usually this is what the organization is billed for
|
||||||
|
/// </summary>
|
||||||
|
public int SmSeatsExcludingBase { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new autoscale limit for seats, expressed as a total (not an adjustment).
|
||||||
|
/// This may or may not be the same as the current autoscale limit.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxAutoscaleSmSeats { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The service accounts to be added or removed from the organization
|
||||||
|
/// </summary>
|
||||||
|
public int SmServiceAccountsAdjustment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total service accounts the organization will have after the update, including the base service accounts
|
||||||
|
/// included in the plan
|
||||||
|
/// </summary>
|
||||||
|
public int SmServiceAccounts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The seats the organization will have after the update, excluding the base seats included in the plan
|
||||||
|
/// Usually this is what the organization is billed for
|
||||||
|
/// </summary>
|
||||||
|
public int SmServiceAccountsExcludingBase { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new autoscale limit for service accounts, expressed as a total (not an adjustment).
|
||||||
|
/// This may or may not be the same as the current autoscale limit.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||||
|
|
||||||
|
public bool SmSeatsChanged => SmSeatsAdjustment != 0;
|
||||||
|
public bool SmServiceAccountsChanged => SmServiceAccountsAdjustment != 0;
|
||||||
|
public bool MaxAutoscaleSmSeatsChanged { get; set; }
|
||||||
|
public bool MaxAutoscaleSmServiceAccountsChanged { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Business;
|
namespace Bit.Core.Models.Business;
|
||||||
@@ -42,7 +43,15 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
|
|||||||
{
|
{
|
||||||
_plan = plan;
|
_plan = plan;
|
||||||
_additionalSeats = additionalSeats;
|
_additionalSeats = additionalSeats;
|
||||||
_previousSeats = organization.Seats ?? 0;
|
switch (plan.BitwardenProduct)
|
||||||
|
{
|
||||||
|
case BitwardenProductType.PasswordManager:
|
||||||
|
_previousSeats = organization.Seats.GetValueOrDefault();
|
||||||
|
break;
|
||||||
|
case BitwardenProductType.SecretsManager:
|
||||||
|
_previousSeats = organization.SmSeats.GetValueOrDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
@@ -77,6 +86,52 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ServiceAccountSubscriptionUpdate : SubscriptionUpdate
|
||||||
|
{
|
||||||
|
private long? _prevServiceAccounts;
|
||||||
|
private readonly StaticStore.Plan _plan;
|
||||||
|
private readonly long? _additionalServiceAccounts;
|
||||||
|
protected override List<string> PlanIds => new() { _plan.StripeServiceAccountPlanId };
|
||||||
|
|
||||||
|
public ServiceAccountSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalServiceAccounts)
|
||||||
|
{
|
||||||
|
_plan = plan;
|
||||||
|
_additionalServiceAccounts = additionalServiceAccounts;
|
||||||
|
_prevServiceAccounts = organization.SmServiceAccounts ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
|
{
|
||||||
|
var item = SubscriptionItem(subscription, PlanIds.Single());
|
||||||
|
_prevServiceAccounts = item?.Quantity ?? 0;
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = item?.Id,
|
||||||
|
Plan = PlanIds.Single(),
|
||||||
|
Quantity = _additionalServiceAccounts,
|
||||||
|
Deleted = (item?.Id != null && _additionalServiceAccounts == 0) ? true : (bool?)null,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||||
|
{
|
||||||
|
var item = SubscriptionItem(subscription, PlanIds.Single());
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = item?.Id,
|
||||||
|
Plan = PlanIds.Single(),
|
||||||
|
Quantity = _prevServiceAccounts,
|
||||||
|
Deleted = _prevServiceAccounts == 0 ? true : (bool?)null,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class StorageSubscriptionUpdate : SubscriptionUpdate
|
public class StorageSubscriptionUpdate : SubscriptionUpdate
|
||||||
{
|
{
|
||||||
private long? _prevStorage;
|
private long? _prevStorage;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Models.Mail;
|
||||||
|
|
||||||
|
public class OrganizationServiceAccountsMaxReachedViewModel
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public int MaxServiceAccountsCount { get; set; }
|
||||||
|
}
|
||||||
@@ -15,8 +15,11 @@ public class Plan
|
|||||||
public short? BaseStorageGb { get; set; }
|
public short? BaseStorageGb { get; set; }
|
||||||
public short? MaxCollections { get; set; }
|
public short? MaxCollections { get; set; }
|
||||||
public short? MaxUsers { get; set; }
|
public short? MaxUsers { get; set; }
|
||||||
|
public short? MaxServiceAccounts { get; set; }
|
||||||
public bool AllowSeatAutoscale { get; set; }
|
public bool AllowSeatAutoscale { get; set; }
|
||||||
|
|
||||||
|
public bool AllowServiceAccountsAutoscale { get; set; }
|
||||||
|
|
||||||
public bool HasAdditionalSeatsOption { get; set; }
|
public bool HasAdditionalSeatsOption { get; set; }
|
||||||
public int? MaxAdditionalSeats { get; set; }
|
public int? MaxAdditionalSeats { get; set; }
|
||||||
public bool HasAdditionalStorageOption { get; set; }
|
public bool HasAdditionalStorageOption { get; set; }
|
||||||
@@ -55,7 +58,7 @@ public class Plan
|
|||||||
public decimal PremiumAccessOptionPrice { get; set; }
|
public decimal PremiumAccessOptionPrice { get; set; }
|
||||||
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
||||||
public short? BaseServiceAccount { get; set; }
|
public short? BaseServiceAccount { get; set; }
|
||||||
public short? MaxServiceAccount { get; set; }
|
public short? MaxAdditionalServiceAccount { get; set; }
|
||||||
public bool HasAdditionalServiceAccountOption { get; set; }
|
public bool HasAdditionalServiceAccountOption { get; set; }
|
||||||
public short? MaxProjects { get; set; }
|
public short? MaxProjects { get; set; }
|
||||||
public BitwardenProductType BitwardenProduct { get; set; }
|
public BitwardenProductType BitwardenProduct { get; set; }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
|
|||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
||||||
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager;
|
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager;
|
||||||
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
|
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -41,6 +43,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddOrganizationGroupCommands();
|
services.AddOrganizationGroupCommands();
|
||||||
services.AddOrganizationLicenseCommandsQueries();
|
services.AddOrganizationLicenseCommandsQueries();
|
||||||
services.AddOrganizationDomainCommandsQueries();
|
services.AddOrganizationDomainCommandsQueries();
|
||||||
|
services.AddOrganizationSubscriptionUpdateCommandsQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
|
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
|
||||||
@@ -110,6 +113,11 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddOrganizationSubscriptionUpdateCommandsQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
private static void AddTokenizers(this IServiceCollection services)
|
private static void AddTokenizers(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>
|
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
||||||
|
|
||||||
|
public interface IUpdateSecretsManagerSubscriptionCommand
|
||||||
|
{
|
||||||
|
Task UpdateSecretsManagerSubscription(SecretsManagerSubscriptionUpdate update);
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
|
||||||
|
|
||||||
|
public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand
|
||||||
|
{
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
private readonly ILogger<UpdateSecretsManagerSubscriptionCommand> _logger;
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
|
||||||
|
public UpdateSecretsManagerSubscriptionCommand(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IPaymentService paymentService,
|
||||||
|
IMailService mailService,
|
||||||
|
ILogger<UpdateSecretsManagerSubscriptionCommand> logger,
|
||||||
|
IServiceAccountRepository serviceAccountRepository)
|
||||||
|
{
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_paymentService = paymentService;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_mailService = mailService;
|
||||||
|
_logger = logger;
|
||||||
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSecretsManagerSubscription(SecretsManagerSubscriptionUpdate update)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(update.OrganizationId);
|
||||||
|
|
||||||
|
ValidateOrganization(organization);
|
||||||
|
|
||||||
|
var plan = GetPlanForOrganization(organization);
|
||||||
|
|
||||||
|
if (update.SmSeatsChanged)
|
||||||
|
{
|
||||||
|
await ValidateSmSeatsUpdateAsync(organization, update, plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.SmServiceAccountsChanged)
|
||||||
|
{
|
||||||
|
await ValidateSmServiceAccountsUpdateAsync(organization, update, plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.MaxAutoscaleSmSeatsChanged)
|
||||||
|
{
|
||||||
|
ValidateMaxAutoscaleSmSeatsUpdateAsync(organization, update.MaxAutoscaleSmSeats.GetValueOrDefault(), plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.MaxAutoscaleSmServiceAccountsChanged)
|
||||||
|
{
|
||||||
|
ValidateMaxAutoscaleSmServiceAccountUpdate(organization, update.MaxAutoscaleSmServiceAccounts.GetValueOrDefault(), plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
await FinalizeSubscriptionAdjustmentAsync(organization, plan, update);
|
||||||
|
|
||||||
|
await SendEmailIfAutoscaleLimitReached(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Plan GetPlanForOrganization(Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||||
|
if (plan == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Existing plan not found.");
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOrganization(Organization organization)
|
||||||
|
{
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Organization is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!organization.UseSecretsManager)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization has no access to Secrets Manager.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FinalizeSubscriptionAdjustmentAsync(Organization organization,
|
||||||
|
Plan plan, SecretsManagerSubscriptionUpdate update)
|
||||||
|
{
|
||||||
|
if (update.SmSeatsChanged)
|
||||||
|
{
|
||||||
|
await ProcessChargesAndRaiseEventsForAdjustSeatsAsync(organization, plan, update);
|
||||||
|
organization.SmSeats = update.SmSeats;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.SmServiceAccountsChanged)
|
||||||
|
{
|
||||||
|
await ProcessChargesAndRaiseEventsForAdjustServiceAccountsAsync(organization, plan, update);
|
||||||
|
organization.SmServiceAccounts = update.SmServiceAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.MaxAutoscaleSmSeatsChanged)
|
||||||
|
{
|
||||||
|
organization.MaxAutoscaleSmSeats = update.MaxAutoscaleSmSeats;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.MaxAutoscaleSmServiceAccountsChanged)
|
||||||
|
{
|
||||||
|
organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessChargesAndRaiseEventsForAdjustSeatsAsync(Organization organization, Plan plan,
|
||||||
|
SecretsManagerSubscriptionUpdate update)
|
||||||
|
{
|
||||||
|
await _paymentService.AdjustSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
|
||||||
|
|
||||||
|
// TODO: call ReferenceEventService - see AC-1481
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessChargesAndRaiseEventsForAdjustServiceAccountsAsync(Organization organization, Plan plan,
|
||||||
|
SecretsManagerSubscriptionUpdate update)
|
||||||
|
{
|
||||||
|
await _paymentService.AdjustServiceAccountsAsync(organization, plan,
|
||||||
|
update.SmServiceAccountsExcludingBase);
|
||||||
|
|
||||||
|
// TODO: call ReferenceEventService - see AC-1481
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendEmailIfAutoscaleLimitReached(Organization organization)
|
||||||
|
{
|
||||||
|
if (organization.SmSeats.HasValue && organization.MaxAutoscaleSmSeats.HasValue && organization.SmSeats == organization.MaxAutoscaleSmSeats)
|
||||||
|
{
|
||||||
|
await SendSeatLimitEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization.SmServiceAccounts.HasValue && organization.MaxAutoscaleSmServiceAccounts.HasValue && organization.SmServiceAccounts == organization.MaxAutoscaleSmServiceAccounts)
|
||||||
|
{
|
||||||
|
await SendServiceAccountLimitEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendSeatLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
|
||||||
|
OrganizationUserType.Owner))
|
||||||
|
.Select(u => u.Email).Distinct();
|
||||||
|
|
||||||
|
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, MaxAutoscaleValue, ownerEmails);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Error encountered notifying organization owners of Seats limit reached.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendServiceAccountLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
|
||||||
|
OrganizationUserType.Owner))
|
||||||
|
.Select(u => u.Email).Distinct();
|
||||||
|
|
||||||
|
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, MaxAutoscaleValue, ownerEmails);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Error encountered notifying organization owners of Service Accounts limit reached.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateSmSeatsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
|
||||||
|
{
|
||||||
|
if (organization.SmSeats == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization has no Secrets Manager seat limit, no need to adjust seats");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats > update.MaxAutoscaleSmSeats.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot set max seat autoscaling below seat count.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No payment method found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No subscription found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plan.HasAdditionalSeatsOption)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Plan does not allow additional Secrets Manager seats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.BaseSeats > update.SmSeats)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} Secrets Manager seats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.SmSeats <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You must have at least 1 Secrets Manager seat.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.MaxAdditionalSeats.HasValue && update.SmSeatsExcludingBase > plan.MaxAdditionalSeats.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Organization plan allows a maximum of " +
|
||||||
|
$"{plan.MaxAdditionalSeats.Value} additional Secrets Manager seats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization.SmSeats.Value > update.SmSeats)
|
||||||
|
{
|
||||||
|
var currentSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
|
if (currentSeats > update.SmSeats)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Your organization currently has {currentSeats} Secrets Manager seats. " +
|
||||||
|
$"Your plan only allows ({update.SmSeats}) Secrets Manager seats. Remove some Secrets Manager users.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateSmServiceAccountsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
|
||||||
|
{
|
||||||
|
if (organization.SmServiceAccounts == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization has no Service Accounts limit, no need to adjust Service Accounts");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.MaxAutoscaleSmServiceAccounts.HasValue && update.SmServiceAccounts > update.MaxAutoscaleSmServiceAccounts.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot set max Service Accounts autoscaling below Service Accounts count.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No payment method found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No subscription found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plan.HasAdditionalServiceAccountOption)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Plan does not allow additional Service Accounts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.BaseServiceAccount > update.SmServiceAccounts)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Plan has a minimum of {plan.BaseServiceAccount} Service Accounts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.SmServiceAccounts <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You must have at least 1 Service Account.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.MaxAdditionalServiceAccount.HasValue && update.SmServiceAccountsExcludingBase > plan.MaxAdditionalServiceAccount.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Organization plan allows a maximum of " +
|
||||||
|
$"{plan.MaxAdditionalServiceAccount.Value} additional Service Accounts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!organization.SmServiceAccounts.HasValue || organization.SmServiceAccounts.Value > update.SmServiceAccounts)
|
||||||
|
{
|
||||||
|
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||||
|
if (currentServiceAccounts > update.SmServiceAccounts)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} Service Accounts. " +
|
||||||
|
$"Your plan only allows ({update.SmServiceAccounts}) Service Accounts. Remove some Service Accounts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateMaxAutoscaleSmSeatsUpdateAsync(Organization organization, int maxAutoscaleSeats, Plan plan)
|
||||||
|
{
|
||||||
|
if (organization.SmSeats.HasValue && maxAutoscaleSeats < organization.SmSeats.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Cannot set max Secrets Manager seat autoscaling below current Secrets Manager seat count.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.MaxUsers.HasValue && maxAutoscaleSeats > plan.MaxUsers)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(string.Concat(
|
||||||
|
$"Your plan has a Secrets Manager seat limit of {plan.MaxUsers}, ",
|
||||||
|
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
|
||||||
|
"Reduce your max autoscale count."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plan.AllowSeatAutoscale)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Your plan does not allow Secrets Manager seat autoscaling.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateMaxAutoscaleSmServiceAccountUpdate(Organization organization, int maxAutoscaleServiceAccounts, Plan plan)
|
||||||
|
{
|
||||||
|
if (organization.SmServiceAccounts.HasValue && maxAutoscaleServiceAccounts < organization.SmServiceAccounts.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(
|
||||||
|
$"Cannot set max Service Accounts autoscaling below current Service Accounts count.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plan.AllowServiceAccountsAutoscale)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Your plan does not allow Service Accounts autoscaling.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.MaxServiceAccounts.HasValue && maxAutoscaleServiceAccounts > plan.MaxServiceAccounts)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(string.Concat(
|
||||||
|
$"Your plan has a Service Accounts limit of {plan.MaxServiceAccounts}, ",
|
||||||
|
$"but you have specified a max autoscale count of {maxAutoscaleServiceAccounts}.",
|
||||||
|
"Reduce your max autoscale count."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,4 +55,6 @@ public interface IMailService
|
|||||||
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
||||||
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
||||||
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||||
|
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||||
|
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public interface IPaymentService
|
|||||||
short additionalStorageGb, TaxInfo taxInfo);
|
short additionalStorageGb, TaxInfo taxInfo);
|
||||||
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||||
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
||||||
|
|
||||||
|
Task<string> AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts,
|
||||||
|
DateTime? prorationDate = null);
|
||||||
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
|
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
|
||||||
bool skipInAppPurchaseCheck = false);
|
bool skipInAppPurchaseCheck = false);
|
||||||
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
||||||
|
|||||||
@@ -896,6 +896,36 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||||
|
IEnumerable<string> ownerEmails)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage($"{organization.Name} Secrets Manager Seat Limit Reached", ownerEmails);
|
||||||
|
var model = new OrganizationSeatsMaxReachedViewModel
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
MaxSeatCount = maxSeatCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
await AddMessageContentAsync(message, "OrganizationSmSeatsMaxReached", model);
|
||||||
|
message.Category = "OrganizationSmSeatsMaxReached";
|
||||||
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||||
|
IEnumerable<string> ownerEmails)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage($"{organization.Name} Secrets Manager Service Accounts Limit Reached", ownerEmails);
|
||||||
|
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
MaxServiceAccountsCount = maxSeatCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
await AddMessageContentAsync(message, "OrganizationSmServiceAccountsMaxReached", model);
|
||||||
|
message.Category = "OrganizationSmServiceAccountsMaxReached";
|
||||||
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetUserIdentifier(string email, string userName)
|
private static string GetUserIdentifier(string email, string userName)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
||||||
|
|||||||
@@ -427,15 +427,15 @@ public class OrganizationService : IOrganizationService
|
|||||||
if (newSecretsManagerPlan.BaseServiceAccount != null)
|
if (newSecretsManagerPlan.BaseServiceAccount != null)
|
||||||
{
|
{
|
||||||
if (!organization.SmServiceAccounts.HasValue ||
|
if (!organization.SmServiceAccounts.HasValue ||
|
||||||
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccount)
|
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccounts)
|
||||||
{
|
{
|
||||||
var currentServiceAccounts =
|
var currentServiceAccounts =
|
||||||
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||||
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccount)
|
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccounts)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
$"Your organization currently has {currentServiceAccounts} service account seats filled. " +
|
$"Your organization currently has {currentServiceAccounts} service account seats filled. " +
|
||||||
$"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccount}) service accounts. Remove some service accounts.");
|
$"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccounts}) service accounts. Remove some service accounts.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -853,6 +853,11 @@ public class StripePaymentService : IPaymentService
|
|||||||
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<string> AdjustServiceAccountsAsync(Organization organization, StaticStore.Plan plan, int additionalServiceAccounts, DateTime? prorationDate = null)
|
||||||
|
{
|
||||||
|
return FinalizeSubscriptionChangeAsync(organization, new ServiceAccountSubscriptionUpdate(organization, plan, additionalServiceAccounts), prorationDate);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage,
|
public Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage,
|
||||||
string storagePlanId, DateTime? prorationDate = null)
|
string storagePlanId, DateTime? prorationDate = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,4 +238,17 @@ public class NoopMailService : IMailService
|
|||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||||
|
IEnumerable<string> ownerEmails)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization,
|
||||||
|
int maxSeatCount,
|
||||||
|
IEnumerable<string> ownerEmails)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,8 @@ public enum ReferenceEventType
|
|||||||
OrganizationCreatedByAdmin,
|
OrganizationCreatedByAdmin,
|
||||||
[EnumMember(Value = "sm-service-account-accessed-secret")]
|
[EnumMember(Value = "sm-service-account-accessed-secret")]
|
||||||
SmServiceAccountAccessedSecret,
|
SmServiceAccountAccessedSecret,
|
||||||
|
[EnumMember(Value = "adjust-service-accounts")]
|
||||||
|
AdjustServiceAccounts,
|
||||||
|
[EnumMember(Value = "adjust-sm-seats")]
|
||||||
|
AdjustSmSeats,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public class ReferenceEvent
|
|||||||
|
|
||||||
public int? Seats { get; set; }
|
public int? Seats { get; set; }
|
||||||
public int? PreviousSeats { get; set; }
|
public int? PreviousSeats { get; set; }
|
||||||
|
public int? PreviousServiceAccounts { get; set; }
|
||||||
|
|
||||||
public short? Storage { get; set; }
|
public short? Storage { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public static class SecretsManagerPlanStore
|
|||||||
SeatPrice = 13,
|
SeatPrice = 13,
|
||||||
AdditionalPricePerServiceAccount = 0.5M,
|
AdditionalPricePerServiceAccount = 0.5M,
|
||||||
AllowSeatAutoscale = true,
|
AllowSeatAutoscale = true,
|
||||||
|
AllowServiceAccountsAutoscale = true
|
||||||
},
|
},
|
||||||
new Plan
|
new Plan
|
||||||
{
|
{
|
||||||
@@ -83,6 +84,7 @@ public static class SecretsManagerPlanStore
|
|||||||
SeatPrice = 144,
|
SeatPrice = 144,
|
||||||
AdditionalPricePerServiceAccount = 6,
|
AdditionalPricePerServiceAccount = 6,
|
||||||
AllowSeatAutoscale = true,
|
AllowSeatAutoscale = true,
|
||||||
|
AllowServiceAccountsAutoscale = true
|
||||||
},
|
},
|
||||||
new Plan
|
new Plan
|
||||||
{
|
{
|
||||||
@@ -113,6 +115,7 @@ public static class SecretsManagerPlanStore
|
|||||||
SeatPrice = 7,
|
SeatPrice = 7,
|
||||||
AdditionalPricePerServiceAccount = 0.5M,
|
AdditionalPricePerServiceAccount = 0.5M,
|
||||||
AllowSeatAutoscale = true,
|
AllowSeatAutoscale = true,
|
||||||
|
AllowServiceAccountsAutoscale = true
|
||||||
},
|
},
|
||||||
new Plan
|
new Plan
|
||||||
{
|
{
|
||||||
@@ -145,6 +148,7 @@ public static class SecretsManagerPlanStore
|
|||||||
SeatPrice = 72,
|
SeatPrice = 72,
|
||||||
AdditionalPricePerServiceAccount = 6,
|
AdditionalPricePerServiceAccount = 6,
|
||||||
AllowSeatAutoscale = true,
|
AllowSeatAutoscale = true,
|
||||||
|
AllowServiceAccountsAutoscale = true
|
||||||
},
|
},
|
||||||
new Plan
|
new Plan
|
||||||
{
|
{
|
||||||
@@ -158,7 +162,7 @@ public static class SecretsManagerPlanStore
|
|||||||
BaseServiceAccount = 3,
|
BaseServiceAccount = 3,
|
||||||
MaxProjects = 3,
|
MaxProjects = 3,
|
||||||
MaxUsers = 2,
|
MaxUsers = 2,
|
||||||
MaxServiceAccount = 3,
|
MaxServiceAccounts = 3,
|
||||||
UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to
|
UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to
|
||||||
DisplaySortOrder = -1,
|
DisplaySortOrder = -1,
|
||||||
AllowSeatAutoscale = false,
|
AllowSeatAutoscale = false,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using AspNetCoreRateLimit;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Repositories.Noop;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Utilities;
|
using Bit.Identity.Utilities;
|
||||||
@@ -46,7 +48,7 @@ public class Startup
|
|||||||
// Repositories
|
// Repositories
|
||||||
services.AddDatabaseRepositories(globalSettings);
|
services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
services.AddOosServices();
|
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
services.AddScoped<ICurrentContext, CurrentContext>();
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.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;
|
||||||
@@ -40,6 +41,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ILicensingService _licensingService;
|
private readonly ILicensingService _licensingService;
|
||||||
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
@@ -64,12 +66,14 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_updateOrganizationLicenseCommand = Substitute.For<IUpdateOrganizationLicenseCommand>();
|
_updateOrganizationLicenseCommand = Substitute.For<IUpdateOrganizationLicenseCommand>();
|
||||||
_featureService = Substitute.For<IFeatureService>();
|
_featureService = Substitute.For<IFeatureService>();
|
||||||
_licensingService = Substitute.For<ILicensingService>();
|
_licensingService = Substitute.For<ILicensingService>();
|
||||||
|
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
||||||
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
|
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
|
||||||
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
||||||
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
||||||
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService);
|
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService,
|
||||||
|
_updateSecretsManagerSubscriptionCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -0,0 +1,745 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class UpdateSecretsManagerSubscriptionCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_NoOrganization_Throws(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organizationId)
|
||||||
|
.Returns((Organization)null);
|
||||||
|
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = null,
|
||||||
|
SmSeatsAdjustment = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Organization is not found", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_NoSecretsManagerAccess_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 5,
|
||||||
|
UseSecretsManager = false,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organizationId)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmSeats = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Organization has no access to Secrets Manager.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_SeatsAdustmentGreaterThanMaxAutoscaleSeats_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmServiceAccounts = 5,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 10,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
};
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 10,
|
||||||
|
SmSeatsAdjustment = 15,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() + 10,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + 10) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + 5,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 5) - (int)plan.BaseServiceAccount
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Cannot set max seat autoscaling below seat count.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_ServiceAccountsGreaterThanMaxAutoscaleSeats_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmServiceAccounts = 5,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 10,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "9"
|
||||||
|
};
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 10,
|
||||||
|
SmServiceAccountsAdjustment = 11,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() + 1,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + 1) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + 11,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 11) - (int)plan.BaseServiceAccount
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Cannot set max Service Accounts autoscaling below Service Accounts count", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_NullGatewayCustomerId_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 5,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 15,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually
|
||||||
|
};
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 15,
|
||||||
|
SmServiceAccountsAdjustment = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("No payment method found.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_NullGatewaySubscriptionId_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmServiceAccounts = 5,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 15,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
GatewayCustomerId = "1"
|
||||||
|
};
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 15,
|
||||||
|
SmServiceAccountsAdjustment = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("No subscription found.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_OrgWithNullSmSeatOnSeatsAdjustment_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = null,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmServiceAccounts = 5,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 15,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
GatewayCustomerId = "1"
|
||||||
|
};
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 15,
|
||||||
|
SmServiceAccountsAdjustment = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
|
||||||
|
Assert.Contains("Organization has no Secrets Manager seat limit, no need to adjust seats", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.Custom)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Existing plan not found", exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.Free)]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_WithHasAdditionalSeatsOptionfalse_ThrowsBadRequestException(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Plan does not allow additional Secrets Manager seats.", exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.Free)]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_WithHasAdditionalServiceAccountOptionFalse_ThrowsBadRequestException(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Plan does not allow additional Service Accounts", exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_ValidInput_Passes(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 350,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 5,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 100,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() + 5,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + 5) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + 100,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 100) - (int)plan.BaseServiceAccount,
|
||||||
|
MaxAutoscaleSmSeatsChanged = 15 != organization.MaxAutoscaleSeats.GetValueOrDefault(),
|
||||||
|
MaxAutoscaleSmServiceAccountsChanged = 200 != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate);
|
||||||
|
if (organizationUpdate.SmServiceAccountsAdjustment != 0)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IPaymentService>().Received(1)
|
||||||
|
.AdjustSeatsAsync(organization, plan, organizationUpdate.SmSeatsExcludingBase);
|
||||||
|
|
||||||
|
// TODO: call ReferenceEventService - see AC-1481
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUpdate.SmSeatsAdjustment != 0)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IPaymentService>().Received(1)
|
||||||
|
.AdjustServiceAccountsAsync(organization, plan, organizationUpdate.SmServiceAccountsExcludingBase);
|
||||||
|
|
||||||
|
// TODO: call ReferenceEventService - see AC-1481
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUpdate.SmSeatsAdjustment != 0)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
|
||||||
|
Arg.Is<Organization>(org => org.SmSeats == organizationUpdate.SmSeats));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUpdate.SmServiceAccountsAdjustment != 0)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
|
||||||
|
Arg.Is<Organization>(org =>
|
||||||
|
org.SmServiceAccounts == organizationUpdate.SmServiceAccounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUpdate.MaxAutoscaleSmSeats != organization.MaxAutoscaleSmSeats)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
|
||||||
|
Arg.Is<Organization>(org =>
|
||||||
|
org.MaxAutoscaleSmSeats == organizationUpdate.MaxAutoscaleSmServiceAccounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUpdate.MaxAutoscaleSmServiceAccounts != organization.MaxAutoscaleSmServiceAccounts)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
|
||||||
|
Arg.Is<Organization>(org =>
|
||||||
|
org.MaxAutoscaleSmServiceAccounts == organizationUpdate.MaxAutoscaleSmServiceAccounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value, Arg.Any<IEnumerable<string>>());
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, Arg.Any<IEnumerable<string>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_ThrowsBadRequestException_WhenMaxAutoscaleSeatsBelowSeatCount(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 5,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 4,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var update = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 4,
|
||||||
|
SmSeatsAdjustment = 1,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 5,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() + 1,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + 1) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + 5,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 5) - (int)plan.BaseServiceAccount
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(update));
|
||||||
|
Assert.Contains("Cannot set max seat autoscaling below seat count.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateSecretsManagerSubscription_ThrowsBadRequestException_WhenOccupiedSeatsExceedNewSeatTotal(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 10,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2",
|
||||||
|
PlanType = PlanType.EnterpriseAnnually
|
||||||
|
};
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var update = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 7,
|
||||||
|
SmSeatsAdjustment = -3,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 5,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() - 3,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() - 3) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + 5,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 5) - (int)plan.BaseServiceAccount
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetOccupiedSmSeatCountByOrganizationIdAsync(organizationId).Returns(8);
|
||||||
|
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(update));
|
||||||
|
Assert.Contains("Your organization currently has 8 Secrets Manager seats. Your plan only allows (7) Secrets Manager seats. Remove some Secrets Manager users", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task AdjustServiceAccountsAsync_ThrowsBadRequestException_WhenSmServiceAccountsIsNull(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 10,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2",
|
||||||
|
SmServiceAccounts = null,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually
|
||||||
|
};
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var update = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 21,
|
||||||
|
SmSeatsAdjustment = 10,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 250,
|
||||||
|
SmServiceAccountsAdjustment = 1,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() + 10,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + 10) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + 1,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 1) - (int)plan.BaseServiceAccount
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(update));
|
||||||
|
Assert.Contains("Organization has no Service Accounts limit, no need to adjust Service Accounts", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task AutoscaleSeatsAsync_ThrowsBadRequestException_WhenMaxAutoscaleSeatsExceedPlanMaxUsers(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
SmSeats = 3,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmServiceAccounts = 100,
|
||||||
|
PlanType = PlanType.Free,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2",
|
||||||
|
};
|
||||||
|
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 200,
|
||||||
|
SmServiceAccountsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmSeatsChanged = 15 != organization.MaxAutoscaleSeats.GetValueOrDefault(),
|
||||||
|
MaxAutoscaleSmServiceAccountsChanged = 200 != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Your plan has a Secrets Manager seat limit of 2, but you have specified a max autoscale count of 15.Reduce your max autoscale count.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.Free)]
|
||||||
|
public async Task AutoscaleSeatsAsync_ThrowsBadRequestException_WhenPlanDoesNotAllowSeatAutoscale(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 1,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 350,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 1,
|
||||||
|
SmSeatsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmSeatsChanged = 1 != organization.MaxAutoscaleSeats.GetValueOrDefault(),
|
||||||
|
MaxAutoscaleSmServiceAccountsChanged = 200 != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Your plan does not allow Secrets Manager seat autoscaling", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.Free)]
|
||||||
|
public async Task UpdateServiceAccountAutoscaling_ThrowsBadRequestException_WhenPlanDoesNotAllowServiceAccountAutoscale(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 200,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 350,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = null,
|
||||||
|
SmSeatsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 0,
|
||||||
|
MaxAutoscaleSmSeatsChanged = false,
|
||||||
|
MaxAutoscaleSmServiceAccountsChanged = 300 != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Your plan does not allow Service Accounts autoscaling.", exception.Message);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
public async Task UpdateServiceAccountAutoscaling_WhenCurrentServiceAccountsIsGreaterThanNew_ThrowsBadRequestException(
|
||||||
|
PlanType planType,
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
UseSecretsManager = true,
|
||||||
|
SmSeats = 10,
|
||||||
|
SmServiceAccounts = 301,
|
||||||
|
MaxAutoscaleSmSeats = 20,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 350,
|
||||||
|
PlanType = planType,
|
||||||
|
GatewayCustomerId = "1",
|
||||||
|
GatewaySubscriptionId = "2"
|
||||||
|
};
|
||||||
|
|
||||||
|
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
|
||||||
|
var organizationUpdate = new SecretsManagerSubscriptionUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
MaxAutoscaleSmSeats = 15,
|
||||||
|
SmSeatsAdjustment = 5,
|
||||||
|
MaxAutoscaleSmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsAdjustment = 100,
|
||||||
|
SmSeats = organization.SmSeats.GetValueOrDefault() + 5,
|
||||||
|
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + 5) - plan.BaseSeats,
|
||||||
|
SmServiceAccounts = 300,
|
||||||
|
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + 100) - (int)plan.BaseServiceAccount,
|
||||||
|
MaxAutoscaleSmSeatsChanged = 15 != organization.MaxAutoscaleSeats.GetValueOrDefault(),
|
||||||
|
MaxAutoscaleSmServiceAccountsChanged = 200 != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
var currentServiceAccounts = 301;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.GetServiceAccountCountByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(currentServiceAccounts);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
|
||||||
|
Assert.Contains("Your organization currently has 301 Service Accounts. Your plan only allows (300) Service Accounts. Remove some Service Accounts", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1).GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||||
|
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task VerifyDependencyNotCalledAsync(SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||||
|
{
|
||||||
|
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
|
||||||
|
.AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
|
||||||
|
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
|
||||||
|
.AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
|
||||||
|
// TODO: call ReferenceEventService - see AC-1481
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().DidNotReceive()
|
||||||
|
.ReplaceAndUpdateCacheAsync(Arg.Any<Organization>());
|
||||||
|
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||||
|
.SendOrganizationMaxSeatLimitReachedEmailAsync(Arg.Any<Organization>(), Arg.Any<int>(),
|
||||||
|
Arg.Any<IEnumerable<string>>());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user