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.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Core.Repositories.Noop;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
@@ -86,6 +88,7 @@ public class Startup
|
||||
services.AddBaseServices(globalSettings);
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddScoped<IAccessControlService, AccessControlService>();
|
||||
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
||||
|
||||
#if OSS
|
||||
services.AddOosServices();
|
||||
|
||||
@@ -18,6 +18,7 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -50,6 +51,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -70,7 +72,8 @@ public class OrganizationsController : Controller
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings,
|
||||
ILicensingService licensingService)
|
||||
ILicensingService licensingService,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -91,6 +94,7 @@ public class OrganizationsController : Controller
|
||||
_featureService = featureService;
|
||||
_globalSettings = globalSettings;
|
||||
_licensingService = licensingService;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -319,10 +323,34 @@ public class OrganizationsController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.UpdateSubscription(orgIdGuid, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/sm-subscription")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostSmSubscription(Guid id, [FromBody] SecretsManagerSubscriptionUpdateRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!await _currentContext.EditSubscription(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secretsManagerPlan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
|
||||
if (secretsManagerPlan == null)
|
||||
{
|
||||
throw new NotFoundException("Invalid Secrets Manager plan.");
|
||||
}
|
||||
|
||||
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, secretsManagerPlan);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSecretsManagerSubscription(organizationUpdate);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/seat")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
||||
|
||||
@@ -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;
|
||||
BaseServiceAccount = plan.BaseServiceAccount;
|
||||
MaxServiceAccount = plan.MaxServiceAccount;
|
||||
MaxServiceAccounts = plan.MaxServiceAccounts;
|
||||
MaxAdditionalServiceAccounts = plan.MaxAdditionalServiceAccount;
|
||||
HasAdditionalServiceAccountOption = plan.HasAdditionalServiceAccountOption;
|
||||
MaxProjects = plan.MaxProjects;
|
||||
BitwardenProduct = plan.BitwardenProduct;
|
||||
StripeServiceAccountPlanId = plan.StripeServiceAccountPlanId;
|
||||
}
|
||||
|
||||
public PlanType Type { get; set; }
|
||||
@@ -105,10 +107,11 @@ public class PlanResponseModel : ResponseModel
|
||||
public decimal SeatPrice { get; set; }
|
||||
public decimal AdditionalStoragePricePerGb { get; set; }
|
||||
public decimal PremiumAccessOptionPrice { get; set; }
|
||||
|
||||
public string StripeServiceAccountPlanId { get; set; }
|
||||
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
||||
public short? BaseServiceAccount { get; set; }
|
||||
public short? MaxServiceAccount { get; set; }
|
||||
public short? MaxServiceAccounts { get; set; }
|
||||
public short? MaxAdditionalServiceAccounts { get; set; }
|
||||
public bool HasAdditionalServiceAccountOption { get; set; }
|
||||
public short? MaxProjects { get; set; }
|
||||
public BitwardenProductType BitwardenProduct { get; set; }
|
||||
|
||||
@@ -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.Enums;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
@@ -42,7 +43,15 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
|
||||
{
|
||||
_plan = plan;
|
||||
_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)
|
||||
@@ -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
|
||||
{
|
||||
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? MaxCollections { get; set; }
|
||||
public short? MaxUsers { get; set; }
|
||||
public short? MaxServiceAccounts { get; set; }
|
||||
public bool AllowSeatAutoscale { get; set; }
|
||||
|
||||
public bool AllowServiceAccountsAutoscale { get; set; }
|
||||
|
||||
public bool HasAdditionalSeatsOption { get; set; }
|
||||
public int? MaxAdditionalSeats { get; set; }
|
||||
public bool HasAdditionalStorageOption { get; set; }
|
||||
@@ -55,7 +58,7 @@ public class Plan
|
||||
public decimal PremiumAccessOptionPrice { get; set; }
|
||||
public decimal? AdditionalPricePerServiceAccount { get; set; }
|
||||
public short? BaseServiceAccount { get; set; }
|
||||
public short? MaxServiceAccount { get; set; }
|
||||
public short? MaxAdditionalServiceAccount { get; set; }
|
||||
public bool HasAdditionalServiceAccountOption { get; set; }
|
||||
public short? MaxProjects { 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.Interfaces;
|
||||
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.Interfaces;
|
||||
using Bit.Core.Services;
|
||||
@@ -41,6 +43,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddOrganizationGroupCommands();
|
||||
services.AddOrganizationLicenseCommandsQueries();
|
||||
services.AddOrganizationDomainCommandsQueries();
|
||||
services.AddOrganizationSubscriptionUpdateCommandsQueries();
|
||||
}
|
||||
|
||||
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
|
||||
@@ -110,6 +113,11 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationSubscriptionUpdateCommandsQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
|
||||
}
|
||||
|
||||
private static void AddTokenizers(this IServiceCollection services)
|
||||
{
|
||||
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 SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
||||
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);
|
||||
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> AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts,
|
||||
DateTime? prorationDate = null);
|
||||
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
|
||||
bool skipInAppPurchaseCheck = false);
|
||||
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
||||
|
||||
@@ -896,6 +896,36 @@ public class HandlebarsMailService : IMailService
|
||||
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)
|
||||
{
|
||||
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
||||
|
||||
@@ -427,15 +427,15 @@ public class OrganizationService : IOrganizationService
|
||||
if (newSecretsManagerPlan.BaseServiceAccount != null)
|
||||
{
|
||||
if (!organization.SmServiceAccounts.HasValue ||
|
||||
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccount)
|
||||
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccounts)
|
||||
{
|
||||
var currentServiceAccounts =
|
||||
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccount)
|
||||
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccounts)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"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);
|
||||
}
|
||||
|
||||
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,
|
||||
string storagePlanId, DateTime? prorationDate = null)
|
||||
{
|
||||
|
||||
@@ -238,4 +238,17 @@ public class NoopMailService : IMailService
|
||||
{
|
||||
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,
|
||||
[EnumMember(Value = "sm-service-account-accessed-secret")]
|
||||
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? PreviousSeats { get; set; }
|
||||
public int? PreviousServiceAccounts { get; set; }
|
||||
|
||||
public short? Storage { get; set; }
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ public static class SecretsManagerPlanStore
|
||||
SeatPrice = 13,
|
||||
AdditionalPricePerServiceAccount = 0.5M,
|
||||
AllowSeatAutoscale = true,
|
||||
AllowServiceAccountsAutoscale = true
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
@@ -83,6 +84,7 @@ public static class SecretsManagerPlanStore
|
||||
SeatPrice = 144,
|
||||
AdditionalPricePerServiceAccount = 6,
|
||||
AllowSeatAutoscale = true,
|
||||
AllowServiceAccountsAutoscale = true
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
@@ -113,6 +115,7 @@ public static class SecretsManagerPlanStore
|
||||
SeatPrice = 7,
|
||||
AdditionalPricePerServiceAccount = 0.5M,
|
||||
AllowSeatAutoscale = true,
|
||||
AllowServiceAccountsAutoscale = true
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
@@ -145,6 +148,7 @@ public static class SecretsManagerPlanStore
|
||||
SeatPrice = 72,
|
||||
AdditionalPricePerServiceAccount = 6,
|
||||
AllowSeatAutoscale = true,
|
||||
AllowServiceAccountsAutoscale = true
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
@@ -158,7 +162,7 @@ public static class SecretsManagerPlanStore
|
||||
BaseServiceAccount = 3,
|
||||
MaxProjects = 3,
|
||||
MaxUsers = 2,
|
||||
MaxServiceAccount = 3,
|
||||
MaxServiceAccounts = 3,
|
||||
UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to
|
||||
DisplaySortOrder = -1,
|
||||
AllowSeatAutoscale = false,
|
||||
|
||||
@@ -4,6 +4,8 @@ using AspNetCoreRateLimit;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories.Noop;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.Utilities;
|
||||
@@ -46,7 +48,7 @@ public class Startup
|
||||
// Repositories
|
||||
services.AddDatabaseRepositories(globalSettings);
|
||||
|
||||
services.AddOosServices();
|
||||
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
||||
|
||||
// Context
|
||||
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -40,6 +41,7 @@ public class OrganizationsControllerTests : IDisposable
|
||||
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
|
||||
private readonly OrganizationsController _sut;
|
||||
|
||||
@@ -64,12 +66,14 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_updateOrganizationLicenseCommand = Substitute.For<IUpdateOrganizationLicenseCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_licensingService = Substitute.For<ILicensingService>();
|
||||
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
||||
|
||||
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
||||
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
|
||||
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
||||
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
||||
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService);
|
||||
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService,
|
||||
_updateSecretsManagerSubscriptionCommand);
|
||||
}
|
||||
|
||||
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