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

[PM-12489] Extract OrganizationService.DeleteAsync and OrganizationService.InitiateDeleteAsync into commands (#5279)

* Create organization deletion command with logic extracted from OrganizationService

* Add unit tests for OrganizationDeleteCommand

* Register OrganizationDeleteCommand for dependency injection

* Refactor organization deletion logic to use IOrganizationDeleteCommand and remove legacy IOrganizationService.DeleteAsync method

* Add organization deletion initiation command and refactor service usage

* Enhance organization deletion commands with detailed XML documentation

* Refactor organization command registration to include sign-up and deletion methods
This commit is contained in:
Rui Tomé 2025-01-27 10:58:08 +00:00 committed by GitHub
parent f2182c2aae
commit 3908edd08f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 337 additions and 95 deletions

View File

@ -5,6 +5,7 @@ using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
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.Enums; using Bit.Core.Billing.Enums;
@ -56,6 +57,7 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService, IOrganizationService organizationService,
@ -82,7 +84,8 @@ public class OrganizationsController : Controller
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IFeatureService featureService) IFeatureService featureService,
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -109,6 +112,7 @@ public class OrganizationsController : Controller
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_featureService = featureService; _featureService = featureService;
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
} }
[RequirePermission(Permission.Org_List_View)] [RequirePermission(Permission.Org_List_View)]
@ -319,7 +323,7 @@ public class OrganizationsController : Controller
var organization = await _organizationRepository.GetByIdAsync(id); var organization = await _organizationRepository.GetByIdAsync(id);
if (organization != null) if (organization != null)
{ {
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail); await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail);
TempData["Success"] = "The request to initiate deletion of the organization has been sent."; TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
} }
} }

View File

@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
@ -58,6 +59,7 @@ public class OrganizationsController : Controller
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
public OrganizationsController( public OrganizationsController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -78,7 +80,8 @@ public class OrganizationsController : Controller
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory, IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand) ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -99,6 +102,7 @@ public class OrganizationsController : Controller
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
_organizationDeleteCommand = organizationDeleteCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -303,7 +307,7 @@ public class OrganizationsController : Controller
} }
} }
await _organizationService.DeleteAsync(organization); await _organizationDeleteCommand.DeleteAsync(organization);
} }
[HttpPost("{id}/delete-recover-token")] [HttpPost("{id}/delete-recover-token")]
@ -333,7 +337,7 @@ public class OrganizationsController : Controller
} }
} }
await _organizationService.DeleteAsync(organization); await _organizationDeleteCommand.DeleteAsync(organization);
} }
[HttpPost("{id}/api-key")] [HttpPost("{id}/api-key")]

View File

@ -0,0 +1,13 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationDeleteCommand
{
/// <summary>
/// Permanently deletes an organization and performs necessary cleanup.
/// </summary>
/// <param name="organization">The organization to delete.</param>
/// <exception cref="BadRequestException">Thrown when the organization cannot be deleted due to configuration constraints.</exception>
Task DeleteAsync(Organization organization);
}

View File

@ -0,0 +1,14 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationInitiateDeleteCommand
{
/// <summary>
/// Initiates a secure deletion process for an organization by requesting confirmation from an organization admin.
/// </summary>
/// <param name="organization">The organization to be deleted.</param>
/// <param name="orgAdminEmail">The email address of the organization admin who will confirm the deletion.</param>
/// <exception cref="BadRequestException">Thrown when the specified admin email is invalid or lacks sufficient permissions.</exception>
Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);
}

View File

@ -0,0 +1,69 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationDeleteCommand : IOrganizationDeleteCommand
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService;
private readonly IReferenceEventService _referenceEventService;
private readonly ISsoConfigRepository _ssoConfigRepository;
public OrganizationDeleteCommand(
IApplicationCacheService applicationCacheService,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IReferenceEventService referenceEventService,
ISsoConfigRepository ssoConfigRepository)
{
_applicationCacheService = applicationCacheService;
_currentContext = currentContext;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_referenceEventService = referenceEventService;
_ssoConfigRepository = ssoConfigRepository;
}
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
try
{
var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow;
await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext));
}
catch (GatewayException) { }
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
}

View File

@ -0,0 +1,49 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationInitiateDeleteCommand : IOrganizationInitiateDeleteCommand
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserRepository _userRepository;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IMailService _mailService;
public const string OrganizationAdminNotFoundErrorMessage = "Org admin not found.";
public OrganizationInitiateDeleteCommand(
IOrganizationUserRepository organizationUserRepository,
IUserRepository userRepository,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IMailService mailService)
{
_organizationUserRepository = organizationUserRepository;
_userRepository = userRepository;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_mailService = mailService;
}
public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)
{
var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);
if (orgAdmin == null)
{
throw new BadRequestException(OrganizationAdminNotFoundErrorMessage);
}
var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);
if (orgAdminOrgUser == null || orgAdminOrgUser.Status is not OrganizationUserStatusType.Confirmed ||
(orgAdminOrgUser.Type is not OrganizationUserType.Admin and not OrganizationUserType.Owner))
{
throw new BadRequestException(OrganizationAdminNotFoundErrorMessage);
}
var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));
await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);
}
}

View File

@ -28,8 +28,6 @@ public interface IOrganizationService
/// </summary> /// </summary>
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
string ownerKey, string collectionName, string publicKey, string privateKey); string ownerKey, string collectionName, string publicKey, string privateKey);
Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);
Task DeleteAsync(Organization organization);
Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);

View File

@ -4,7 +4,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
@ -68,7 +67,6 @@ public class OrganizationService : IOrganizationService
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory; private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
@ -106,7 +104,6 @@ public class OrganizationService : IOrganizationService
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory, IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IProviderRepository providerRepository, IProviderRepository providerRepository,
IFeatureService featureService, IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
@ -139,7 +136,6 @@ public class OrganizationService : IOrganizationService
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory; _orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
@ -690,44 +686,6 @@ public class OrganizationService : IOrganizationService
} }
} }
public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)
{
var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);
if (orgAdmin == null)
{
throw new BadRequestException("Org admin not found.");
}
var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);
if (orgAdminOrgUser == null || orgAdminOrgUser.Status != OrganizationUserStatusType.Confirmed ||
(orgAdminOrgUser.Type != OrganizationUserType.Admin && orgAdminOrgUser.Type != OrganizationUserType.Owner))
{
throw new BadRequestException("Org admin not found.");
}
var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));
await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);
}
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
try
{
var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow;
await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext));
}
catch (GatewayException) { }
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate) public async Task EnableAsync(Guid organizationId, DateTime? expirationDate)
{ {
var org = await GetOrgById(organizationId); var org = await GetOrgById(organizationId);
@ -1978,15 +1936,6 @@ public class OrganizationService : IOrganizationService
return true; return true;
} }
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId) public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId)
{ {
if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value) if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value)

View File

@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfa
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
@ -52,6 +53,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationSignUpCommands(); services.AddOrganizationSignUpCommands();
services.AddOrganizationDeleteCommands();
services.AddOrganizationAuthCommands(); services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommands(); services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries(); services.AddOrganizationUserCommandsQueries();
@ -61,6 +63,12 @@ public static class OrganizationServiceCollectionExtensions
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) => private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>(); services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
private static void AddOrganizationDeleteCommands(this IServiceCollection services)
{
services.AddScoped<IOrganizationDeleteCommand, OrganizationDeleteCommand>();
services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services) private static void AddOrganizationConnectionCommands(this IServiceCollection services)
{ {
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>(); services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
@ -52,6 +53,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly OrganizationsController _sut; private readonly OrganizationsController _sut;
public OrganizationsControllerTests() public OrganizationsControllerTests()
@ -75,6 +77,7 @@ public class OrganizationsControllerTests : IDisposable
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>(); _orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>(); _removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>(); _cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
_sut = new OrganizationsController( _sut = new OrganizationsController(
_organizationRepository, _organizationRepository,
@ -95,7 +98,8 @@ public class OrganizationsControllerTests : IDisposable
_providerBillingService, _providerBillingService,
_orgDeleteTokenDataFactory, _orgDeleteTokenDataFactory,
_removeOrganizationUserCommand, _removeOrganizationUserCommand,
_cloudOrganizationSignUpCommand); _cloudOrganizationSignUpCommand,
_organizationDeleteCommand);
} }
public void Dispose() public void Dispose()
@ -226,6 +230,6 @@ public class OrganizationsControllerTests : IDisposable
await _providerBillingService.Received(1) await _providerBillingService.Received(1)
.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); .ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);
await _organizationService.Received(1).DeleteAsync(organization); await _organizationDeleteCommand.Received(1).DeleteAsync(organization);
} }
} }

View File

@ -0,0 +1,53 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
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 OrganizationDeleteCommandTests
{
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task Delete_Success(Organization organization, SutProvider<OrganizationDeleteCommand> sutProvider)
{
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
await sutProvider.Sut.DeleteAsync(organization);
await organizationRepository.Received().DeleteAsync(organization);
await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id);
}
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task Delete_Fails_KeyConnector(Organization organization, SutProvider<OrganizationDeleteCommand> sutProvider,
SsoConfig ssoConfig)
{
ssoConfig.Enabled = true;
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(organization));
Assert.Contains("You cannot delete an Organization that is using Key Connector.", exception.Message);
await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default);
await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);
}
}

View File

@ -0,0 +1,112 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
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 OrganizationInitiateDeleteCommandTests
{
[Theory]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task InitiateDeleteAsync_ValidAdminUser_Success(OrganizationUserType organizationUserType,
Organization organization, User orgAdmin, OrganizationUserOrganizationDetails orgAdminUser,
string token, SutProvider<OrganizationInitiateDeleteCommand> sutProvider)
{
orgAdminUser.Type = organizationUserType;
orgAdminUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(orgAdmin.Email)
.Returns(orgAdmin);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByUserAsync(orgAdmin.Id, organization.Id)
.Returns(orgAdminUser);
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgDeleteTokenable>>()
.Protect(Arg.Any<OrgDeleteTokenable>())
.Returns(token);
await sutProvider.Sut.InitiateDeleteAsync(organization, orgAdmin.Email);
await sutProvider.GetDependency<IMailService>().Received(1)
.SendInitiateDeleteOrganzationEmailAsync(orgAdmin.Email, organization, token);
}
[Theory, BitAutoData]
public async Task InitiateDeleteAsync_UserNotFound_ThrowsBadRequest(
Organization organization, string email, SutProvider<OrganizationInitiateDeleteCommand> sutProvider)
{
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns((User)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InitiateDeleteAsync(organization, email));
Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message);
}
[Theory]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task InitiateDeleteAsync_UserNotOrgAdmin_ThrowsBadRequest(OrganizationUserType organizationUserType,
Organization organization, User user, OrganizationUserOrganizationDetails orgUser,
SutProvider<OrganizationInitiateDeleteCommand> sutProvider)
{
orgUser.Type = organizationUserType;
orgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(user.Email)
.Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByUserAsync(user.Id, organization.Id)
.Returns(orgUser);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InitiateDeleteAsync(organization, user.Email));
Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message);
}
[Theory]
[BitAutoData(OrganizationUserStatusType.Invited)]
[BitAutoData(OrganizationUserStatusType.Revoked)]
[BitAutoData(OrganizationUserStatusType.Accepted)]
public async Task InitiateDeleteAsync_UserNotConfirmed_ThrowsBadRequest(
OrganizationUserStatusType organizationUserStatusType,
Organization organization, User user, OrganizationUserOrganizationDetails orgUser,
SutProvider<OrganizationInitiateDeleteCommand> sutProvider)
{
orgUser.Type = OrganizationUserType.Admin;
orgUser.Status = organizationUserStatusType;
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(user.Email)
.Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByUserAsync(user.Id, organization.Id)
.Returns(orgUser);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InitiateDeleteAsync(organization, user.Email));
Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message);
}
}

View File

@ -6,9 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
@ -1427,39 +1425,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
Assert.Contains("Seat limit has been reached. Contact your provider to purchase additional seats.", failureMessage); Assert.Contains("Seat limit has been reached. Contact your provider to purchase additional seats.", failureMessage);
} }
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task Delete_Success(Organization organization, SutProvider<OrganizationService> sutProvider)
{
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
await sutProvider.Sut.DeleteAsync(organization);
await organizationRepository.Received().DeleteAsync(organization);
await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id);
}
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task Delete_Fails_KeyConnector(Organization organization, SutProvider<OrganizationService> sutProvider,
SsoConfig ssoConfig)
{
ssoConfig.Enabled = true;
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(organization));
Assert.Contains("You cannot delete an Organization that is using Key Connector.", exception.Message);
await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default);
await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);
}
private void RestoreRevokeUser_Setup( private void RestoreRevokeUser_Setup(
Organization organization, Organization organization,
OrganizationUser? requestingOrganizationUser, OrganizationUser? requestingOrganizationUser,