Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,30 @@
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Vault.Repositories;

namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;

public class OrganizationDeleteCommand : IOrganizationDeleteCommand
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly IAttachmentStorageService _attachmentStorageService;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IStripePaymentService _paymentService;
private readonly ISsoConfigRepository _ssoConfigRepository;

public OrganizationDeleteCommand(
IApplicationCacheService applicationCacheService,
IAttachmentStorageService attachmentStorageService,
ICipherRepository cipherRepository,
IOrganizationRepository organizationRepository,
IStripePaymentService paymentService,
ISsoConfigRepository ssoConfigRepository)
{
_applicationCacheService = applicationCacheService;
_attachmentStorageService = attachmentStorageService;
_cipherRepository = cipherRepository;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_ssoConfigRepository = ssoConfigRepository;
Expand All @@ -43,8 +50,16 @@ public async Task DeleteAsync(Organization organization)
catch (GatewayException) { }
}

// Fetch cipher IDs before DB deletion so we can clean up attachment storage
var orgCiphers = await _cipherRepository.GetManyByOrganizationIdAsync(organization.Id);

await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);

foreach (var cipher in orgCiphers)
{
await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipher.Id);
}
}

private async Task ValidateDeleteOrganizationAsync(Organization organization)
Expand Down
16 changes: 16 additions & 0 deletions src/Core/Vault/Services/Implementations/CipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ public async Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUser
await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);
}

// Clean up attachment files from storage
foreach (var cipher in deletingCiphers)
{
await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipher.Id);
}

var events = deletingCiphers.Select(c =>
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Deleted, null));
foreach (var eventsBatch in events.Chunk(100))
Expand Down Expand Up @@ -492,7 +498,17 @@ public async Task PurgeAsync(Guid organizationId)
{
throw new NotFoundException();
}

// Fetch cipher IDs before DB deletion so we can clean up attachment storage
var orgCiphers = await _cipherRepository.GetManyByOrganizationIdAsync(organizationId);

await _cipherRepository.DeleteByOrganizationIdAsync(organizationId);

foreach (var cipher in orgCiphers)
{
await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipher.Id);
}

await _eventService.LogOrganizationEventAsync(org, EventType.Organization_PurgedVault);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
Expand All @@ -19,15 +21,25 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationDeleteCommandTests
{
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task Delete_Success(Organization organization, SutProvider<OrganizationDeleteCommand> sutProvider)
public async Task Delete_Success(Organization organization, List<Cipher> ciphers,
SutProvider<OrganizationDeleteCommand> sutProvider)
{
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
var attachmentStorageService = sutProvider.GetDependency<IAttachmentStorageService>();

sutProvider.GetDependency<ICipherRepository>()
.GetManyByOrganizationIdAsync(organization.Id)
.Returns(ciphers);

await sutProvider.Sut.DeleteAsync(organization);

await organizationRepository.Received().DeleteAsync(organization);
await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id);
foreach (var cipher in ciphers)
{
await attachmentStorageService.Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
}
}

[Theory, PaidOrganizationCustomize, BitAutoData]
Expand Down
45 changes: 45 additions & 0 deletions test/Core.Test/Vault/Services/CipherServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,12 @@ await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.DeleteByIdsOrganizationIdAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == cipherIds.Count() &&
ids.All(id => cipherIds.Contains(id))), organizationId);
foreach (var cipher in ciphers)
{
await sutProvider.GetDependency<IAttachmentStorageService>()
.Received(1)
.DeleteAttachmentsForCipherAsync(cipher.Id);
}
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
Expand Down Expand Up @@ -1840,6 +1846,12 @@ await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.DeleteAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == cipherIds.Count() &&
ids.All(id => cipherIds.Contains(id))), deletingUserId);
foreach (var cipher in ciphers)
{
await sutProvider.GetDependency<IAttachmentStorageService>()
.Received(1)
.DeleteAttachmentsForCipherAsync(cipher.Id);
}
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
Expand Down Expand Up @@ -1980,6 +1992,39 @@ await sutProvider.GetDependency<IPushNotificationService>()
.PushSyncCiphersAsync(deletingUserId);
}

[Theory]
[BitAutoData]
public async Task PurgeAsync_WithOrganizationId_DeletesCiphersAndAttachments(
Organization org, List<Cipher> ciphers, SutProvider<CipherService> sutProvider)
{
foreach (var cipher in ciphers)
{
cipher.OrganizationId = org.Id;
}

sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(org.Id)
.Returns(org);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByOrganizationIdAsync(org.Id)
.Returns(ciphers);

await sutProvider.Sut.PurgeAsync(org.Id);

await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.DeleteByOrganizationIdAsync(org.Id);
foreach (var cipher in ciphers)
{
await sutProvider.GetDependency<IAttachmentStorageService>()
.Received(1)
.DeleteAttachmentsForCipherAsync(cipher.Id);
}
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationEventAsync(org, EventType.Organization_PurgedVault);
}

[Theory]
[BitAutoData]
public async Task SoftDeleteAsync_WithPersonalCipherOwner_SoftDeletesCipher(
Expand Down
Loading