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

[PM-12491] Create Organization disable command (#5348)

* Add command interface and implementation for disabling organizations

* Register organization disable command for dependency injection

* Add unit tests for OrganizationDisableCommand

* Refactor subscription handlers to use IOrganizationDisableCommand for disabling organizations

* Remove DisableAsync method from IOrganizationService and its implementation in OrganizationService

* Remove IOrganizationService dependency from SubscriptionDeletedHandler

* Remove commented TODO for sending email to owners in OrganizationDisableCommand
This commit is contained in:
Rui Tomé 2025-02-25 14:57:30 +00:00 committed by GitHub
parent 0f10ca52b4
commit d15c1faa74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 141 additions and 22 deletions

View File

@ -1,4 +1,5 @@
using Bit.Billing.Constants;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Services;
using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
@ -6,20 +7,20 @@ namespace Bit.Billing.Services.Implementations;
public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
{
private readonly IStripeEventService _stripeEventService;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService;
private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
public SubscriptionDeletedHandler(
IStripeEventService stripeEventService,
IOrganizationService organizationService,
IUserService userService,
IStripeEventUtilityService stripeEventUtilityService)
IStripeEventUtilityService stripeEventUtilityService,
IOrganizationDisableCommand organizationDisableCommand)
{
_stripeEventService = stripeEventService;
_organizationService = organizationService;
_userService = userService;
_stripeEventUtilityService = stripeEventUtilityService;
_organizationDisableCommand = organizationDisableCommand;
}
/// <summary>
@ -44,7 +45,7 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
subscription.CancellationDetails.Comment != providerMigrationCancellationComment &&
!subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment))
{
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
}
else if (userId.HasValue)
{

View File

@ -26,6 +26,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly ISchedulerFactory _schedulerFactory;
private readonly IFeatureService _featureService;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService,
@ -38,7 +39,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
IOrganizationRepository organizationRepository,
ISchedulerFactory schedulerFactory,
IFeatureService featureService,
IOrganizationEnableCommand organizationEnableCommand)
IOrganizationEnableCommand organizationEnableCommand,
IOrganizationDisableCommand organizationDisableCommand)
{
_stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService;
@ -51,6 +53,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_schedulerFactory = schedulerFactory;
_featureService = featureService;
_organizationEnableCommand = organizationEnableCommand;
_organizationDisableCommand = organizationDisableCommand;
}
/// <summary>
@ -67,7 +70,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired
when organizationId.HasValue:
{
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
if (subscription.Status == StripeSubscriptionStatus.Unpaid &&
subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" })
{

View File

@ -0,0 +1,14 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
/// <summary>
/// Command interface for disabling organizations.
/// </summary>
public interface IOrganizationDisableCommand
{
/// <summary>
/// Disables an organization with an optional expiration date.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization to disable.</param>
/// <param name="expirationDate">Optional date when the disable status should expire.</param>
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
}

View File

@ -0,0 +1,33 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationDisableCommand : IOrganizationDisableCommand
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IApplicationCacheService _applicationCacheService;
public OrganizationDisableCommand(
IOrganizationRepository organizationRepository,
IApplicationCacheService applicationCacheService)
{
_organizationRepository = organizationRepository;
_applicationCacheService = applicationCacheService;
}
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization is { Enabled: true })
{
organization.Enabled = false;
organization.ExpirationDate = expirationDate;
organization.RevisionDate = DateTime.UtcNow;
await _organizationRepository.ReplaceAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
}
}
}

View File

@ -28,7 +28,6 @@ public interface IOrganizationService
/// </summary>
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
string ownerKey, string collectionName, string publicKey, string privateKey);
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);

View File

@ -686,20 +686,6 @@ public class OrganizationService : IOrganizationService
}
}
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
if (org != null && org.Enabled)
{
org.Enabled = false;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCacheAsync(org);
// TODO: send email to owners?
}
}
public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);

View File

@ -55,6 +55,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationSignUpCommands();
services.AddOrganizationDeleteCommands();
services.AddOrganizationEnableCommands();
services.AddOrganizationDisableCommands();
services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries();
@ -73,6 +74,9 @@ public static class OrganizationServiceCollectionExtensions
private static void AddOrganizationEnableCommands(this IServiceCollection services) =>
services.AddScoped<IOrganizationEnableCommand, OrganizationEnableCommand>();
private static void AddOrganizationDisableCommands(this IServiceCollection services) =>
services.AddScoped<IOrganizationDisableCommand, OrganizationDisableCommand>();
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
{
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();

View File

@ -0,0 +1,79 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class OrganizationDisableCommandTests
{
[Theory, BitAutoData]
public async Task DisableAsync_WhenOrganizationEnabled_DisablesSuccessfully(
Organization organization,
DateTime expirationDate,
SutProvider<OrganizationDisableCommand> sutProvider)
{
organization.Enabled = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.DisableAsync(organization.Id, expirationDate);
Assert.False(organization.Enabled);
Assert.Equal(expirationDate, organization.ExpirationDate);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.ReplaceAsync(organization);
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
}
[Theory, BitAutoData]
public async Task DisableAsync_WhenOrganizationNotFound_DoesNothing(
Guid organizationId,
DateTime expirationDate,
SutProvider<OrganizationDisableCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organizationId)
.Returns((Organization)null);
await sutProvider.Sut.DisableAsync(organizationId, expirationDate);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.ReplaceAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>()
.DidNotReceive()
.UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
}
[Theory, BitAutoData]
public async Task DisableAsync_WhenOrganizationAlreadyDisabled_DoesNothing(
Organization organization,
DateTime expirationDate,
SutProvider<OrganizationDisableCommand> sutProvider)
{
organization.Enabled = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.DisableAsync(organization.Id, expirationDate);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.ReplaceAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>()
.DidNotReceive()
.UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
}
}