mirror of
https://github.com/bitwarden/server.git
synced 2025-05-21 03:24:31 -05:00
[AC-2461] Scale provider seats on client organization deletion (#3996)
* Scaled provider seats on client organization deletion * Thomas' feedback
This commit is contained in:
parent
e6bd8779a6
commit
821f7620b6
@ -3,10 +3,12 @@ using Bit.Admin.AdminConsole.Models;
|
|||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -53,6 +55,8 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
@ -78,7 +82,9 @@ public class OrganizationsController : Controller
|
|||||||
IServiceAccountRepository serviceAccountRepository,
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||||
IRemovePaymentMethodCommand removePaymentMethodCommand)
|
IRemovePaymentMethodCommand removePaymentMethodCommand,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IScaleSeatsCommand scaleSeatsCommand)
|
||||||
{
|
{
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
@ -104,6 +110,8 @@ public class OrganizationsController : Controller
|
|||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||||
|
_featureService = featureService;
|
||||||
|
_scaleSeatsCommand = scaleSeatsCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Org_List_View)]
|
[RequirePermission(Permission.Org_List_View)]
|
||||||
@ -234,12 +242,30 @@ public class OrganizationsController : Controller
|
|||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
if (organization != null)
|
|
||||||
|
if (organization == null)
|
||||||
{
|
{
|
||||||
await _organizationRepository.DeleteAsync(organization);
|
return RedirectToAction("Index");
|
||||||
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
|
||||||
|
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
if (provider.IsBillable())
|
||||||
|
{
|
||||||
|
await _scaleSeatsCommand.ScalePasswordManagerSeats(
|
||||||
|
provider,
|
||||||
|
organization.PlanType,
|
||||||
|
-organization.Seats ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationRepository.DeleteAsync(organization);
|
||||||
|
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||||
|
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ using Bit.Core.Auth.Enums;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Queries;
|
using Bit.Core.Billing.Queries;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -69,6 +70,8 @@ public class OrganizationsController : Controller
|
|||||||
private readonly ISubscriberQueries _subscriberQueries;
|
private readonly ISubscriberQueries _subscriberQueries;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -95,7 +98,9 @@ public class OrganizationsController : Controller
|
|||||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||||
ISubscriberQueries subscriberQueries,
|
ISubscriberQueries subscriberQueries,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
|
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
IScaleSeatsCommand scaleSeatsCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -122,6 +127,8 @@ public class OrganizationsController : Controller
|
|||||||
_subscriberQueries = subscriberQueries;
|
_subscriberQueries = subscriberQueries;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||||
|
_providerRepository = providerRepository;
|
||||||
|
_scaleSeatsCommand = scaleSeatsCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -560,10 +567,23 @@ public class OrganizationsController : Controller
|
|||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
|
||||||
|
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||||
{
|
{
|
||||||
await _organizationService.DeleteAsync(organization);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
if (provider.IsBillable())
|
||||||
|
{
|
||||||
|
await _scaleSeatsCommand.ScalePasswordManagerSeats(
|
||||||
|
provider,
|
||||||
|
organization.PlanType,
|
||||||
|
-organization.Seats ?? 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _organizationService.DeleteAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/import")]
|
[HttpPost("{id}/import")]
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Extensions;
|
namespace Bit.Core.Billing.Extensions;
|
||||||
|
|
||||||
public static class BillingExtensions
|
public static class BillingExtensions
|
||||||
{
|
{
|
||||||
|
public static bool IsBillable(this Provider provider) =>
|
||||||
|
provider is
|
||||||
|
{
|
||||||
|
Type: ProviderType.Msp,
|
||||||
|
Status: ProviderStatusType.Billable
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsValidClient(this Organization organization)
|
||||||
|
=> organization is
|
||||||
|
{
|
||||||
|
Seats: not null,
|
||||||
|
Status: OrganizationStatusType.Managed,
|
||||||
|
PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly
|
||||||
|
};
|
||||||
|
|
||||||
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
||||||
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly;
|
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
using AutoFixture.Xunit2;
|
using AutoFixture.Xunit2;
|
||||||
using Bit.Api.AdminConsole.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -25,6 +28,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
using NSubstitute.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -59,6 +63,8 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly ISubscriberQueries _subscriberQueries;
|
private readonly ISubscriberQueries _subscriberQueries;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
|
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
@ -89,6 +95,8 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_subscriberQueries = Substitute.For<ISubscriberQueries>();
|
_subscriberQueries = Substitute.For<ISubscriberQueries>();
|
||||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||||
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
|
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
|
||||||
|
_providerRepository = Substitute.For<IProviderRepository>();
|
||||||
|
_scaleSeatsCommand = Substitute.For<IScaleSeatsCommand>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(
|
_sut = new OrganizationsController(
|
||||||
_organizationRepository,
|
_organizationRepository,
|
||||||
@ -115,7 +123,9 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_cancelSubscriptionCommand,
|
_cancelSubscriptionCommand,
|
||||||
_subscriberQueries,
|
_subscriberQueries,
|
||||||
_referenceEventService,
|
_referenceEventService,
|
||||||
_organizationEnableCollectionEnhancementsCommand);
|
_organizationEnableCollectionEnhancementsCommand,
|
||||||
|
_providerRepository,
|
||||||
|
_scaleSeatsCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@ -414,4 +424,39 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
await _organizationEnableCollectionEnhancementsCommand.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any<Organization>());
|
await _organizationEnableCollectionEnhancementsCommand.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any<Organization>());
|
||||||
await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncOrganizationsAsync(Arg.Any<Guid>());
|
await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncOrganizationsAsync(Arg.Any<Guid>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
User user,
|
||||||
|
Guid organizationId,
|
||||||
|
SecretVerificationRequestModel requestModel)
|
||||||
|
{
|
||||||
|
organization.Status = OrganizationStatusType.Managed;
|
||||||
|
organization.PlanType = PlanType.TeamsMonthly;
|
||||||
|
organization.Seats = 10;
|
||||||
|
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
|
_currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
|
|
||||||
|
_userService.VerifySecretAsync(user, requestModel.Secret).Returns(true);
|
||||||
|
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||||
|
|
||||||
|
_providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);
|
||||||
|
|
||||||
|
await _sut.Delete(organizationId.ToString(), requestModel);
|
||||||
|
|
||||||
|
await _scaleSeatsCommand.Received(1)
|
||||||
|
.ScalePasswordManagerSeats(provider, organization.PlanType, -organization.Seats.Value);
|
||||||
|
|
||||||
|
await _organizationService.Received(1).DeleteAsync(organization);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user