1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

[AC-1512] Feature: Secrets Manager Billing - round 2 (#3119)

* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem (#3037)

* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem

- Add a helper method to determine the appropriate addon type based on the subscription items StripeId

* [AC-1423] Add helper to StaticStore.cs to find a Plan by StripePlanId

* [AC-1423] Use the helper method to set SubscriptionInfo.BitwardenProduct

* Add SecretsManagerBilling feature flag to Constants

* [AC 1409] Secrets Manager Subscription Stripe Integration  (#3019)

* Adding the Secret manager to the Plan List

* Adding the unit test for the StaticStoreTests class

* Fix whitespace formatting

* Fix whitespace formatting

* Price update

* Resolving the PR comments

* Resolving PR comments

* Fixing the whitespace

* only password manager plans are return for now

* format whitespace

* Resolve the test issue

* Fixing the failing test

* Refactoring the Plan separation

* add a unit test for SingleOrDefault

* Fix the whitespace format

* Separate the PM and SM plans

* Fixing the whitespace

* Remove unnecessary directive

* Fix imports ordering

* Fix imports ordering

* Resolve imports ordering

* Fixing imports ordering

* Fix response model, add MaxProjects

* Fix filename

* Fix format

* Fix: seat price should match annual/monthly

* Fix service account annual pricing

* Changes for secret manager signup and upgradeplan

* Changes for secrets manager signup and upgrade

* refactoring the code

* Format whitespace

* remove unnecessary using directive

* Resolve the PR comment on Subscription creation

* Resolve PR comment

* Add password manager to the error message

* Add UseSecretsManager to the event log

* Resolve PR comment on plan validation

* Resolving pr comments for service account count

* Resolving pr comments for service account count

* Resolve the pr comments

* Remove the store procedure that is no-longer needed

* Rename a property properly

* Resolving the PR comment

* Resolve PR comments

* Resolving PR comments

* Resolving the Pr comments

* Resolving some PR comments

* Resolving the PR comments

* Resolving the build identity build

* Add additional Validation

* Resolve the Lint issues

* remove unnecessary using directive

* Remove the white spaces

* Adding unit test for the stripe payment

* Remove the incomplete test

* Fixing the failing test

* Fix the failing test

* Fix the fail test on organization service

* Fix the failing unit test

* Fix the whitespace format

* Fix the failing test

* Fix the whitespace format

* resolve pr comments

* Fix the lint message

* Resolve the PR comments

* resolve pr comments

* Resolve pr comments

* Resolve the pr comments

* remove unused code

* Added for sm validation test

* Fix the whitespace format issues

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* SM-802: Add SecretsManagerBetaColumn SQL migration and Org table update

* SM-802: Run EF Migrations for SecretsManagerBeta

* SM-802: Update the two Org procs and View, and move data migration to a separate file

* SM-802: Add missing comma to Organization_Create

* [AC-1418] Add missing SecretsManagerPlan property to OrganizationResponseModel (#3055)

* SM-802: Remove extra GO statement from data migration script

* [AC 1460] Update Stripe Configuration (#3070)

* change the stripeseat id

* change service accountId to align with new product

* make all the Id name for consistent

* SM-802: Add SecretsManagerBeta to OrganizationResponseModel

* SM-802: Move SecretsManagerBeta from OrganizationResponseModel to OrganizationSubscriptionResponseModel. Use sp_refreshview instead of sp_refreshsqlmodule in the migration script.

* SM-802: Remove OrganizationUserOrganizationDetailsView.sql changes

* [AC 1410] Secrets Manager subscription adjustment back-end changes (#3036)

* Create UpgradeSecretsManagerSubscription command

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>

* SM-802: Remove SecretsManagerBetaColumn migration

* SM-802: Add SecretsManagerBetaColumn migration

* SM-802: Remove OrganizationUserOrganizationDetailsView update

* [AC-1495] Extract UpgradePlanAsync into a command (#3081)

* This is a pure lift & shift with no refactors

* Only register subscription commands in Api

---------

Co-authored-by: cyprain-okeke <cokeke@bitwarden.com>

* [AC-1503] Fix Stripe integration on organization upgrade (#3084)

* Fix SM parameters not being passed to Stripe

* Fix flaky test

* Fix error message

* [AC-1504] Allow SM max autoscale limits to be disabled (#3085)

* [AC-1488] Changed SM Signup and Upgrade paths to set SmServiceAccounts to include the plan BaseServiceAccount (#3086)

* [AC-1510] Enable access to Secrets Manager to Organization owner for new Subscription (#3089)

* Revert changes to ReferenceEvent code (#3091)

* Revert changes to ReferenceEvent code

This will be done in AC-1481

* Revert ReferenceEventType change

* Move NoopServiceAccountRepository to SM and update namespace

* [AC-1462] Add secrets manager service accounts autoscaling commands (#3059)

* Adding the Secret manager to the Plan List

* Adding the unit test for the StaticStoreTests class

* Fix whitespace formatting

* Fix whitespace formatting

* Price update

* Resolving the PR comments

* Resolving PR comments

* Fixing the whitespace

* only password manager plans are return for now

* format whitespace

* Resolve the test issue

* Fixing the failing test

* Refactoring the Plan separation

* add a unit test for SingleOrDefault

* Fix the whitespace format

* Separate the PM and SM plans

* Fixing the whitespace

* Remove unnecessary directive

* Fix imports ordering

* Fix imports ordering

* Resolve imports ordering

* Fixing imports ordering

* Fix response model, add MaxProjects

* Fix filename

* Fix format

* Fix: seat price should match annual/monthly

* Fix service account annual pricing

* Changes for secret manager signup and upgradeplan

* Changes for secrets manager signup and upgrade

* refactoring the code

* Format whitespace

* remove unnecessary using directive

* Changes for subscription Update

* Update the seatAdjustment and update

* Resolve the PR comment on Subscription creation

* Resolve PR comment

* Add password manager to the error message

* Add UseSecretsManager to the event log

* Resolve PR comment on plan validation

* Resolving pr comments for service account count

* Resolving pr comments for service account count

* Resolve the pr comments

* Remove the store procedure that is no-longer needed

* Add a new class for update subscription

* Modify the Update subscription for sm

* Add the missing property

* Rename a property properly

* Resolving the PR comment

* Resolve PR comments

* Resolving PR comments

* Resolving the Pr comments

* Resolving some PR comments

* Resolving the PR comments

* Resolving the build identity build

* Add additional Validation

* Resolve the Lint issues

* remove unnecessary using directive

* Remove the white spaces

* Adding unit test for the stripe payment

* Remove the incomplete test

* Fixing the failing test

* Fix the failing test

* Fix the fail test on organization service

* Fix the failing unit test

* Fix the whitespace format

* Fix the failing test

* Fix the whitespace format

* resolve pr comments

* Fix the lint message

* refactor the code

* Fix the failing Test

* adding a new endpoint

* Remove the unwanted code

* Changes for Command and Queries

* changes for command and queries

* Fix the Lint issues

* Fix imports ordering

* Resolve the PR comments

* resolve pr comments

* Resolve pr comments

* Fix the failing test on adjustSeatscommandtests

* Fix the failing test

* Fix the whitespaces

* resolve failing test

* rename a property

* Resolve the pr comments

* refactoring the existing implementation

* Resolve the whitespaces format issue

* Resolve the pr comments

* [AC-1462] Created IAvailableServiceAccountsQuery along its implementation and with unit tests

* [AC-1462] Renamed ICountNewServiceAccountSlotsRequiredQuery

* [AC-1462] Added IAutoscaleServiceAccountsCommand and implementation

* Add more unit testing

* fix the whitespaces issues

* [AC-1462] Added unit tests for AutoscaleServiceAccountsCommand

* Add more unit test

* Remove unnecessary directive

* Resolve some pr comments

* Adding more unit test

* adding more test

* add more test

* Resolving some pr comments

* Resolving some pr comments

* Resolving some pr comments

* resolve some pr comments

* Resolving pr comments

* remove whitespaces

* remove white spaces

* Resolving pr comments

* resolving pr comments and fixing white spaces

* resolving the lint error

* Run dotnet format

* resolving the pr comments

* Add a missing properties to plan response model

* Add the email sender for sm seat and service acct

* Add the email sender for sm seat and service acct

* Fix the failing test after email sender changes

* Add staticstorewrapper to properly test the plans

* Add more test and validate the existing test

* Fix the white spaces issues

* Remove staticstorewrapper and fix the test

* fix a null issue on autoscaling

* Suggestion: do all seat calculations in update model

* Resolve some pr comments

* resolving some pr comments

* Return value is unnecessary

* Resolve the failing test

* resolve pr comments

* Resolve the pr comments

* Resolving admin api failure and adding more test

* Resolve the issue failing admin project

* Fixing the failed test

* Clarify naming and add comments

* Clarify naming conventions

* Dotnet format

* Fix the failing dependency

* remove similar test

* [AC-1462] Rewrote AutoscaleServiceAccountsCommand to use UpdateSecretsManagerSubscriptionCommand which has the same logic

* [AC-1462] Deleted IAutoscaleServiceAccountsCommand as the logic will be moved to UpdateSecretsManagerSubscriptionCommand

* [AC-1462] Created method AdjustSecretsManagerServiceAccountsAsync

* [AC-1462] Changed SecretsManagerSubscriptionUpdate to only be set by its constructor

* [AC-1462] Added check to CountNewServiceAccountSlotsRequiredQuery and revised unit tests

* [AC-1462] Revised logic for CountNewServiceAccountSlotsRequiredQuery and fixed unit tests

* [AC-1462] Changed SecretsManagerSubscriptionUpdate to receive Organization as a parameter and fixed the unit tests

* [AC-1462] Renamed IUpdateSecretsManagerSubscriptionCommand methods UpdateSubscriptionAsync and AdjustServiceAccountsAsync

* [AC-1462] Rewrote unit test UpdateSubscriptionAsync_ValidInput_Passes

* [AC-1462] Registered CountNewServiceAccountSlotsRequiredQuery for dependency injection

* [AC-1462] Added parameter names to SecretsManagerSubscriptionUpdateRequestModel

* [AC-1462] Updated SecretsManagerSubscriptionUpdate logic to handle null parameters. Revised the unit tests to test null values

---------

Co-authored-by: cyprain-okeke <cokeke@bitwarden.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Add UsePasswordManager to sync data (#3114)

* [AC-1522] Fix service account check on upgrading (#3111)

* Resolved the checkmarx issues

* [AC-1521] Address checkmarx security feedback (#3124)

* Reinstate target attribute but add noopener noreferrer

* Update date on migration script

* Remove unused constant

* Revert "Remove unused constant"

This reverts commit 4fcb9da4d62af815c01579ab265d0ce11b47a9bb.

This is required to make feature flags work on the client

* [AC-1458] Add Endpoint And Service Logic for secrets manager to existing subscription (#3087)

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>

* Remove duplicate migrations from incorrectly resolved merge

* [AC-1468] Modified CountNewServiceAccountSlotsRequiredQuery to return zero if organization has SecretsManagerBeta == true (#3112)

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>

* [Ac 1563] Unable to load billing and subscription related pages for non-enterprise organizations (#3138)

* Resolve the failing family plan

* resolve issues

* Resolve code related pr comments

* Resolve test related comments

* Resolving or comments

* [SM-809] Add service account slot limit check (#3093)

* Add service account slot limit check

* Add query to DI

* [AC-1462] Registered CountNewServiceAccountSlotsRequiredQuery for dependency injection

* remove duplicate DI entry

* Update unit tests

* Remove comment

* Code review updates

---------

Co-authored-by: cyprain-okeke <cokeke@bitwarden.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Rui Tome <rtome@bitwarden.com>

* [AC-1461] Secrets manager seat autoscaling (#3121)

* Add autoscaling code to invite user, save user, and bulk enable SM
  flows

* Add tests

* Delete command for BulkEnableSecretsManager

* circular dependency between OrganizationService and
  UpdateSecretsManagerSubscriptionCommand - fixed by temporarily
  duplicating ReplaceAndUpdateCache

* Unresolvable dependencies in other services - fixed by temporarily
  registering noop services and moving around some DI code

All should be resolved in PM-1880

* Refactor: improve the update object and use it to adjust values,
  remove excess interfaces on the command

* Handle autoscaling-specific errors

---------

Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>

* Move bitwarden_license include reference into conditional block

* [AC 1526]Show current SM seat and service account usage in Bitwarden Portal (#3142)

* changes base on the tickets request

* Code refactoring

* Removed the unwanted method

* Add implementation to the new method

* Resolve some pr comments

* resolve lint issue

* resolve pr comments

* add the new noop files

* Add new noop file and resolve some pr comments

* resolve pr comments

* removed unused method

---------

Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: cyprain-okeke <cokeke@bitwarden.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
Co-authored-by: Rui Tome <rtome@bitwarden.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
Thomas Rittson 2023-08-05 07:51:12 +10:00 committed by GitHub
parent 174d890234
commit 3573aee2ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2003 additions and 652 deletions

View File

@ -0,0 +1,44 @@
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
public class CountNewServiceAccountSlotsRequiredQuery : ICountNewServiceAccountSlotsRequiredQuery
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public CountNewServiceAccountSlotsRequiredQuery(
IOrganizationRepository organizationRepository,
IServiceAccountRepository serviceAccountRepository)
{
_organizationRepository = organizationRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<int> CountNewServiceAccountSlotsRequiredAsync(Guid organizationId, int serviceAccountsToAdd)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization == null || !organization.UseSecretsManager)
{
throw new NotFoundException();
}
if (!organization.SmServiceAccounts.HasValue || serviceAccountsToAdd == 0 || organization.SecretsManagerBeta)
{
return 0;
}
var serviceAccountCount = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organizationId);
var availableServiceAccountSlots = organization.SmServiceAccounts.Value - serviceAccountCount;
if (availableServiceAccountSlots >= serviceAccountsToAdd)
{
return 0;
}
return serviceAccountsToAdd - availableServiceAccountSlots;
}
}

View File

@ -44,6 +44,7 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<ICreateServiceAccountCommand, CreateServiceAccountCommand>(); services.AddScoped<ICreateServiceAccountCommand, CreateServiceAccountCommand>();
services.AddScoped<IUpdateServiceAccountCommand, UpdateServiceAccountCommand>(); services.AddScoped<IUpdateServiceAccountCommand, UpdateServiceAccountCommand>();
services.AddScoped<IDeleteServiceAccountsCommand, DeleteServiceAccountsCommand>(); services.AddScoped<IDeleteServiceAccountsCommand, DeleteServiceAccountsCommand>();
services.AddScoped<ICountNewServiceAccountSlotsRequiredQuery, CountNewServiceAccountSlotsRequiredQuery>();
services.AddScoped<IRevokeAccessTokensCommand, RevokeAccessTokensCommand>(); services.AddScoped<IRevokeAccessTokensCommand, RevokeAccessTokensCommand>();
services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>(); services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();
services.AddScoped<ICreateAccessPoliciesCommand, CreateAccessPoliciesCommand>(); services.AddScoped<ICreateAccessPoliciesCommand, CreateAccessPoliciesCommand>();

View File

@ -40,6 +40,16 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
return await projects.ToListAsync(); return await projects.ToListAsync();
} }
public async Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
return await dbContext.Project
.CountAsync(ou => ou.OrganizationId == organizationId);
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyByOrganizationIdWriteAccessAsync( public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyByOrganizationIdWriteAccessAsync(
Guid organizationId, Guid userId, AccessClientType accessType) Guid organizationId, Guid userId, AccessClientType accessType)
{ {

View File

@ -57,6 +57,16 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return await secrets.ToListAsync(); return await secrets.ToListAsync();
} }
public async Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
return await dbContext.Secret
.CountAsync(ou => ou.OrganizationId == organizationId && ou.DeletedDate == null);
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids) public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())

View File

@ -1,5 +1,7 @@
using System.Globalization; using System.Globalization;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.SecretsManager.Repositories.Noop;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Scim.Context; using Bit.Scim.Context;
@ -69,6 +71,10 @@ public class Startup
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should
// TODO: no longer be required - see PM-1880
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
// Mvc // Mvc
services.AddMvc(config => services.AddMvc(config =>
{ {

View File

@ -1,5 +1,7 @@
using Bit.Core; using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.SecretsManager.Repositories.Noop;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
@ -78,6 +80,10 @@ public class Startup
services.AddBaseServices(globalSettings); services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices(); services.AddCoreLocalizationServices();
// TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should
// TODO: no longer be required - see PM-1880
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
} }
public void Configure( public void Configure(

View File

@ -0,0 +1,125 @@
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.ServiceAccounts;
[SutProviderCustomize]
public class CountNewServiceAccountSlotsRequiredQueryTests
{
[Theory]
[BitAutoData(2, 5, 2, 0)]
[BitAutoData(0, 5, 2, 0)]
[BitAutoData(6, 5, 2, 3)]
[BitAutoData(2, 5, 10, 7)]
public async Task CountNewServiceAccountSlotsRequiredAsync_ReturnsCorrectCount(
int serviceAccountsToAdd,
int organizationSmServiceAccounts,
int currentServiceAccounts,
int expectedNewServiceAccountsRequired,
Organization organization,
SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)
{
organization.UseSecretsManager = true;
organization.SmServiceAccounts = organizationSmServiceAccounts;
organization.SecretsManagerBeta = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IServiceAccountRepository>()
.GetServiceAccountCountByOrganizationIdAsync(organization.Id)
.Returns(currentServiceAccounts);
var result = await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd);
Assert.Equal(expectedNewServiceAccountsRequired, result);
if (serviceAccountsToAdd > 0)
{
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
}
}
[Theory]
[BitAutoData(0)]
[BitAutoData(5)]
public async Task CountNewServiceAccountSlotsRequiredAsync_WithNullSmServiceAccounts_ReturnsZero(
int currentServiceAccounts,
int serviceAccountsToAdd,
Organization organization,
SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)
{
const int expectedRequiredServiceAccountsToScale = 0;
organization.UseSecretsManager = true;
organization.SmServiceAccounts = null;
organization.SecretsManagerBeta = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IServiceAccountRepository>()
.GetServiceAccountCountByOrganizationIdAsync(organization.Id)
.Returns(currentServiceAccounts);
var result = await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd);
Assert.Equal(expectedRequiredServiceAccountsToScale, result);
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs()
.GetServiceAccountCountByOrganizationIdAsync(default);
}
[Theory, BitAutoData]
public async Task CountNewServiceAccountSlotsRequiredAsync_WithSecretsManagerBeta_ReturnsZero(
int serviceAccountsToAdd,
Organization organization,
SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)
{
organization.UseSecretsManager = true;
organization.SecretsManagerBeta = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var result = await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd);
Assert.Equal(0, result);
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs()
.GetServiceAccountCountByOrganizationIdAsync(default);
}
[Theory, BitAutoData]
public async Task CountNewServiceAccountSlotsRequiredAsync_WithNonExistentOrganizationId_ThrowsNotFound(
Guid organizationId, int serviceAccountsToAdd,
SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)
{
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organizationId, serviceAccountsToAdd));
}
[Theory, BitAutoData]
public async Task CountNewServiceAccountSlotsRequiredAsync_WithOrganizationUseSecretsManagerFalse_ThrowsNotFound(
Organization organization, int serviceAccountsToAdd,
SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)
{
organization.UseSecretsManager = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd));
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<UserSecretsId>bitwarden-Admin</UserSecretsId> <UserSecretsId>bitwarden-Admin</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Admin' " /> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Admin' " />
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Admin-SelfHost' " /> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Admin-SelfHost' " />
<ItemGroup> <ItemGroup>
@ -19,6 +19,7 @@
<When Condition="!$(DefineConstants.Contains('OSS'))"> <When Condition="!$(DefineConstants.Contains('OSS'))">
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\bitwarden_license\src\Commercial.Core\Commercial.Core.csproj" /> <ProjectReference Include="..\..\bitwarden_license\src\Commercial.Core\Commercial.Core.csproj" />
<ProjectReference Include="..\..\bitwarden_license\src\Commercial.Infrastructure.EntityFramework\Commercial.Infrastructure.EntityFramework.csproj" />
</ItemGroup> </ItemGroup>
</When> </When>
</Choose> </Choose>

View File

@ -8,6 +8,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
@ -42,6 +43,9 @@ public class OrganizationsController : Controller
private readonly ILogger<OrganizationsController> _logger; private readonly ILogger<OrganizationsController> _logger;
private readonly IAccessControlService _accessControlService; private readonly IAccessControlService _accessControlService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService, IOrganizationService organizationService,
@ -62,7 +66,10 @@ public class OrganizationsController : Controller
IProviderRepository providerRepository, IProviderRepository providerRepository,
ILogger<OrganizationsController> logger, ILogger<OrganizationsController> logger,
IAccessControlService accessControlService, IAccessControlService accessControlService,
ICurrentContext currentContext) ICurrentContext currentContext,
ISecretRepository secretRepository,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -83,6 +90,9 @@ public class OrganizationsController : Controller
_logger = logger; _logger = logger;
_accessControlService = accessControlService; _accessControlService = accessControlService;
_currentContext = currentContext; _currentContext = currentContext;
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
} }
[RequirePermission(Permission.Org_List_View)] [RequirePermission(Permission.Org_List_View)]
@ -137,7 +147,14 @@ public class OrganizationsController : Controller
} }
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null; var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies)); var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;
var smSeats = organization.UseSecretsManager
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
: -1;
return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies,
secrets, projects, serviceAccounts, smSeats));
} }
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
@ -165,8 +182,14 @@ public class OrganizationsController : Controller
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var billingInfo = await _paymentService.GetBillingAsync(organization); var billingInfo = await _paymentService.GetBillingAsync(organization);
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null; var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;
var smSeats = organization.UseSecretsManager
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
: -1;
return View(new OrganizationEditModel(organization, provider, users, ciphers, collections, groups, policies, return View(new OrganizationEditModel(organization, provider, users, ciphers, collections, groups, policies,
billingInfo, billingSyncConnection, _globalSettings)); billingInfo, billingSyncConnection, _globalSettings, secrets, projects, serviceAccounts, smSeats));
} }
[HttpPost] [HttpPost]

View File

@ -28,8 +28,9 @@ public class OrganizationEditModel : OrganizationViewModel
public OrganizationEditModel(Organization org, Provider provider, IEnumerable<OrganizationUserUserDetails> orgUsers, public OrganizationEditModel(Organization org, Provider provider, IEnumerable<OrganizationUserUserDetails> orgUsers,
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups,
IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections, IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections,
GlobalSettings globalSettings) GlobalSettings globalSettings, int secrets, int projects, int serviceAccounts, int smSeats)
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies) : base(org, provider, connections, orgUsers, ciphers, collections, groups, policies, secrets, projects,
serviceAccounts, smSeats)
{ {
BillingInfo = billingInfo; BillingInfo = billingInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId; BraintreeMerchantId = globalSettings.Braintree.MerchantId;

View File

@ -12,7 +12,9 @@ public class OrganizationViewModel
public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections, public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections,
IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<Group> groups, IEnumerable<Policy> policies) IEnumerable<Group> groups, IEnumerable<Policy> policies, int secretsCount, int projectCount, int serviceAccountsCount,
int smSeatsCount)
{ {
Organization = org; Organization = org;
Provider = provider; Provider = provider;
@ -37,6 +39,10 @@ public class OrganizationViewModel
orgUsers orgUsers
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus) .Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
.Select(u => u.Email)); .Select(u => u.Email));
Secrets = secretsCount;
Projects = projectCount;
ServiceAccounts = serviceAccountsCount;
SmSeats = smSeatsCount;
} }
public Organization Organization { get; set; } public Organization Organization { get; set; }
@ -53,4 +59,9 @@ public class OrganizationViewModel
public int GroupCount { get; set; } public int GroupCount { get; set; }
public int PolicyCount { get; set; } public int PolicyCount { get; set; }
public bool HasPublicPrivateKeys { get; set; } public bool HasPublicPrivateKeys { get; set; }
public int Secrets { get; set; }
public int Projects { get; set; }
public int ServiceAccounts { get; set; }
public int SmSeats { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager;
} }

View File

@ -12,6 +12,7 @@ using Bit.Admin.Services;
#if !OSS #if !OSS
using Bit.Commercial.Core.Utilities; using Bit.Commercial.Core.Utilities;
using Bit.Commercial.Infrastructure.EntityFramework.SecretsManager;
#endif #endif
namespace Bit.Admin; namespace Bit.Admin;
@ -91,6 +92,7 @@ public class Startup
services.AddOosServices(); services.AddOosServices();
#else #else
services.AddCommercialCoreServices(); services.AddCommercialCoreServices();
services.AddSecretsManagerEfRepositories();
#endif #endif
// Mvc // Mvc

View File

@ -32,6 +32,18 @@
<dt class="col-sm-4 col-lg-3">Collections</dt> <dt class="col-sm-4 col-lg-3">Collections</dt>
<dd class="col-sm-8 col-lg-9">@Model.CollectionCount</dd> <dd class="col-sm-8 col-lg-9">@Model.CollectionCount</dd>
<dt class="col-sm-4 col-lg-3">Secrets</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.Secrets: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Projects</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.Projects: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Service Accounts</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ServiceAccounts: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.SmSeats: "N/A" )</dd>
<dt class="col-sm-4 col-lg-3">Groups</dt> <dt class="col-sm-4 col-lg-3">Groups</dt>
<dd class="col-sm-8 col-lg-9">@Model.GroupCount</dd> <dd class="col-sm-8 col-lg-9">@Model.GroupCount</dd>

View File

@ -2821,7 +2821,15 @@
"commercial.core": { "commercial.core": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )" "Core": "[2023.7.1, )"
}
},
"commercial.infrastructure.entityframework": {
"type": "Project",
"dependencies": {
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
"Core": "[2023.7.1, )",
"Infrastructure.EntityFramework": "[2023.7.1, )"
} }
}, },
"core": { "core": {
@ -2869,7 +2877,7 @@
"infrastructure.dapper": { "infrastructure.dapper": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Dapper": "[2.0.123, )" "Dapper": "[2.0.123, )"
} }
}, },
@ -2877,7 +2885,7 @@
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )", "AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )", "Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )", "Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )", "Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
@ -2889,7 +2897,7 @@
"migrator": { "migrator": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Microsoft.Extensions.Logging": "[6.0.0, )", "Microsoft.Extensions.Logging": "[6.0.0, )",
"dbup-sqlserver": "[5.0.8, )" "dbup-sqlserver": "[5.0.8, )"
} }
@ -2897,30 +2905,30 @@
"mysqlmigrations": { "mysqlmigrations": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Infrastructure.EntityFramework": "[2023.5.1, )" "Infrastructure.EntityFramework": "[2023.7.1, )"
} }
}, },
"postgresmigrations": { "postgresmigrations": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Infrastructure.EntityFramework": "[2023.5.1, )" "Infrastructure.EntityFramework": "[2023.7.1, )"
} }
}, },
"sharedweb": { "sharedweb": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Infrastructure.Dapper": "[2023.5.1, )", "Infrastructure.Dapper": "[2023.7.1, )",
"Infrastructure.EntityFramework": "[2023.5.1, )" "Infrastructure.EntityFramework": "[2023.7.1, )"
} }
}, },
"sqlitemigrations": { "sqlitemigrations": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Core": "[2023.5.1, )", "Core": "[2023.7.1, )",
"Infrastructure.EntityFramework": "[2023.5.1, )" "Infrastructure.EntityFramework": "[2023.7.1, )"
} }
} }
} }

View File

@ -8,8 +8,9 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -20,7 +21,6 @@ namespace Bit.Api.Controllers;
[Authorize("Application")] [Authorize("Application")]
public class OrganizationUsersController : Controller public class OrganizationUsersController : Controller
{ {
private readonly IEnableAccessSecretsManagerCommand _enableAccessSecretsManagerCommand;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
@ -29,9 +29,10 @@ public class OrganizationUsersController : Controller
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
public OrganizationUsersController( public OrganizationUsersController(
IEnableAccessSecretsManagerCommand enableAccessSecretsManagerCommand,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
@ -39,9 +40,10 @@ public class OrganizationUsersController : Controller
IGroupRepository groupRepository, IGroupRepository groupRepository,
IUserService userService, IUserService userService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ICurrentContext currentContext) ICurrentContext currentContext,
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
{ {
_enableAccessSecretsManagerCommand = enableAccessSecretsManagerCommand;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationService = organizationService; _organizationService = organizationService;
@ -50,6 +52,8 @@ public class OrganizationUsersController : Controller
_userService = userService; _userService = userService;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_currentContext = currentContext; _currentContext = currentContext;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -426,7 +430,7 @@ public class OrganizationUsersController : Controller
[HttpPatch("enable-secrets-manager")] [HttpPatch("enable-secrets-manager")]
[HttpPut("enable-secrets-manager")] [HttpPut("enable-secrets-manager")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkEnableSecretsManagerAsync(Guid orgId, public async Task BulkEnableSecretsManagerAsync(Guid orgId,
[FromBody] OrganizationUserBulkRequestModel model) [FromBody] OrganizationUserBulkRequestModel model)
{ {
if (!await _currentContext.ManageUsers(orgId)) if (!await _currentContext.ManageUsers(orgId))
@ -435,16 +439,28 @@ public class OrganizationUsersController : Controller
} }
var orgUsers = (await _organizationUserRepository.GetManyAsync(model.Ids)) var orgUsers = (await _organizationUserRepository.GetManyAsync(model.Ids))
.Where(ou => ou.OrganizationId == orgId).ToList(); .Where(ou => ou.OrganizationId == orgId && !ou.AccessSecretsManager).ToList();
if (orgUsers.Count == 0) if (orgUsers.Count == 0)
{ {
throw new BadRequestException("Users invalid."); throw new BadRequestException("Users invalid.");
} }
var results = await _enableAccessSecretsManagerCommand.EnableUsersAsync(orgUsers); var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(orgId,
orgUsers.Count);
if (additionalSmSeatsRequired > 0)
{
var organization = await _organizationRepository.GetByIdAsync(orgId);
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r => foreach (var orgUser in orgUsers)
new OrganizationUserBulkResponseModel(r.organizationUser.Id, r.error))); {
orgUser.AccessSecretsManager = true;
}
await _organizationUserRepository.ReplaceManyAsync(orgUsers);
} }
private async Task RestoreOrRevokeUserAsync( private async Task RestoreOrRevokeUserAsync(

View File

@ -53,6 +53,7 @@ public class OrganizationsController : Controller
private readonly ILicensingService _licensingService; private readonly ILicensingService _licensingService;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand; private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
public OrganizationsController( public OrganizationsController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -75,7 +76,8 @@ public class OrganizationsController : Controller
GlobalSettings globalSettings, GlobalSettings globalSettings,
ILicensingService licensingService, ILicensingService licensingService,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand) IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -98,6 +100,7 @@ public class OrganizationsController : Controller
_licensingService = licensingService; _licensingService = licensingService;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand; _upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -344,14 +347,33 @@ public class OrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
var secretsManagerPlan = StaticStore.GetSecretsManagerPlan(organization.PlanType); var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization);
if (secretsManagerPlan == null) await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
}
[HttpPost("{id}/subscribe-secrets-manager")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<ProfileOrganizationResponseModel> PostSubscribeSecretsManagerAsync(Guid id, [FromBody] SecretsManagerSubscribeRequestModel model)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{ {
throw new NotFoundException("Invalid Secrets Manager plan."); throw new NotFoundException();
} }
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, secretsManagerPlan); if (!await _currentContext.EditSubscription(id))
await _updateSecretsManagerSubscriptionCommand.UpdateSecretsManagerSubscription(organizationUpdate); {
throw new NotFoundException();
}
await _addSecretsManagerSubscriptionCommand.SignUpAsync(organization, model.AdditionalSmSeats,
model.AdditionalServiceAccounts);
var userId = _userService.GetProperUserId(User).Value;
var organizationDetails = await _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
OrganizationUserStatusType.Confirmed);
return new ProfileOrganizationResponseModel(organizationDetails);
} }
[HttpPost("{id}/seat")] [HttpPost("{id}/seat")]

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models.Request.Organizations;
public class SecretsManagerSubscribeRequestModel
{
[Required]
[Range(0, int.MaxValue)]
public int AdditionalSmSeats { get; set; }
[Required]
[Range(0, int.MaxValue)]
public int AdditionalServiceAccounts { get; set; }
}

View File

@ -1,7 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Models.Request.Organizations; namespace Bit.Api.Models.Request.Organizations;
@ -13,32 +12,12 @@ public class SecretsManagerSubscriptionUpdateRequestModel
public int ServiceAccountAdjustment { get; set; } public int ServiceAccountAdjustment { get; set; }
public int? MaxAutoscaleServiceAccounts { get; set; } public int? MaxAutoscaleServiceAccounts { get; set; }
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan) public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
{ {
var newTotalSeats = organization.SmSeats.GetValueOrDefault() + SeatAdjustment; var orgUpdate = new SecretsManagerSubscriptionUpdate(
var newTotalServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + ServiceAccountAdjustment; organization,
seatAdjustment: SeatAdjustment, maxAutoscaleSeats: MaxAutoscaleSeats,
var orgUpdate = new SecretsManagerSubscriptionUpdate serviceAccountAdjustment: ServiceAccountAdjustment, maxAutoscaleServiceAccounts: MaxAutoscaleServiceAccounts);
{
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 =
MaxAutoscaleSeats.GetValueOrDefault() != organization.MaxAutoscaleSmSeats.GetValueOrDefault(),
MaxAutoscaleSmServiceAccountsChanged =
MaxAutoscaleServiceAccounts.GetValueOrDefault() != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault()
};
return orgUpdate; return orgUpdate;
} }

View File

@ -27,7 +27,11 @@ public class OrganizationResponseModel : ResponseModel
BusinessTaxNumber = organization.BusinessTaxNumber; BusinessTaxNumber = organization.BusinessTaxNumber;
BillingEmail = organization.BillingEmail; BillingEmail = organization.BillingEmail;
Plan = new PlanResponseModel(StaticStore.PasswordManagerPlans.FirstOrDefault(plan => plan.Type == organization.PlanType)); Plan = new PlanResponseModel(StaticStore.PasswordManagerPlans.FirstOrDefault(plan => plan.Type == organization.PlanType));
SecretsManagerPlan = new PlanResponseModel(StaticStore.SecretManagerPlans.FirstOrDefault(plan => plan.Type == organization.PlanType)); var matchingPlan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
if (matchingPlan != null)
{
SecretsManagerPlan = new PlanResponseModel(matchingPlan);
}
PlanType = organization.PlanType; PlanType = organization.PlanType;
Seats = organization.Seats; Seats = organization.Seats;
MaxAutoscaleSeats = organization.MaxAutoscaleSeats; MaxAutoscaleSeats = organization.MaxAutoscaleSeats;

View File

@ -4,6 +4,8 @@ using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces; using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
@ -27,6 +29,9 @@ public class ServiceAccountsController : Controller
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IApiKeyRepository _apiKeyRepository; private readonly IApiKeyRepository _apiKeyRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly ICountNewServiceAccountSlotsRequiredQuery _countNewServiceAccountSlotsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IServiceAccountSecretsDetailsQuery _serviceAccountSecretsDetailsQuery; private readonly IServiceAccountSecretsDetailsQuery _serviceAccountSecretsDetailsQuery;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand; private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand; private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
@ -40,6 +45,9 @@ public class ServiceAccountsController : Controller
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
IServiceAccountRepository serviceAccountRepository, IServiceAccountRepository serviceAccountRepository,
IApiKeyRepository apiKeyRepository, IApiKeyRepository apiKeyRepository,
IOrganizationRepository organizationRepository,
ICountNewServiceAccountSlotsRequiredQuery countNewServiceAccountSlotsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IServiceAccountSecretsDetailsQuery serviceAccountSecretsDetailsQuery, IServiceAccountSecretsDetailsQuery serviceAccountSecretsDetailsQuery,
ICreateAccessTokenCommand createAccessTokenCommand, ICreateAccessTokenCommand createAccessTokenCommand,
ICreateServiceAccountCommand createServiceAccountCommand, ICreateServiceAccountCommand createServiceAccountCommand,
@ -52,12 +60,15 @@ public class ServiceAccountsController : Controller
_authorizationService = authorizationService; _authorizationService = authorizationService;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository; _apiKeyRepository = apiKeyRepository;
_organizationRepository = organizationRepository;
_countNewServiceAccountSlotsRequiredQuery = countNewServiceAccountSlotsRequiredQuery;
_serviceAccountSecretsDetailsQuery = serviceAccountSecretsDetailsQuery; _serviceAccountSecretsDetailsQuery = serviceAccountSecretsDetailsQuery;
_createServiceAccountCommand = createServiceAccountCommand; _createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand;
_deleteServiceAccountsCommand = deleteServiceAccountsCommand; _deleteServiceAccountsCommand = deleteServiceAccountsCommand;
_revokeAccessTokensCommand = revokeAccessTokensCommand; _revokeAccessTokensCommand = revokeAccessTokensCommand;
_createAccessTokenCommand = createAccessTokenCommand; _createAccessTokenCommand = createAccessTokenCommand;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
} }
[HttpGet("/organizations/{organizationId}/service-accounts")] [HttpGet("/organizations/{organizationId}/service-accounts")]
@ -109,6 +120,15 @@ public class ServiceAccountsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
var newServiceAccountSlotsRequired = await _countNewServiceAccountSlotsRequiredQuery
.CountNewServiceAccountSlotsRequiredAsync(organizationId, 1);
if (newServiceAccountSlotsRequired > 0)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
await _updateSecretsManagerSubscriptionCommand.AdjustServiceAccountsAsync(org,
newServiceAccountSlotsRequired);
}
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var result = var result =
await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId); await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId);

View File

@ -1,5 +1,7 @@
using System.Globalization; using System.Globalization;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.SecretsManager.Repositories.Noop;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
@ -58,6 +60,10 @@ public class Startup
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should
// TODO: no longer be required - see PM-1880
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
// Mvc // Mvc
services.AddMvc(config => services.AddMvc(config =>
{ {

View File

@ -1,24 +1,17 @@
namespace Bit.Core.Models.Business; using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.StaticStore;
namespace Bit.Core.Models.Business;
public class SecretsManagerSubscriptionUpdate public class SecretsManagerSubscriptionUpdate
{ {
public Guid OrganizationId { get; set; } public Organization Organization { get; set; }
/// <summary>
/// The seats to be added or removed from the organization
/// </summary>
public int SmSeatsAdjustment { get; set; }
/// <summary> /// <summary>
/// The total seats the organization will have after the update, including any base seats included in the plan /// The total seats the organization will have after the update, including any base seats included in the plan
/// </summary> /// </summary>
public int SmSeats { get; set; } 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> /// <summary>
/// The new autoscale limit for seats, expressed as a total (not an adjustment). /// The new autoscale limit for seats, expressed as a total (not an adjustment).
@ -26,22 +19,11 @@ public class SecretsManagerSubscriptionUpdate
/// </summary> /// </summary>
public int? MaxAutoscaleSmSeats { get; set; } public int? MaxAutoscaleSmSeats { get; set; }
/// <summary>
/// The service accounts to be added or removed from the organization
/// </summary>
public int SmServiceAccountsAdjustment { get; set; }
/// <summary> /// <summary>
/// The total service accounts the organization will have after the update, including the base service accounts /// The total service accounts the organization will have after the update, including the base service accounts
/// included in the plan /// included in the plan
/// </summary> /// </summary>
public int SmServiceAccounts { get; set; } 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> /// <summary>
/// The new autoscale limit for service accounts, expressed as a total (not an adjustment). /// The new autoscale limit for service accounts, expressed as a total (not an adjustment).
@ -49,8 +31,73 @@ public class SecretsManagerSubscriptionUpdate
/// </summary> /// </summary>
public int? MaxAutoscaleSmServiceAccounts { get; set; } public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool SmSeatsChanged => SmSeatsAdjustment != 0; /// <summary>
public bool SmServiceAccountsChanged => SmServiceAccountsAdjustment != 0; /// The proration date for the subscription update (optional)
public bool MaxAutoscaleSmSeatsChanged { get; set; } /// </summary>
public bool MaxAutoscaleSmServiceAccountsChanged { get; set; } public DateTime? ProrationDate { get; set; }
/// <summary>
/// Whether the subscription update is a result of autoscaling
/// </summary>
public bool Autoscaling { get; init; }
/// <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 => SmSeats.HasValue ? SmSeats.Value - Plan.BaseSeats : 0;
/// <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 => SmServiceAccounts.HasValue ? SmServiceAccounts.Value - Plan.BaseServiceAccount.GetValueOrDefault() : 0;
public bool SmSeatsChanged => SmSeats != Organization.SmSeats;
public bool SmServiceAccountsChanged => SmServiceAccounts != Organization.SmServiceAccounts;
public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats;
public bool MaxAutoscaleSmServiceAccountsChanged =>
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
public Plan Plan => Utilities.StaticStore.GetSecretsManagerPlan(Organization.PlanType);
public SecretsManagerSubscriptionUpdate(
Organization organization,
int seatAdjustment, int? maxAutoscaleSeats,
int serviceAccountAdjustment, int? maxAutoscaleServiceAccounts) : this(organization, false)
{
AdjustSeats(seatAdjustment);
AdjustServiceAccounts(serviceAccountAdjustment);
MaxAutoscaleSmSeats = maxAutoscaleSeats;
MaxAutoscaleSmServiceAccounts = maxAutoscaleServiceAccounts;
}
public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling)
{
if (organization == null)
{
throw new NotFoundException("Organization is not found.");
}
Organization = organization;
if (Plan == null)
{
throw new NotFoundException("Invalid Secrets Manager plan.");
}
SmSeats = organization.SmSeats;
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
SmServiceAccounts = organization.SmServiceAccounts;
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
Autoscaling = autoscaling;
}
public void AdjustSeats(int adjustment)
{
SmSeats = SmSeats.GetValueOrDefault() + adjustment;
}
public void AdjustServiceAccounts(int adjustment)
{
SmServiceAccounts = SmServiceAccounts.GetValueOrDefault() + adjustment;
}
} }

View File

@ -30,7 +30,6 @@ public abstract class SubscriptionUpdate
planId == null ? null : subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); planId == null ? null : subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId);
} }
public class SeatSubscriptionUpdate : SubscriptionUpdate public class SeatSubscriptionUpdate : SubscriptionUpdate
{ {
private readonly int _previousSeats; private readonly int _previousSeats;
@ -262,3 +261,77 @@ public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate
SubscriptionItem(subscription, _existingPlanStripeId); SubscriptionItem(subscription, _existingPlanStripeId);
} }
public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
{
private readonly StaticStore.Plan _plan;
private readonly long? _additionalSeats;
private readonly long? _additionalServiceAccounts;
private readonly int _previousSeats;
private readonly int _previousServiceAccounts;
protected override List<string> PlanIds => new() { _plan.StripeSeatPlanId, _plan.StripeServiceAccountPlanId };
public SecretsManagerSubscribeUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats, long? additionalServiceAccounts)
{
_plan = plan;
_additionalSeats = additionalSeats;
_additionalServiceAccounts = additionalServiceAccounts;
_previousSeats = organization.SmSeats.GetValueOrDefault();
_previousServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault();
}
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
{
var updatedItems = new List<SubscriptionItemOptions>();
RemovePreviousSecretsManagerItems(updatedItems);
return updatedItems;
}
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
{
var updatedItems = new List<SubscriptionItemOptions>();
AddNewSecretsManagerItems(updatedItems);
return updatedItems;
}
private void AddNewSecretsManagerItems(List<SubscriptionItemOptions> updatedItems)
{
if (_additionalSeats > 0)
{
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.StripeSeatPlanId,
Quantity = _additionalSeats
});
}
if (_additionalServiceAccounts > 0)
{
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.StripeServiceAccountPlanId,
Quantity = _additionalServiceAccounts
});
}
}
private void RemovePreviousSecretsManagerItems(List<SubscriptionItemOptions> updatedItems)
{
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.StripeSeatPlanId,
Quantity = _previousSeats,
Deleted = _previousSeats == 0 ? true : (bool?)null,
});
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.StripeServiceAccountPlanId,
Quantity = _previousServiceAccounts,
Deleted = _previousServiceAccounts == 0 ? true : (bool?)null,
});
}
}

View File

@ -17,8 +17,10 @@ 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.SecretsManager.Commands.EnableAccessSecretsManager; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens; using Bit.Core.Tokens;
@ -33,7 +35,6 @@ public static class OrganizationServiceCollectionExtensions
public static void AddOrganizationServices(this IServiceCollection services, IGlobalSettings globalSettings) public static void AddOrganizationServices(this IServiceCollection services, IGlobalSettings globalSettings)
{ {
services.AddScoped<IOrganizationService, OrganizationService>(); services.AddScoped<IOrganizationService, OrganizationService>();
services.AddScoped<IEnableAccessSecretsManagerCommand, EnableAccessSecretsManagerCommand>();
services.AddTokenizers(); services.AddTokenizers();
services.AddOrganizationGroupCommands(); services.AddOrganizationGroupCommands();
services.AddOrganizationConnectionCommands(); services.AddOrganizationConnectionCommands();
@ -44,6 +45,8 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationAuthCommands(); services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommandsQueries();
services.AddBaseOrganizationSubscriptionCommandsQueries();
} }
private static void AddOrganizationConnectionCommands(this IServiceCollection services) private static void AddOrganizationConnectionCommands(this IServiceCollection services)
@ -118,6 +121,18 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>(); services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>();
} }
private static void AddOrganizationUserCommandsQueries(this IServiceCollection services)
{
services.AddScoped<ICountNewSmSeatsRequiredQuery, CountNewSmSeatsRequiredQuery>();
}
// TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of
// TODO: OrganizationService - see PM-1880
private static void AddBaseOrganizationSubscriptionCommandsQueries(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 =>

View File

@ -0,0 +1,76 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscriptionCommand
{
private readonly IPaymentService _paymentService;
private readonly IOrganizationService _organizationService;
public AddSecretsManagerSubscriptionCommand(
IPaymentService paymentService,
IOrganizationService organizationService)
{
_paymentService = paymentService;
_organizationService = organizationService;
}
public async Task SignUpAsync(Organization organization, int additionalSmSeats,
int additionalServiceAccounts)
{
ValidateOrganization(organization);
var plan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts);
_organizationService.ValidateSecretsManagerPlan(plan, signup);
if (plan.Product != ProductType.Free)
{
await _paymentService.AddSecretsManagerToSubscription(organization, plan, additionalSmSeats, additionalServiceAccounts);
}
organization.SmSeats = plan.BaseSeats + additionalSmSeats;
organization.SmServiceAccounts = plan.BaseServiceAccount.GetValueOrDefault() + additionalServiceAccounts;
organization.UseSecretsManager = true;
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
// TODO: call ReferenceEventService - see AC-1481
}
private static OrganizationUpgrade SetOrganizationUpgrade(Organization organization, int additionalSeats,
int additionalServiceAccounts)
{
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = additionalSeats,
AdditionalServiceAccounts = additionalServiceAccounts,
AdditionalSeats = organization.Seats.GetValueOrDefault()
};
return signup;
}
private static void ValidateOrganization(Organization organization)
{
if (organization == null)
{
throw new NotFoundException();
}
var plan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.Product != ProductType.Free)
{
throw new BadRequestException("No payment method found.");
}
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId) && plan.Product != ProductType.Free)
{
throw new BadRequestException("No subscription found.");
}
}
}

View File

@ -0,0 +1,11 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
/// <summary>
/// This is only for adding SM to an existing organization
/// </summary>
public interface IAddSecretsManagerSubscriptionCommand
{
Task SignUpAsync(Organization organization, int additionalSmSeats, int additionalServiceAccounts);
}

View File

@ -1,8 +1,11 @@
using Bit.Core.Models.Business; using Bit.Core.Entities;
using Bit.Core.Models.Business;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
public interface IUpdateSecretsManagerSubscriptionCommand public interface IUpdateSecretsManagerSubscriptionCommand
{ {
Task UpdateSecretsManagerSubscription(SecretsManagerSubscriptionUpdate update); Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update);
Task AdjustServiceAccountsAsync(Organization organization, int smServiceAccountsAdjustment);
Task ValidateUpdate(SecretsManagerSubscriptionUpdate update);
} }

View File

@ -7,7 +7,7 @@ public static class OrganizationSubscriptionServiceCollectionExtensions
{ {
public static void AddOrganizationSubscriptionServices(this IServiceCollection services) public static void AddOrganizationSubscriptionServices(this IServiceCollection services)
{ {
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
services.AddScoped<IUpgradeOrganizationPlanCommand, UpgradeOrganizationPlanCommand>(); services.AddScoped<IUpgradeOrganizationPlanCommand, UpgradeOrganizationPlanCommand>();
services.AddScoped<IAddSecretsManagerSubscriptionCommand, AddSecretsManagerSubscriptionCommand>();
} }
} }

View File

@ -1,5 +1,4 @@
#nullable enable using Bit.Core.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
@ -8,6 +7,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -15,86 +15,57 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand
{ {
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IOrganizationService _organizationService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly ILogger<UpdateSecretsManagerSubscriptionCommand> _logger; private readonly ILogger<UpdateSecretsManagerSubscriptionCommand> _logger;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationRepository _organizationRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IEventService _eventService;
public UpdateSecretsManagerSubscriptionCommand( public UpdateSecretsManagerSubscriptionCommand(
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IPaymentService paymentService, IPaymentService paymentService,
IMailService mailService, IMailService mailService,
ILogger<UpdateSecretsManagerSubscriptionCommand> logger, ILogger<UpdateSecretsManagerSubscriptionCommand> logger,
IServiceAccountRepository serviceAccountRepository) IServiceAccountRepository serviceAccountRepository,
IGlobalSettings globalSettings,
IOrganizationRepository organizationRepository,
IApplicationCacheService applicationCacheService,
IEventService eventService)
{ {
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_paymentService = paymentService; _paymentService = paymentService;
_organizationService = organizationService;
_mailService = mailService; _mailService = mailService;
_logger = logger; _logger = logger;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
_globalSettings = globalSettings;
_organizationRepository = organizationRepository;
_applicationCacheService = applicationCacheService;
_eventService = eventService;
} }
public async Task UpdateSecretsManagerSubscription(SecretsManagerSubscriptionUpdate update) public async Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update)
{ {
var organization = await _organizationRepository.GetByIdAsync(update.OrganizationId); await ValidateUpdate(update);
ValidateOrganization(organization); await FinalizeSubscriptionAdjustmentAsync(update.Organization, update.Plan, update);
var plan = GetPlanForOrganization(organization); await SendEmailIfAutoscaleLimitReached(update.Organization);
if (update.SmSeatsChanged)
{
await ValidateSmSeatsUpdateAsync(organization, update, plan);
}
if (update.SmServiceAccountsChanged)
{
await ValidateSmServiceAccountsUpdateAsync(organization, update, plan);
}
if (update.MaxAutoscaleSmSeatsChanged)
{
ValidateMaxAutoscaleSmSeatsUpdateAsync(organization, update.MaxAutoscaleSmSeats, plan);
}
if (update.MaxAutoscaleSmServiceAccountsChanged)
{
ValidateMaxAutoscaleSmServiceAccountUpdate(organization, update.MaxAutoscaleSmServiceAccounts, plan);
}
await FinalizeSubscriptionAdjustmentAsync(organization, plan, update);
await SendEmailIfAutoscaleLimitReached(organization);
} }
private Plan GetPlanForOrganization(Organization organization) public async Task AdjustServiceAccountsAsync(Organization organization, int smServiceAccountsAdjustment)
{ {
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(
if (plan == null) organization, seatAdjustment: 0, maxAutoscaleSeats: organization?.MaxAutoscaleSmSeats,
serviceAccountAdjustment: smServiceAccountsAdjustment, maxAutoscaleServiceAccounts: organization?.MaxAutoscaleSmServiceAccounts)
{ {
throw new BadRequestException("Existing plan not found."); Autoscaling = true
} };
return plan;
}
private static void ValidateOrganization(Organization organization) await UpdateSubscriptionAsync(update);
{
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, private async Task FinalizeSubscriptionAdjustmentAsync(Organization organization,
@ -122,13 +93,13 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts; organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;
} }
await _organizationService.ReplaceAndUpdateCacheAsync(organization); await ReplaceAndUpdateCacheAsync(organization);
} }
private async Task ProcessChargesAndRaiseEventsForAdjustSeatsAsync(Organization organization, Plan plan, private async Task ProcessChargesAndRaiseEventsForAdjustSeatsAsync(Organization organization, Plan plan,
SecretsManagerSubscriptionUpdate update) SecretsManagerSubscriptionUpdate update)
{ {
await _paymentService.AdjustSeatsAsync(organization, plan, update.SmSeatsExcludingBase); await _paymentService.AdjustSeatsAsync(organization, plan, update.SmSeatsExcludingBase, update.ProrationDate);
// TODO: call ReferenceEventService - see AC-1481 // TODO: call ReferenceEventService - see AC-1481
} }
@ -137,7 +108,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
SecretsManagerSubscriptionUpdate update) SecretsManagerSubscriptionUpdate update)
{ {
await _paymentService.AdjustServiceAccountsAsync(organization, plan, await _paymentService.AdjustServiceAccountsAsync(organization, plan,
update.SmServiceAccountsExcludingBase); update.SmServiceAccountsExcludingBase, update.ProrationDate);
// TODO: call ReferenceEventService - see AC-1481 // TODO: call ReferenceEventService - see AC-1481
} }
@ -170,7 +141,6 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
{ {
_logger.LogError(e, $"Error encountered notifying organization owners of Seats limit reached."); _logger.LogError(e, $"Error encountered notifying organization owners of Seats limit reached.");
} }
} }
private async Task SendServiceAccountLimitEmailAsync(Organization organization, int MaxAutoscaleValue) private async Task SendServiceAccountLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
@ -191,16 +161,59 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
} }
private async Task ValidateSmSeatsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan) public async Task ValidateUpdate(SecretsManagerSubscriptionUpdate update)
{ {
if (organization.SmSeats == null) if (_globalSettings.SelfHosted)
{ {
throw new BadRequestException("Organization has no Secrets Manager seat limit, no need to adjust seats"); var message = update.Autoscaling
? "Cannot autoscale on a self-hosted instance."
: "Cannot update subscription on a self-hosted instance.";
throw new BadRequestException(message);
} }
if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats > update.MaxAutoscaleSmSeats.Value) var organization = update.Organization;
ValidateOrganization(organization);
var plan = GetPlanForOrganization(organization);
if (update.SmSeatsChanged)
{ {
throw new BadRequestException("Cannot set max seat autoscaling below seat count."); await ValidateSmSeatsUpdateAsync(organization, update, plan);
}
if (update.SmServiceAccountsChanged)
{
await ValidateSmServiceAccountsUpdateAsync(organization, update, plan);
}
if (update.MaxAutoscaleSmSeatsChanged)
{
ValidateMaxAutoscaleSmSeatsUpdateAsync(organization, update.MaxAutoscaleSmSeats, plan);
}
if (update.MaxAutoscaleSmServiceAccountsChanged)
{
ValidateMaxAutoscaleSmServiceAccountUpdate(organization, update.MaxAutoscaleSmServiceAccounts, plan);
}
}
private 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.");
}
var plan = GetPlanForOrganization(organization);
if (plan.Product == ProductType.Free)
{
// No need to check the organization is set up with Stripe
return;
} }
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
@ -212,32 +225,65 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
{ {
throw new BadRequestException("No subscription found."); throw new BadRequestException("No subscription found.");
} }
}
if (!plan.HasAdditionalSeatsOption) private Plan GetPlanForOrganization(Organization organization)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
{ {
throw new BadRequestException("Plan does not allow additional Secrets Manager seats."); throw new BadRequestException("Existing plan not found.");
}
return plan;
}
private async Task ValidateSmSeatsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
{
// Check if the organization has unlimited seats
if (organization.SmSeats == null)
{
throw new BadRequestException("Organization has no Secrets Manager seat limit, no need to adjust seats");
} }
if (plan.BaseSeats > update.SmSeats) if (update.Autoscaling && update.SmSeats.Value < organization.SmSeats.Value)
{
throw new BadRequestException("Cannot use autoscaling to subtract seats.");
}
// Check plan maximum seats
if (!plan.HasAdditionalSeatsOption ||
(plan.MaxAdditionalSeats.HasValue && update.SmSeatsExcludingBase > plan.MaxAdditionalSeats.Value))
{
var planMaxSeats = plan.BaseSeats + plan.MaxAdditionalSeats.GetValueOrDefault();
throw new BadRequestException($"You have reached the maximum number of Secrets Manager seats ({planMaxSeats}) for this plan.");
}
// Check autoscale maximum seats
if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats.Value > update.MaxAutoscaleSmSeats.Value)
{
var message = update.Autoscaling
? "Secrets Manager seat limit has been reached."
: "Cannot set max seat autoscaling below seat count.";
throw new BadRequestException(message);
}
// Check minimum seats included with plan
if (plan.BaseSeats > update.SmSeats.Value)
{ {
throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} Secrets Manager seats."); throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} Secrets Manager seats.");
} }
if (update.SmSeats <= 0) // Check minimum seats required by business logic
if (update.SmSeats.Value <= 0)
{ {
throw new BadRequestException("You must have at least 1 Secrets Manager seat."); throw new BadRequestException("You must have at least 1 Secrets Manager seat.");
} }
if (plan.MaxAdditionalSeats.HasValue && update.SmSeatsExcludingBase > plan.MaxAdditionalSeats.Value) // Check minimum seats currently in use by the organization
{ if (organization.SmSeats.Value > update.SmSeats.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); var currentSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
if (currentSeats > update.SmSeats) if (currentSeats > update.SmSeats.Value)
{ {
throw new BadRequestException($"Your organization currently has {currentSeats} Secrets Manager seats. " + 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."); $"Your plan only allows {update.SmSeats} Secrets Manager seats. Remove some Secrets Manager users.");
@ -247,48 +293,50 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
private async Task ValidateSmServiceAccountsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan) private async Task ValidateSmServiceAccountsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
{ {
// Check if the organization has unlimited service accounts
if (organization.SmServiceAccounts == null) if (organization.SmServiceAccounts == null)
{ {
throw new BadRequestException("Organization has no Service Accounts limit, no need to adjust Service Accounts"); throw new BadRequestException("Organization has no Service Accounts limit, no need to adjust Service Accounts");
} }
if (update.MaxAutoscaleSmServiceAccounts.HasValue && update.SmServiceAccounts > update.MaxAutoscaleSmServiceAccounts.Value) if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
{ {
throw new BadRequestException("Cannot set max Service Accounts autoscaling below Service Accounts count."); throw new BadRequestException("Cannot use autoscaling to subtract service accounts.");
} }
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) // Check plan maximum service accounts
if (!plan.HasAdditionalServiceAccountOption ||
(plan.MaxAdditionalServiceAccount.HasValue && update.SmServiceAccountsExcludingBase > plan.MaxAdditionalServiceAccount.Value))
{ {
throw new BadRequestException("No payment method found."); var planMaxServiceAccounts = plan.BaseServiceAccount.GetValueOrDefault() +
plan.MaxAdditionalServiceAccount.GetValueOrDefault();
throw new BadRequestException($"You have reached the maximum number of service accounts ({planMaxServiceAccounts}) for this plan.");
} }
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) // Check autoscale maximum service accounts
if (update.MaxAutoscaleSmServiceAccounts.HasValue &&
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
{ {
throw new BadRequestException("No subscription found."); var message = update.Autoscaling
? "Secrets Manager service account limit has been reached."
: "Cannot set max service accounts autoscaling below service account amount.";
throw new BadRequestException(message);
} }
if (!plan.HasAdditionalServiceAccountOption) // Check minimum service accounts included with plan
{ if (plan.BaseServiceAccount.HasValue && plan.BaseServiceAccount.Value > update.SmServiceAccounts.Value)
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."); throw new BadRequestException($"Plan has a minimum of {plan.BaseServiceAccount} Service Accounts.");
} }
if (update.SmServiceAccounts <= 0) // Check minimum service accounts required by business logic
if (update.SmServiceAccounts.Value <= 0)
{ {
throw new BadRequestException("You must have at least 1 Service Account."); throw new BadRequestException("You must have at least 1 Service Account.");
} }
if (plan.MaxAdditionalServiceAccount.HasValue && update.SmServiceAccountsExcludingBase > plan.MaxAdditionalServiceAccount.Value) // Check minimum service accounts currently in use by the organization
{ if (!organization.SmServiceAccounts.HasValue || organization.SmServiceAccounts.Value > update.SmServiceAccounts.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); var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > update.SmServiceAccounts) if (currentServiceAccounts > update.SmServiceAccounts)
@ -353,4 +401,17 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
"Reduce your max autoscale count.")); "Reduce your max autoscale count."));
} }
} }
// TODO: This is a temporary duplication of OrganizationService.ReplaceAndUpdateCache to avoid a circular dependency.
// TODO: This should no longer be necessary when user-related methods are extracted from OrganizationService: see PM-1880
private async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
{
await _organizationRepository.ReplaceAsync(org);
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
if (orgEvent.HasValue)
{
await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);
}
}
} }

View File

@ -267,10 +267,15 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
organization.PublicKey = upgrade.PublicKey; organization.PublicKey = upgrade.PublicKey;
organization.PrivateKey = upgrade.PrivateKey; organization.PrivateKey = upgrade.PrivateKey;
organization.UsePasswordManager = true; organization.UsePasswordManager = true;
organization.SmSeats = (short)(newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault());
organization.SmServiceAccounts = newSecretsManagerPlan.BaseServiceAccount + upgrade.AdditionalServiceAccounts.GetValueOrDefault();
organization.UseSecretsManager = upgrade.UseSecretsManager; organization.UseSecretsManager = upgrade.UseSecretsManager;
if (upgrade.UseSecretsManager)
{
organization.SmSeats = newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault();
organization.SmServiceAccounts = newSecretsManagerPlan.BaseServiceAccount.GetValueOrDefault() +
upgrade.AdditionalServiceAccounts.GetValueOrDefault();
}
await _organizationService.ReplaceAndUpdateCacheAsync(organization); await _organizationService.ReplaceAndUpdateCacheAsync(organization);
if (success) if (success)

View File

@ -0,0 +1,54 @@
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers;
public class CountNewSmSeatsRequiredQuery : ICountNewSmSeatsRequiredQuery
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository;
public CountNewSmSeatsRequiredQuery(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository)
{
_organizationUserRepository = organizationUserRepository;
_organizationRepository = organizationRepository;
}
public async Task<int> CountNewSmSeatsRequiredAsync(Guid organizationId, int usersToAdd)
{
if (usersToAdd == 0)
{
return 0;
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (!organization.UseSecretsManager)
{
throw new BadRequestException("Organization does not use Secrets Manager");
}
if (!organization.SmSeats.HasValue || organization.SecretsManagerBeta)
{
return 0;
}
var occupiedSmSeats =
await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
var availableSmSeats = organization.SmSeats.Value - occupiedSmSeats;
if (availableSmSeats >= usersToAdd)
{
return 0;
}
return usersToAdd - availableSmSeats;
}
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface ICountNewSmSeatsRequiredQuery
{
public Task<int> CountNewSmSeatsRequiredAsync(Guid organizationId, int usersToAdd);
}

View File

@ -1,43 +0,0 @@
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
namespace Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager;
public class EnableAccessSecretsManagerCommand : IEnableAccessSecretsManagerCommand
{
private readonly IOrganizationUserRepository _organizationUserRepository;
public EnableAccessSecretsManagerCommand(IOrganizationUserRepository organizationUserRepository)
{
_organizationUserRepository = organizationUserRepository;
}
public async Task<List<(OrganizationUser organizationUser, string error)>> EnableUsersAsync(
IEnumerable<OrganizationUser> organizationUsers)
{
var results = new List<(OrganizationUser organizationUser, string error)>();
var usersToEnable = new List<OrganizationUser>();
foreach (var orgUser in organizationUsers)
{
if (orgUser.AccessSecretsManager)
{
results.Add((orgUser, "User already has access to Secrets Manager"));
}
else
{
orgUser.AccessSecretsManager = true;
usersToEnable.Add(orgUser);
results.Add((orgUser, ""));
}
}
if (usersToEnable.Any())
{
await _organizationUserRepository.ReplaceManyAsync(usersToEnable);
}
return results;
}
}

View File

@ -1,9 +0,0 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
public interface IEnableAccessSecretsManagerCommand
{
Task<List<(OrganizationUser organizationUser, string error)>> EnableUsersAsync(
IEnumerable<OrganizationUser> organizationUsers);
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
public interface ICountNewServiceAccountSlotsRequiredQuery
{
Task<int> CountNewServiceAccountSlotsRequiredAsync(Guid organizationId, int serviceAccountsToAdd);
}

View File

@ -16,4 +16,5 @@ public interface IProjectRepository
Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects); Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects);
Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType); Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType);
Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId); Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId);
Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId);
} }

View File

@ -21,4 +21,5 @@ public interface ISecretRepository
Task UpdateRevisionDates(IEnumerable<Guid> ids); Task UpdateRevisionDates(IEnumerable<Guid> ids);
Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType); Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays); Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);
} }

View File

@ -0,0 +1,65 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Core.SecretsManager.Repositories.Noop;
public class NoopProjectRepository : IProjectRepository
{
public Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<ProjectPermissionDetails>);
}
public Task<IEnumerable<Project>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<Project>);
}
public Task<IEnumerable<Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)
{
return Task.FromResult(null as IEnumerable<Project>);
}
public Task<Project> GetByIdAsync(Guid id)
{
return Task.FromResult(null as Project);
}
public Task<Project> CreateAsync(Project project)
{
return Task.FromResult(null as Project);
}
public Task ReplaceAsync(Project project)
{
return Task.FromResult(0);
}
public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects)
{
return Task.FromResult(null as IEnumerable<Project>);
}
public Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType)
{
return Task.FromResult((false, false));
}
public Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId)
{
return Task.FromResult(false);
}
public Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId)
{
return Task.FromResult(0);
}
}

View File

@ -0,0 +1,91 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Core.SecretsManager.Repositories.Noop;
public class NoopSecretRepository : ISecretRepository
{
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId,
IEnumerable<Guid> ids)
{
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids)
{
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<Secret> GetByIdAsync(Guid id)
{
return Task.FromResult(null as Secret);
}
public Task<Secret> CreateAsync(Secret secret)
{
return Task.FromResult(null as Secret);
}
public Task<Secret> UpdateAsync(Secret secret)
{
return Task.FromResult(null as Secret);
}
public Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets)
{
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task UpdateRevisionDates(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
{
return Task.FromResult((false, false));
}
public Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays)
{
return Task.FromResult(0);
}
public Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId)
{
return Task.FromResult(0);
}
}

View File

@ -37,4 +37,6 @@ public interface IPaymentService
Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate); Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate);
Task UpdateTaxRateAsync(TaxRate taxRate); Task UpdateTaxRateAsync(TaxRate taxRate);
Task ArchiveTaxRateAsync(TaxRate taxRate); Task ArchiveTaxRateAsync(TaxRate taxRate);
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
int additionalServiceAccount, DateTime? prorationDate = null);
} }

View File

@ -11,6 +11,8 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
@ -50,6 +52,8 @@ public class OrganizationService : IOrganizationService
private readonly ILogger<OrganizationService> _logger; private readonly ILogger<OrganizationService> _logger;
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
public OrganizationService( public OrganizationService(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -76,7 +80,9 @@ public class OrganizationService : IOrganizationService
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<OrganizationService> logger, ILogger<OrganizationService> logger,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderUserRepository providerUserRepository) IProviderUserRepository providerUserRepository,
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -103,6 +109,8 @@ public class OrganizationService : IOrganizationService
_logger = logger; _logger = logger;
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
} }
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -446,11 +454,16 @@ public class OrganizationService : IOrganizationService
RevisionDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created, Status = OrganizationStatusType.Created,
UsePasswordManager = true, UsePasswordManager = true,
SmSeats = (short)(secretsManagerPlan.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault()), UseSecretsManager = signup.UseSecretsManager,
SmServiceAccounts = secretsManagerPlan.BaseServiceAccount + signup.AdditionalServiceAccounts.GetValueOrDefault(),
UseSecretsManager = signup.UseSecretsManager
}; };
if (signup.UseSecretsManager)
{
organization.SmSeats = secretsManagerPlan.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
organization.SmServiceAccounts = secretsManagerPlan.BaseServiceAccount.GetValueOrDefault() +
signup.AdditionalServiceAccounts.GetValueOrDefault();
}
if (passwordManagerPlan.Type == PlanType.Free && !provider) if (passwordManagerPlan.Type == PlanType.Free && !provider)
{ {
var adminCount = var adminCount =
@ -816,9 +829,12 @@ public class OrganizationService : IOrganizationService
throw new NotFoundException(); throw new NotFoundException();
} }
var newSeatsRequired = 0;
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync( var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase); organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
// Seat autoscaling
var initialSmSeatCount = organization.SmSeats;
var newSeatsRequired = 0;
if (organization.Seats.HasValue) if (organization.Seats.HasValue)
{ {
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
@ -835,6 +851,21 @@ public class OrganizationService : IOrganizationService
} }
} }
// Secrets Manager seat autoscaling
SecretsManagerSubscriptionUpdate smSubscriptionUpdate = null;
var inviteWithSmAccessCount = invites
.Where(i => i.invite.AccessSecretsManager)
.SelectMany(i => i.invite.Emails)
.Count(email => !existingEmails.Contains(email));
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
if (additionalSmSeatsRequired > 0)
{
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true);
smSubscriptionUpdate.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.ValidateUpdate(smSubscriptionUpdate);
}
var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner); var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner);
if (!invitedAreAllOwners && !await HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true)) if (!invitedAreAllOwners && !await HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true))
{ {
@ -928,6 +959,11 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Cannot add seats. Cannot manage organization users."); throw new BadRequestException("Cannot add seats. Cannot manage organization users.");
} }
if (additionalSmSeatsRequired > 0)
{
smSubscriptionUpdate.ProrationDate = prorationDate;
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdate);
}
await AutoAddSeatsAsync(organization, newSeatsRequired, prorationDate); await AutoAddSeatsAsync(organization, newSeatsRequired, prorationDate);
await SendInvitesAsync(orgUsers.Concat(limitedCollectionOrgUsers.Select(u => u.Item1)), organization); await SendInvitesAsync(orgUsers.Concat(limitedCollectionOrgUsers.Select(u => u.Item1)), organization);
@ -942,11 +978,24 @@ public class OrganizationService : IOrganizationService
// Revert any added users. // Revert any added users.
var invitedOrgUserIds = orgUsers.Select(u => u.Id).Concat(limitedCollectionOrgUsers.Select(u => u.Item1.Id)); var invitedOrgUserIds = orgUsers.Select(u => u.Id).Concat(limitedCollectionOrgUsers.Select(u => u.Item1.Id));
await _organizationUserRepository.DeleteManyAsync(invitedOrgUserIds); await _organizationUserRepository.DeleteManyAsync(invitedOrgUserIds);
var currentSeatCount = (await _organizationRepository.GetByIdAsync(organization.Id)).Seats; var currentOrganization = await _organizationRepository.GetByIdAsync(organization.Id);
if (initialSeatCount.HasValue && currentSeatCount.HasValue && currentSeatCount.Value != initialSeatCount.Value) // Revert autoscaling
if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue && currentOrganization.Seats.Value != initialSeatCount.Value)
{ {
await AdjustSeatsAsync(organization, initialSeatCount.Value - currentSeatCount.Value, prorationDate); await AdjustSeatsAsync(organization, initialSeatCount.Value - currentOrganization.Seats.Value, prorationDate);
}
// Revert SmSeat autoscaling
if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue &&
currentOrganization.SmSeats.Value != initialSmSeatCount.Value)
{
var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, false)
{
SmSeats = initialSmSeatCount.Value,
ProrationDate = prorationDate
};
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert);
} }
exceptions.Add(e); exceptions.Add(e);
@ -1343,6 +1392,20 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Organization must have at least one confirmed owner."); throw new BadRequestException("Organization must have at least one confirmed owner.");
} }
// Only autoscale (if required) after all validation has passed so that we know it's a valid request before
// updating Stripe
if (!originalUser.AccessSecretsManager && user.AccessSecretsManager)
{
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1);
if (additionalSmSeatsRequired > 0)
{
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}
}
if (user.AccessAll) if (user.AccessAll)
{ {
// We don't need any collections if we're flagged to have all access. // We don't need any collections if we're flagged to have all access.

View File

@ -1710,6 +1710,12 @@ public class StripePaymentService : IPaymentService
} }
} }
public async Task<string> AddSecretsManagerToSubscription(Organization org, StaticStore.Plan plan, int additionalSmSeats,
int additionalServiceAccount, DateTime? prorationDate = null)
{
return await FinalizeSubscriptionChangeAsync(org, new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), prorationDate);
}
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId) private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId)
{ {
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(

View File

@ -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.SecretsManager.Repositories;
using Bit.Core.SecretsManager.Repositories.Noop;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Identity.Utilities; using Bit.Identity.Utilities;
@ -138,6 +140,10 @@ public class Startup
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices(); services.AddCoreLocalizationServices();
// TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should
// TODO: no longer be required - see PM-1880
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))
{ {

View File

@ -332,6 +332,8 @@ public static class ServiceCollectionExtensions
{ {
services.AddScoped<IProviderService, NoopProviderService>(); services.AddScoped<IProviderService, NoopProviderService>();
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>(); services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
services.AddScoped<ISecretRepository, NoopSecretRepository>();
services.AddScoped<IProjectRepository, NoopProjectRepository>();
} }
public static void AddNoopServices(this IServiceCollection services) public static void AddNoopServices(this IServiceCollection services)

View File

@ -43,6 +43,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly ILicensingService _licensingService; private readonly ILicensingService _licensingService;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand; private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
private readonly OrganizationsController _sut; private readonly OrganizationsController _sut;
@ -69,13 +70,14 @@ public class OrganizationsControllerTests : IDisposable
_licensingService = Substitute.For<ILicensingService>(); _licensingService = Substitute.For<ILicensingService>();
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>(); _updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>(); _upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
_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, _upgradeOrganizationPlanCommand); _updateSecretsManagerSubscriptionCommand, _upgradeOrganizationPlanCommand, _addSecretsManagerSubscriptionCommand);
} }
public void Dispose() public void Dispose()

View File

@ -2,8 +2,11 @@
using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces; using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
@ -89,14 +92,51 @@ public class ServiceAccountsControllerTests
.CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>()); .CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
} }
[Theory]
[BitAutoData(0)]
public async void CreateServiceAccount_WhenAutoscalingNotRequired_DoesNotCallUpdateSubscription(
int newSlotsRequired, SutProvider<ServiceAccountsController> sutProvider,
ServiceAccountCreateRequestModel data, Organization organization)
{
ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);
await sutProvider.Sut.CreateAsync(organization.Id, data);
await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)
.CreateAsync(Arg.Is<ServiceAccount>(sa => sa.Name == data.Name), Arg.Any<Guid>());
await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().DidNotReceiveWithAnyArgs()
.AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<int>());
}
[Theory]
[BitAutoData(1)]
[BitAutoData(2)]
public async void CreateServiceAccount_WhenAutoscalingRequired_CallsUpdateSubscription(int newSlotsRequired,
SutProvider<ServiceAccountsController> sutProvider,
ServiceAccountCreateRequestModel data, Organization organization)
{
ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);
await sutProvider.Sut.CreateAsync(organization.Id, data);
await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)
.CreateAsync(Arg.Is<ServiceAccount>(sa => sa.Name == data.Name), Arg.Any<Guid>());
await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
.AdjustServiceAccountsAsync(Arg.Is(organization), Arg.Is(newSlotsRequired));
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider,
ServiceAccountCreateRequestModel data, Guid organizationId) ServiceAccountCreateRequestModel data, Guid organizationId, Organization mockOrg)
{ {
mockOrg.Id = organizationId;
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId), .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success()); Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Is(organizationId)).Returns(mockOrg);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(organizationId); var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default) sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default)
@ -365,4 +405,20 @@ public class ServiceAccountsControllerTests
Assert.Null(result.Error); Assert.Null(result.Error);
} }
} }
private static void ArrangeCreateServiceAccountAutoScalingTest(int newSlotsRequired, SutProvider<ServiceAccountsController> sutProvider,
ServiceAccountCreateRequestModel data, Organization organization)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organization.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Is(organization.Id)).Returns(organization);
sutProvider.GetDependency<ICountNewServiceAccountSlotsRequiredQuery>()
.CountNewServiceAccountSlotsRequiredAsync(organization.Id, 1)
.ReturnsForAnyArgs(newSlotsRequired);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(organization.Id);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default)
.ReturnsForAnyArgs(resultServiceAccount);
}
} }

View File

@ -0,0 +1,107 @@
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.OrganizationSubscriptions;
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 AddSecretsManagerSubscriptionCommandTests
{
[Theory]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
public async Task SignUpAsync_ReturnsSuccessAndClientSecret_WhenOrganizationAndPlanExist(PlanType planType,
SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,
int additionalServiceAccounts,
int additionalSmSeats,
Organization organization,
bool useSecretsManager)
{
organization.PlanType = planType;
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts);
sutProvider.GetDependency<IOrganizationService>().Received(1)
.ValidateSecretsManagerPlan(plan, Arg.Is<OrganizationUpgrade>(c =>
c.UseSecretsManager == useSecretsManager &&
c.AdditionalSmSeats == additionalSmSeats &&
c.AdditionalServiceAccounts == additionalServiceAccounts &&
c.AdditionalSeats == organization.Seats.GetValueOrDefault()));
await sutProvider.GetDependency<IPaymentService>().Received()
.AddSecretsManagerToSubscription(organization, plan, additionalSmSeats, additionalServiceAccounts);
// TODO: call ReferenceEventService - see AC-1481
sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(c =>
c.SmSeats == plan.BaseSeats + additionalSmSeats &&
c.SmServiceAccounts == plan.BaseServiceAccount.GetValueOrDefault() + additionalServiceAccounts &&
c.UseSecretsManager == true));
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_ThrowsNotFoundException_WhenOrganizationIsNull(
SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,
int additionalServiceAccounts,
int additionalSmSeats)
{
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.SignUpAsync(null, additionalSmSeats, additionalServiceAccounts));
await VerifyDependencyNotCalledAsync(sutProvider);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_ThrowsGatewayException_WhenGatewayCustomerIdIsNullOrWhitespace(
SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,
Organization organization,
int additionalServiceAccounts,
int additionalSmSeats)
{
organization.GatewayCustomerId = null;
organization.PlanType = PlanType.EnterpriseAnnually;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
Assert.Contains("No payment method found.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_ThrowsGatewayException_WhenGatewaySubscriptionIdIsNullOrWhitespace(
SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,
Organization organization,
int additionalServiceAccounts,
int additionalSmSeats)
{
organization.GatewaySubscriptionId = null;
organization.PlanType = PlanType.EnterpriseAnnually;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
Assert.Contains("No subscription found.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
private static async Task VerifyDependencyNotCalledAsync(SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
.AddSecretsManagerToSubscription(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>(), Arg.Any<int>());
// TODO: call ReferenceEventService - see AC-1481
await sutProvider.GetDependency<IOrganizationService>().DidNotReceive().ReplaceAndUpdateCacheAsync(Arg.Any<Organization>());
}
}

View File

@ -7,6 +7,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -20,36 +21,26 @@ public class UpdateSecretsManagerSubscriptionCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_NoOrganization_Throws( public async Task UpdateSubscriptionAsync_NoOrganization_Throws(
Guid organizationId, SecretsManagerSubscriptionUpdate secretsManagerSubscriptionUpdate,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
sutProvider.GetDependency<IOrganizationRepository>() secretsManagerSubscriptionUpdate.Organization = null;
.GetByIdAsync(organizationId)
.Returns((Organization)null);
var organizationUpdate = new SecretsManagerSubscriptionUpdate
{
OrganizationId = organizationId,
MaxAutoscaleSmSeats = null,
SmSeatsAdjustment = 0
};
var exception = await Assert.ThrowsAsync<NotFoundException>( var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate)); () => sutProvider.Sut.UpdateSubscriptionAsync(secretsManagerSubscriptionUpdate));
Assert.Contains("Organization is not found", exception.Message); Assert.Contains("Organization is not found", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_NoSecretsManagerAccess_ThrowsException( public async Task UpdateSubscriptionAsync_NoSecretsManagerAccess_ThrowsException(
Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var organization = new Organization var organization = new Organization
{ {
Id = organizationId,
SmSeats = 10, SmSeats = 10,
SmServiceAccounts = 5, SmServiceAccounts = 5,
UseSecretsManager = false, UseSecretsManager = false,
@ -57,26 +48,18 @@ public class UpdateSecretsManagerSubscriptionCommandTests
MaxAutoscaleSmServiceAccounts = 10 MaxAutoscaleSmServiceAccounts = 10
}; };
sutProvider.GetDependency<IOrganizationRepository>() var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, seatAdjustment: 0, maxAutoscaleSeats: null, serviceAccountAdjustment: 0, maxAutoscaleServiceAccounts: null);
.GetByIdAsync(organizationId)
.Returns(organization);
var organizationUpdate = new SecretsManagerSubscriptionUpdate
{
OrganizationId = organizationId,
SmSeatsAdjustment = 1,
MaxAutoscaleSmSeats = 1
};
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate)); () => sutProvider.Sut.UpdateSubscriptionAsync(secretsManagerSubscriptionUpdate));
Assert.Contains("Organization has no access to Secrets Manager.", exception.Message); Assert.Contains("Organization has no access to Secrets Manager.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_SeatsAdustmentGreaterThanMaxAutoscaleSeats_ThrowsException( public async Task UpdateSubscriptionAsync_SeatsAdjustmentGreaterThanMaxAutoscaleSeats_ThrowsException(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -89,29 +72,20 @@ public class UpdateSecretsManagerSubscriptionCommandTests
MaxAutoscaleSmSeats = 20, MaxAutoscaleSmSeats = 20,
MaxAutoscaleSmServiceAccounts = 10, MaxAutoscaleSmServiceAccounts = 10,
PlanType = PlanType.EnterpriseAnnually, PlanType = PlanType.EnterpriseAnnually,
GatewayCustomerId = "1",
GatewaySubscriptionId = "2"
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var organizationUpdate = new SecretsManagerSubscriptionUpdate(
var organizationUpdate = new SecretsManagerSubscriptionUpdate organization, seatAdjustment: 15, maxAutoscaleSeats: 10, serviceAccountAdjustment: 0, maxAutoscaleServiceAccounts: null);
{
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.UpdateSubscriptionAsync(organizationUpdate));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("Cannot set max seat autoscaling below seat count.", exception.Message); Assert.Contains("Cannot set max seat autoscaling below seat count.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_ServiceAccountsGreaterThanMaxAutoscaleSeats_ThrowsException( public async Task UpdateSubscriptionAsync_ServiceAccountsGreaterThanMaxAutoscaleSeats_ThrowsException(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -127,30 +101,17 @@ public class UpdateSecretsManagerSubscriptionCommandTests
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "9" GatewaySubscriptionId = "9"
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var organizationUpdate = new SecretsManagerSubscriptionUpdate(
var organizationUpdate = new SecretsManagerSubscriptionUpdate organization, seatAdjustment: 1, maxAutoscaleSeats: 15, serviceAccountAdjustment: 11, maxAutoscaleServiceAccounts: 10);
{
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.UpdateSubscriptionAsync(organizationUpdate));
Assert.Contains("Cannot set max service accounts autoscaling below service account amount", exception.Message);
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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_NullGatewayCustomerId_ThrowsException( public async Task UpdateSubscriptionAsync_NullGatewayCustomerId_ThrowsException(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -164,25 +125,17 @@ public class UpdateSecretsManagerSubscriptionCommandTests
MaxAutoscaleSmServiceAccounts = 15, MaxAutoscaleSmServiceAccounts = 15,
PlanType = PlanType.EnterpriseAnnually PlanType = PlanType.EnterpriseAnnually
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 1, maxAutoscaleSeats: 15, serviceAccountAdjustment: 1, maxAutoscaleServiceAccounts: 15);
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.UpdateSubscriptionAsync(organizationUpdate));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("No payment method found.", exception.Message); Assert.Contains("No payment method found.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_NullGatewaySubscriptionId_ThrowsException( public async Task UpdateSubscriptionAsync_NullGatewaySubscriptionId_ThrowsException(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -197,25 +150,17 @@ public class UpdateSecretsManagerSubscriptionCommandTests
PlanType = PlanType.EnterpriseAnnually, PlanType = PlanType.EnterpriseAnnually,
GatewayCustomerId = "1" GatewayCustomerId = "1"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 1, maxAutoscaleSeats: 15, serviceAccountAdjustment: 1, maxAutoscaleServiceAccounts: 15);
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.UpdateSubscriptionAsync(organizationUpdate));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("No subscription found.", exception.Message); Assert.Contains("No subscription found.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_OrgWithNullSmSeatOnSeatsAdjustment_ThrowsException( public async Task UpdateSubscriptionAsync_OrgWithNullSmSeatOnSeatsAdjustment_ThrowsException(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -228,21 +173,14 @@ public class UpdateSecretsManagerSubscriptionCommandTests
MaxAutoscaleSmSeats = 20, MaxAutoscaleSmSeats = 20,
MaxAutoscaleSmServiceAccounts = 15, MaxAutoscaleSmServiceAccounts = 15,
PlanType = PlanType.EnterpriseAnnually, PlanType = PlanType.EnterpriseAnnually,
GatewayCustomerId = "1" GatewayCustomerId = "1",
GatewaySubscriptionId = "2"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 1, maxAutoscaleSeats: 15, serviceAccountAdjustment: 1, maxAutoscaleServiceAccounts: 15);
OrganizationId = organizationId,
MaxAutoscaleSmSeats = 15,
SmSeatsAdjustment = 1,
MaxAutoscaleSmServiceAccounts = 15,
SmServiceAccountsAdjustment = 1
};
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate)); () => sutProvider.Sut.UpdateSubscriptionAsync(organizationUpdate));
Assert.Contains("Organization has no Secrets Manager seat limit, no need to adjust seats", exception.Message); Assert.Contains("Organization has no Secrets Manager seat limit, no need to adjust seats", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
@ -256,12 +194,11 @@ public class UpdateSecretsManagerSubscriptionCommandTests
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.TeamsMonthly2019)] [BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsAnnually2019)] [BitAutoData(PlanType.TeamsAnnually2019)]
public async Task UpdateSecretsManagerSubscription_WithNonSecretsManagerPlanType_ThrowsBadRequestException( public async Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
PlanType planType, PlanType planType,
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var organization = new Organization var organization = new Organization
{ {
Id = organizationId, Id = organizationId,
@ -270,95 +207,68 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SmServiceAccounts = 200, SmServiceAccounts = 200,
MaxAutoscaleSmSeats = 20, MaxAutoscaleSmSeats = 20,
MaxAutoscaleSmServiceAccounts = 300, MaxAutoscaleSmServiceAccounts = 300,
PlanType = planType, PlanType = PlanType.EnterpriseAnnually,
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 1, maxAutoscaleSeats: 15, serviceAccountAdjustment: 1, maxAutoscaleServiceAccounts: 300);
OrganizationId = organization.Id, organization.PlanType = planType;
MaxAutoscaleSmSeats = 15,
SmSeatsAdjustment = 1,
MaxAutoscaleSmServiceAccounts = 300,
SmServiceAccountsAdjustment = 1
};
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(organizationUpdate));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("Existing plan not found", exception.Message, StringComparison.InvariantCultureIgnoreCase); Assert.Contains("Existing plan not found", exception.Message, StringComparison.InvariantCultureIgnoreCase);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData(PlanType.Free)] [BitAutoData(PlanType.Free)]
public async Task UpdateSecretsManagerSubscription_WithHasAdditionalSeatsOptionfalse_ThrowsBadRequestException( public async Task UpdateSubscriptionAsync_WithHasAdditionalSeatsOptionFalse_ThrowsBadRequestException(
PlanType planType, PlanType planType,
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var organization = new Organization var organization = new Organization
{ {
Id = organizationId, Id = organizationId,
UseSecretsManager = true, UseSecretsManager = true,
SmSeats = 10, SmSeats = 2,
SmServiceAccounts = 200, SmServiceAccounts = 3,
MaxAutoscaleSmSeats = 20,
MaxAutoscaleSmServiceAccounts = 300,
PlanType = planType, PlanType = planType,
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 1, maxAutoscaleSeats: null, serviceAccountAdjustment: 0, maxAutoscaleServiceAccounts: null);
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.UpdateSubscriptionAsync(organizationUpdate));
Assert.Contains("You have reached the maximum number of Secrets Manager seats (2) for this plan",
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate)); exception.Message, StringComparison.InvariantCultureIgnoreCase);
Assert.Contains("Plan does not allow additional Secrets Manager seats.", exception.Message, StringComparison.InvariantCultureIgnoreCase);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData(PlanType.Free)] [BitAutoData(PlanType.Free)]
public async Task UpdateSecretsManagerSubscription_WithHasAdditionalServiceAccountOptionFalse_ThrowsBadRequestException( public async Task UpdateSubscriptionAsync_WithHasAdditionalServiceAccountOptionFalse_ThrowsBadRequestException(
PlanType planType, PlanType planType,
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var organization = new Organization var organization = new Organization
{ {
Id = organizationId, Id = organizationId,
UseSecretsManager = true, UseSecretsManager = true,
SmSeats = 10, SmSeats = 2,
SmServiceAccounts = 200, SmServiceAccounts = 3,
MaxAutoscaleSmSeats = 20,
MaxAutoscaleSmServiceAccounts = 300,
PlanType = planType, PlanType = planType,
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 0, maxAutoscaleSeats: null, serviceAccountAdjustment: 1, maxAutoscaleServiceAccounts: null);
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.UpdateSubscriptionAsync(organizationUpdate));
Assert.Contains("You have reached the maximum number of service accounts (3) for this plan",
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate)); exception.Message, StringComparison.InvariantCultureIgnoreCase);
Assert.Contains("Plan does not allow additional Service Accounts", exception.Message, StringComparison.InvariantCultureIgnoreCase);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -367,7 +277,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
[BitAutoData(PlanType.EnterpriseMonthly)] [BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly)] [BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsAnnually)]
public async Task UpdateSecretsManagerSubscription_ValidInput_Passes( public async Task UpdateSubscriptionAsync_ValidInput_Passes(
PlanType planType, PlanType planType,
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
@ -392,71 +302,89 @@ public class UpdateSecretsManagerSubscriptionCommandTests
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization,
OrganizationId = organizationId, seatAdjustment: seatAdjustment, maxAutoscaleSeats: maxAutoscaleSeats,
SmSeatsAdjustment = seatAdjustment, serviceAccountAdjustment: serviceAccountAdjustment, maxAutoscaleServiceAccounts: maxAutoScaleServiceAccounts);
SmSeats = organization.SmSeats.GetValueOrDefault() + seatAdjustment,
SmSeatsExcludingBase = (organization.SmSeats.GetValueOrDefault() + seatAdjustment) - plan.BaseSeats,
MaxAutoscaleSmSeats = maxAutoscaleSeats,
SmServiceAccountsAdjustment = serviceAccountAdjustment, await sutProvider.Sut.UpdateSubscriptionAsync(organizationUpdate);
SmServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault() + serviceAccountAdjustment,
SmServiceAccountsExcludingBase = (organization.SmServiceAccounts.GetValueOrDefault() + serviceAccountAdjustment) - (int)plan.BaseServiceAccount,
MaxAutoscaleSmServiceAccounts = maxAutoScaleServiceAccounts,
MaxAutoscaleSmSeatsChanged = maxAutoscaleSeats != organization.MaxAutoscaleSeats.GetValueOrDefault(), await sutProvider.GetDependency<IPaymentService>().Received(1)
MaxAutoscaleSmServiceAccountsChanged = 200 != organization.MaxAutoscaleSmServiceAccounts.GetValueOrDefault() .AdjustSeatsAsync(organization, plan, organizationUpdate.SmSeatsExcludingBase);
}; await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustServiceAccountsAsync(organization, plan, organizationUpdate.SmServiceAccountsExcludingBase);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization); // TODO: call ReferenceEventService - see AC-1481
await sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate); AssertUpdatedOrganization(() => Arg.Is<Organization>(org =>
org.Id == organizationId
if (organizationUpdate.SmSeatsAdjustment != 0) && org.SmSeats == organizationUpdate.SmSeats
{ && org.MaxAutoscaleSmSeats == organizationUpdate.MaxAutoscaleSmSeats
await sutProvider.GetDependency<IPaymentService>().Received(1) && org.SmServiceAccounts == (organizationServiceAccounts + serviceAccountAdjustment)
.AdjustServiceAccountsAsync(organization, plan, organizationUpdate.SmServiceAccountsExcludingBase); && org.MaxAutoscaleSmServiceAccounts == organizationUpdate.MaxAutoscaleSmServiceAccounts), sutProvider);
// TODO: call ReferenceEventService - see AC-1481
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
Arg.Is<Organization>(org => org.SmSeats == organizationUpdate.SmSeats));
}
if (organizationUpdate.SmServiceAccountsAdjustment != 0)
{
await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustSeatsAsync(organization, plan, organizationUpdate.SmSeatsExcludingBase);
// TODO: call ReferenceEventService - see AC-1481
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
Arg.Is<Organization>(org =>
org.SmServiceAccounts == (organizationServiceAccounts + organizationUpdate.SmServiceAccountsAdjustment)));
}
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).SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value, Arg.Any<IEnumerable<string>>());
await sutProvider.GetDependency<IMailService>().Received(1).SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, Arg.Any<IEnumerable<string>>()); await sutProvider.GetDependency<IMailService>().Received(1).SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, Arg.Any<IEnumerable<string>>());
} }
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
public async Task UpdateSubscriptionAsync_ValidInput_WithNullMaxAutoscale_Passes(
PlanType planType,
Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
int organizationServiceAccounts = 200;
int seatAdjustment = 5;
int? maxAutoscaleSeats = null;
int serviceAccountAdjustment = 100;
int? maxAutoScaleServiceAccounts = null;
var organization = new Organization
{
Id = organizationId,
UseSecretsManager = true,
SmSeats = 10,
MaxAutoscaleSmSeats = 20,
SmServiceAccounts = organizationServiceAccounts,
MaxAutoscaleSmServiceAccounts = 350,
PlanType = planType,
GatewayCustomerId = "1",
GatewaySubscriptionId = "2"
};
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType);
var organizationUpdate = new SecretsManagerSubscriptionUpdate(
organization,
seatAdjustment: seatAdjustment, maxAutoscaleSeats: maxAutoscaleSeats,
serviceAccountAdjustment: serviceAccountAdjustment, maxAutoscaleServiceAccounts: maxAutoScaleServiceAccounts);
await sutProvider.Sut.UpdateSubscriptionAsync(organizationUpdate);
await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustSeatsAsync(organization, plan, organizationUpdate.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustServiceAccountsAsync(organization, plan, organizationUpdate.SmServiceAccountsExcludingBase);
// TODO: call ReferenceEventService - see AC-1481
AssertUpdatedOrganization(() => Arg.Is<Organization>(org =>
org.Id == organizationId
&& org.SmSeats == organizationUpdate.SmSeats
&& org.MaxAutoscaleSmSeats == organizationUpdate.MaxAutoscaleSmSeats
&& org.SmServiceAccounts == (organizationServiceAccounts + serviceAccountAdjustment)
&& org.MaxAutoscaleSmServiceAccounts == organizationUpdate.MaxAutoscaleSmServiceAccounts), sutProvider);
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendSecretsManagerMaxSeatLimitReachedEmailAsync(default, default, default);
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(default, default, default);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_ThrowsBadRequestException_WhenMaxAutoscaleSeatsBelowSeatCount( public async Task UpdateSubscriptionAsync_ThrowsBadRequestException_WhenMaxAutoscaleSeatsBelowSeatCount(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -472,30 +400,17 @@ public class UpdateSecretsManagerSubscriptionCommandTests
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(
var update = new SecretsManagerSubscriptionUpdate organization, seatAdjustment: 1, maxAutoscaleSeats: 4, serviceAccountAdjustment: 5, maxAutoscaleServiceAccounts: 300);
{
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.UpdateSubscriptionAsync(update));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(update));
Assert.Contains("Cannot set max seat autoscaling below seat count.", exception.Message); Assert.Contains("Cannot set max seat autoscaling below seat count.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateSecretsManagerSubscription_ThrowsBadRequestException_WhenOccupiedSeatsExceedNewSeatTotal( public async Task UpdateSubscriptionAsync_ThrowsBadRequestException_WhenOccupiedSeatsExceedNewSeatTotal(
Guid organizationId, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
@ -508,25 +423,12 @@ public class UpdateSecretsManagerSubscriptionCommandTests
GatewaySubscriptionId = "2", GatewaySubscriptionId = "2",
PlanType = PlanType.EnterpriseAnnually PlanType = PlanType.EnterpriseAnnually
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(
var update = new SecretsManagerSubscriptionUpdate organization, seatAdjustment: -3, maxAutoscaleSeats: 7, serviceAccountAdjustment: 5, maxAutoscaleServiceAccounts: 300);
{
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); sutProvider.GetDependency<IOrganizationUserRepository>().GetOccupiedSmSeatCountByOrganizationIdAsync(organizationId).Returns(8);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
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); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -547,23 +449,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SmServiceAccounts = null, SmServiceAccounts = null,
PlanType = PlanType.EnterpriseAnnually PlanType = PlanType.EnterpriseAnnually
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(
var update = new SecretsManagerSubscriptionUpdate organization, seatAdjustment: 10, maxAutoscaleSeats: 21, serviceAccountAdjustment: 1, maxAutoscaleServiceAccounts: 250);
{
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.UpdateSubscriptionAsync(update));
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); Assert.Contains("Organization has no Service Accounts limit, no need to adjust Service Accounts", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -585,20 +474,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
GatewaySubscriptionId = "2", GatewaySubscriptionId = "2",
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 0, maxAutoscaleSeats: 15, serviceAccountAdjustment: 0, maxAutoscaleServiceAccounts: 200);
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.UpdateSubscriptionAsync(organizationUpdate));
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); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -623,20 +502,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 0, maxAutoscaleSeats: 1, serviceAccountAdjustment: 0, maxAutoscaleServiceAccounts: 300);
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.UpdateSubscriptionAsync(organizationUpdate));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("Your plan does not allow Secrets Manager seat autoscaling", exception.Message); Assert.Contains("Your plan does not allow Secrets Manager seat autoscaling", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
@ -662,20 +531,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var organizationUpdate = new SecretsManagerSubscriptionUpdate var organizationUpdate = new SecretsManagerSubscriptionUpdate(
{ organization, seatAdjustment: 0, maxAutoscaleSeats: null, serviceAccountAdjustment: 0, maxAutoscaleServiceAccounts: 300);
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.UpdateSubscriptionAsync(organizationUpdate));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("Your plan does not allow Service Accounts autoscaling.", exception.Message); Assert.Contains("Your plan does not allow Service Accounts autoscaling.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
@ -696,42 +555,173 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Id = organizationId, Id = organizationId,
UseSecretsManager = true, UseSecretsManager = true,
SmSeats = 10, SmSeats = 10,
SmServiceAccounts = 301,
MaxAutoscaleSmSeats = 20, MaxAutoscaleSmSeats = 20,
SmServiceAccounts = 301,
MaxAutoscaleSmServiceAccounts = 350, MaxAutoscaleSmServiceAccounts = 350,
PlanType = planType, PlanType = planType,
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "2" GatewaySubscriptionId = "2"
}; };
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == organization.PlanType); var organizationUpdate = new SecretsManagerSubscriptionUpdate(
var organizationUpdate = new SecretsManagerSubscriptionUpdate organization, seatAdjustment: 5, maxAutoscaleSeats: 15, serviceAccountAdjustment: -100, maxAutoscaleServiceAccounts: 300);
{
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; var currentServiceAccounts = 301;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<IServiceAccountRepository>() sutProvider.GetDependency<IServiceAccountRepository>()
.GetServiceAccountCountByOrganizationIdAsync(organization.Id) .GetServiceAccountCountByOrganizationIdAsync(organization.Id)
.Returns(currentServiceAccounts); .Returns(currentServiceAccounts);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(organizationUpdate));
Assert.Contains("Your organization currently has 301 Service Accounts. Your plan only allows 300 Service Accounts. Remove some Service Accounts", exception.Message); Assert.Contains("Your organization currently has 301 Service Accounts. Your plan only allows 201 Service Accounts. Remove some Service Accounts", exception.Message);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1).GetServiceAccountCountByOrganizationIdAsync(organization.Id); await sutProvider.GetDependency<IServiceAccountRepository>().Received(1).GetServiceAccountCountByOrganizationIdAsync(organization.Id);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(PlanType planType, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == planType);
var organizationSeats = plan.BaseSeats + 10;
var organizationMaxAutoscaleSeats = 20;
var organizationServiceAccounts = plan.BaseServiceAccount.GetValueOrDefault() + 10;
var organizationMaxAutoscaleServiceAccounts = 300;
var organization = new Organization
{
Id = organizationId,
PlanType = planType,
GatewayCustomerId = "1",
GatewaySubscriptionId = "2",
UseSecretsManager = true,
SmSeats = organizationSeats,
MaxAutoscaleSmSeats = organizationMaxAutoscaleSeats,
SmServiceAccounts = organizationServiceAccounts,
MaxAutoscaleSmServiceAccounts = organizationMaxAutoscaleServiceAccounts
};
var smServiceAccountsAdjustment = 10;
var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment;
var expectedSmServiceAccountsExcludingBase = expectedSmServiceAccounts - plan.BaseServiceAccount.GetValueOrDefault();
await sutProvider.Sut.AdjustServiceAccountsAsync(organization, smServiceAccountsAdjustment);
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustServiceAccountsAsync(
Arg.Is<Organization>(o => o.Id == organizationId),
plan,
expectedSmServiceAccountsExcludingBase);
// TODO: call ReferenceEventService - see AC-1481
AssertUpdatedOrganization(() => Arg.Is<Organization>(o =>
o.Id == organizationId
&& o.SmSeats == organizationSeats
&& o.MaxAutoscaleSmSeats == organizationMaxAutoscaleSeats
&& o.SmServiceAccounts == expectedSmServiceAccounts
&& o.MaxAutoscaleSmServiceAccounts == organizationMaxAutoscaleServiceAccounts), sutProvider);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task ServiceAccountAutoscaling_MaxLimitReached_ThrowsBadRequestException(
PlanType planType,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.UseSecretsManager = true;
organization.SmServiceAccounts = 9;
organization.MaxAutoscaleSmServiceAccounts = 10;
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustServiceAccounts(2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Secrets Manager service account limit has been reached.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task ServiceAccountAutoscaling_Subtracting_ThrowsBadRequestException(
PlanType planType,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.UseSecretsManager = true;
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustServiceAccounts(-2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Cannot use autoscaling to subtract service accounts.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task SmSeatAutoscaling_MaxLimitReached_ThrowsBadRequestException(
PlanType planType,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.UseSecretsManager = true;
organization.SmSeats = 9;
organization.MaxAutoscaleSmSeats = 10;
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustSeats(2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task SmSeatAutoscaling_Subtracting_ThrowsBadRequestException(
PlanType planType,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.UseSecretsManager = true;
var update = new SecretsManagerSubscriptionUpdate(organization, true);
update.AdjustSeats(-2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
[Theory]
[BitAutoData(false, "Cannot update subscription on a self-hosted instance.")]
[BitAutoData(true, "Cannot autoscale on a self-hosted instance.")]
public async Task UpdatingSubscription_WhenSelfHosted_ThrowsBadRequestException(
bool autoscaling,
string expectedError,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = PlanType.EnterpriseAnnually;
organization.UseSecretsManager = true;
var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling);
update.AdjustSeats(2);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains(expectedError, exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
private static async Task VerifyDependencyNotCalledAsync(SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) private static async Task VerifyDependencyNotCalledAsync(SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
await sutProvider.GetDependency<IPaymentService>().DidNotReceive() await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
@ -739,10 +729,17 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.GetDependency<IPaymentService>().DidNotReceive() await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
.AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>()); .AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
// TODO: call ReferenceEventService - see AC-1481 // TODO: call ReferenceEventService - see AC-1481
await sutProvider.GetDependency<IOrganizationService>().DidNotReceive()
.ReplaceAndUpdateCacheAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IMailService>().DidNotReceive() await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationMaxSeatLimitReachedEmailAsync(Arg.Any<Organization>(), Arg.Any<int>(), .SendOrganizationMaxSeatLimitReachedEmailAsync(Arg.Any<Organization>(), Arg.Any<int>(),
Arg.Any<IEnumerable<string>>()); Arg.Any<IEnumerable<string>>());
sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
sutProvider.GetDependency<IApplicationCacheService>().DidNotReceiveWithAnyArgs().UpsertOrganizationAbilityAsync(default);
}
private void AssertUpdatedOrganization(Func<Organization> organizationMatcher, SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organizationMatcher());
sutProvider.GetDependency<IApplicationCacheService>().Received(1).UpsertOrganizationAbilityAsync(organizationMatcher());
} }
} }

View File

@ -0,0 +1,110 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class CountNewSmSeatsRequiredQueryTests
{
[Theory]
[BitAutoData(2, 5, 2, 0)]
[BitAutoData(0, 5, 2, 0)]
[BitAutoData(6, 5, 2, 3)]
[BitAutoData(2, 5, 10, 7)]
public async Task CountNewSmSeatsRequiredAsync_ReturnsCorrectCount(
int usersToAdd,
int organizationSmSeats,
int currentOccupiedSmSeats,
int expectedNewSmSeatsRequired,
Organization organization,
SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)
{
organization.UseSecretsManager = true;
organization.SmSeats = organizationSmSeats;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
.Returns(currentOccupiedSmSeats);
var result = await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd);
Assert.Equal(expectedNewSmSeatsRequired, result);
}
[Theory]
[BitAutoData(0)]
[BitAutoData(5)]
public async Task CountNewSmSeatsRequiredAsync_WithNullSmSeats_ReturnsZero(
int usersToAdd,
Organization organization,
SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)
{
const int expected = 0;
organization.UseSecretsManager = true;
organization.SmSeats = null;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var result = await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd);
Assert.Equal(expected, result);
}
[Theory, BitAutoData]
public async Task CountNewSmSeatsRequiredAsync_WithNonExistentOrganizationId_ThrowsNotFound(
Guid organizationId, int usersToAdd,
SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)
{
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organizationId, usersToAdd));
}
[Theory, BitAutoData]
public async Task CountNewSmSeatsRequiredAsync_WithOrganizationUseSecretsManagerFalse_ThrowsNotFound(
Organization organization, int usersToAdd,
SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)
{
organization.UseSecretsManager = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd));
Assert.Contains("Organization does not use Secrets Manager", exception.Message);
}
[Theory, BitAutoData]
public async Task CountNewSmSeatsRequiredAsync_WithSecretsManagerBeta_ReturnsZero(
int usersToAdd,
Organization organization,
SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)
{
organization.UseSecretsManager = true;
organization.SecretsManagerBeta = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var result = await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd);
Assert.Equal(0, result);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.GetOccupiedSmSeatCountByOrganizationIdAsync(default);
}
}

View File

@ -1,81 +0,0 @@
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.SecretsManager.Commands.EnableAccessSecretsManager;
[SutProviderCustomize]
public class EnableAccessSecretsManagerCommandTests
{
[Theory]
[BitAutoData]
public async Task EnableUsers_UsersAlreadyEnabled_DoesNotCallRepository(
SutProvider<EnableAccessSecretsManagerCommand> sutProvider, ICollection<OrganizationUser> data)
{
foreach (var item in data)
{
item.AccessSecretsManager = true;
}
var result = await sutProvider.Sut.EnableUsersAsync(data);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.ReplaceManyAsync(default);
Assert.Equal(data.Count, result.Count);
Assert.Equal(data.Count,
result.Where(x => x.error == "User already has access to Secrets Manager").ToList().Count);
}
[Theory]
[BitAutoData]
public async Task EnableUsers_OneUserNotEnabled_CallsRepositoryForOne(
SutProvider<EnableAccessSecretsManagerCommand> sutProvider, ICollection<OrganizationUser> data)
{
var firstUser = new List<OrganizationUser>();
foreach (var item in data)
{
if (item == data.First())
{
item.AccessSecretsManager = false;
firstUser.Add(item);
}
else
{
item.AccessSecretsManager = true;
}
}
var result = await sutProvider.Sut.EnableUsersAsync(data);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.ReplaceManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(firstUser)));
Assert.Equal(data.Count, result.Count);
Assert.Equal(data.Count - 1,
result.Where(x => x.error == "User already has access to Secrets Manager").ToList().Count);
}
[Theory]
[BitAutoData]
public async Task EnableUsers_Success(
SutProvider<EnableAccessSecretsManagerCommand> sutProvider, ICollection<OrganizationUser> data)
{
foreach (var item in data)
{
item.AccessSecretsManager = false;
}
var result = await sutProvider.Sut.EnableUsersAsync(data);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.ReplaceManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
Assert.Equal(data.Count, result.Count);
}
}

View File

@ -14,6 +14,8 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -27,6 +29,7 @@ using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit; using Xunit;
using Organization = Bit.Core.Entities.Organization; using Organization = Bit.Core.Entities.Organization;
using OrganizationUser = Bit.Core.Entities.OrganizationUser; using OrganizationUser = Bit.Core.Entities.OrganizationUser;
@ -145,6 +148,59 @@ public class OrganizationServiceTests
referenceEvent.Users == expectedNewUsersCount)); referenceEvent.Users == expectedNewUsersCount));
} }
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
var passwordManagerPlan = StaticStore.GetPasswordManagerPlan(signup.Plan);
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
var purchaseOrganizationPlan = StaticStore.Plans.Where(x => x.Type == signup.Plan).ToList();
var result = await sutProvider.Sut.SignUpAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == passwordManagerPlan.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == null
&& o.SmServiceAccounts == null));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == passwordManagerPlan.Name &&
referenceEvent.PlanType == passwordManagerPlan.Type &&
referenceEvent.Seats == result.Item1.Seats &&
referenceEvent.Storage == result.Item1.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result);
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
Assert.IsType<Tuple<Organization, OrganizationUser>>(result);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
Arg.Is<List<Plan>>(plan => plan.Single() == passwordManagerPlan),
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault()
);
}
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseAnnually)]
@ -586,6 +642,97 @@ public class OrganizationServiceTests
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>()); await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>());
} }
[Theory, BitAutoData]
public async Task InviteUser_WithSecretsManager_Passes(Organization organization,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
[OrganizationUser(type: OrganizationUserType.Owner, status: OrganizationUserStatusType.Confirmed)] OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
{
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
// Set up some invites to grant access to SM
invites.First().invite.AccessSecretsManager = true;
var invitedSmUsers = invites.First().invite.Emails.Count();
// Assume we need to add seats for all invited SM users
sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()
.CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers);
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, invites);
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
.UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>
update.SmSeats == organization.SmSeats + invitedSmUsers &&
!update.SmServiceAccountsChanged &&
!update.MaxAutoscaleSmSeatsChanged &&
!update.MaxAutoscaleSmSeatsChanged));
}
[Theory, BitAutoData]
public async Task InviteUser_WithSecretsManager_WhenErrorIsThrown_RevertsAutoscaling(Organization organization,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
[OrganizationUser(type: OrganizationUserType.Owner, status: OrganizationUserStatusType.Confirmed)] OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
{
var initialSmSeats = organization.SmSeats;
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
// Set up some invites to grant access to SM
invites.First().invite.AccessSecretsManager = true;
var invitedSmUsers = invites.First().invite.Emails.Count();
// Assume we need to add seats for all invited SM users
sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()
.CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers);
// Mock SecretsManagerSubscriptionUpdateCommand to actually change the organization's subscription in memory
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()
.UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>())
.ReturnsForAnyArgs(Task.FromResult(0)).AndDoes(x => organization.SmSeats += invitedSmUsers);
// Throw error at the end of the try block
sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default).ThrowsForAnyArgs<BadRequestException>();
await Assert.ThrowsAsync<AggregateException>(async () => await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, invites));
// OrgUser is reverted
// Note: we don't know what their guids are so comparing length is the best we can do
var invitedEmails = invites.SelectMany(i => i.invite.Emails);
sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).DeleteManyAsync(
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == invitedEmails.Count()));
Received.InOrder(() =>
{
// Initial autoscaling
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()
.UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>
update.SmSeats == initialSmSeats + invitedSmUsers &&
!update.SmServiceAccountsChanged &&
!update.MaxAutoscaleSmSeatsChanged &&
!update.MaxAutoscaleSmSeatsChanged));
// Revert autoscaling
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()
.UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>
update.SmSeats == initialSmSeats &&
!update.SmServiceAccountsChanged &&
!update.MaxAutoscaleSmSeatsChanged &&
!update.MaxAutoscaleSmSeatsChanged));
});
}
private void InviteUserHelper_ArrangeValidPermissions(Organization organization, OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
{
savingUser.OrganizationId = organization.Id;
organization.UseCustomPermissions = true;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByOrganizationAsync(savingUser.OrganizationId, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser> { savingUser });
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SaveUser_NoUserId_Throws(OrganizationUser user, Guid? savingUserId, public async Task SaveUser_NoUserId_Throws(OrganizationUser user, Guid? savingUserId,
IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, SutProvider<OrganizationService> sutProvider) IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, SutProvider<OrganizationService> sutProvider)
@ -1534,4 +1681,154 @@ public class OrganizationServiceTests
Assert.Equal(includeProvider, result); Assert.Equal(includeProvider, result);
} }
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenInvalidPlanSelected(
PlanType planType, SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.Plans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 1,
AdditionalServiceAccounts = 10,
AdditionalSeats = 1
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("Invalid Secrets Manager plan selected.", exception.Message);
}
[Theory]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenNoSecretsManagerSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 0,
AdditionalServiceAccounts = 5,
AdditionalSeats = 2
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("You do not have any Secrets Manager seats!", exception.Message);
}
[Theory]
[BitAutoData(PlanType.Free)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = -1,
AdditionalServiceAccounts = 5
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("You can't subtract Secrets Manager seats!", exception.Message);
}
[Theory]
[BitAutoData(PlanType.Free)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenPlanDoesNotAllowAdditionalServiceAccounts(
PlanType planType,
SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 2,
AdditionalServiceAccounts = 5,
AdditionalSeats = 3
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("Plan does not allow additional Service Accounts.", exception.Message);
}
[Theory]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenMoreSeatsThanPasswordManagerSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 4,
AdditionalServiceAccounts = 5,
AdditionalSeats = 3
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats.", exception.Message);
}
[Theory]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingServiceAccounts(
PlanType planType,
SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 4,
AdditionalServiceAccounts = -5,
AdditionalSeats = 5
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
}
[Theory]
[BitAutoData(PlanType.Free)]
public void ValidateSecretsManagerPlan_ThrowsException_WhenPlanDoesNotAllowAdditionalUsers(
PlanType planType,
SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 2,
AdditionalServiceAccounts = 0,
AdditionalSeats = 5
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("Plan does not allow additional users.", exception.Message);
}
[Theory]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
public void ValidateSecretsManagerPlan_ValidPlan_NoExceptionThrown(
PlanType planType,
SutProvider<OrganizationService> sutProvider)
{
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(x => x.Type == planType);
var signup = new OrganizationUpgrade
{
UseSecretsManager = true,
AdditionalSmSeats = 2,
AdditionalServiceAccounts = 0,
AdditionalSeats = 4
};
sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup);
}
} }