diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index e180c83a0d..244d325a55 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -376,7 +376,7 @@ namespace Bit.Api.Controllers } [HttpPost("purge")] - public async Task PostPurge([FromBody]CipherPurgeRequestModel model) + public async Task PostPurge([FromBody]CipherPurgeRequestModel model, string organizationId = null) { var user = await _userService.GetUserByPrincipalAsync(User); if(user == null) @@ -391,7 +391,19 @@ namespace Bit.Api.Controllers throw new BadRequestException(ModelState); } - await _cipherRepository.DeleteByUserIdAsync(user.Id); + if(string.IsNullOrWhiteSpace(organizationId)) + { + await _cipherRepository.DeleteByUserIdAsync(user.Id); + } + else + { + var orgId = new Guid(organizationId); + if(!_currentContext.OrganizationAdmin(orgId)) + { + throw new NotFoundException(); + } + await _cipherService.PurgeAsync(orgId); + } } [HttpPost("{id}/attachment")] diff --git a/src/Core/Enums/EventType.cs b/src/Core/Enums/EventType.cs index d6d6080194..c40cf339de 100644 --- a/src/Core/Enums/EventType.cs +++ b/src/Core/Enums/EventType.cs @@ -32,6 +32,7 @@ OrganizationUser_Removed = 1503, OrganizationUser_UpdatedGroups = 1504, - Organization_Updated = 1600 + Organization_Updated = 1600, + Organization_PurgedVault = 1601, } } diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index bf937acf4f..d10ebede4a 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -24,6 +24,7 @@ namespace Bit.Core.Repositories Task DeleteAsync(IEnumerable ids, Guid userId); Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId); Task DeleteByUserIdAsync(Guid userId); + Task DeleteByOrganizationIdAsync(Guid organizationId); Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders); Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers); Task CreateAsync(IEnumerable ciphers, IEnumerable folders); diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index 1ece812099..f6d5677281 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -217,6 +217,17 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task DeleteByOrganizationIdAsync(Guid organizationId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_DeleteByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + } + } + public Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index d3d2285115..9d2f99ed9a 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -18,6 +18,7 @@ namespace Bit.Core.Services Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId); Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); + Task PurgeAsync(Guid organizationId); Task MoveManyAsync(IEnumerable cipherIds, Guid? destinationFolderId, Guid movingUserId); Task SaveFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder); diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index f560042666..7abbdde223 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -285,6 +285,17 @@ namespace Bit.Core.Services await _pushService.PushSyncCipherUpdateAsync(cipher, null); } + public async Task PurgeAsync(Guid organizationId) + { + var org = await _organizationRepository.GetByIdAsync(organizationId); + if(org == null) + { + throw new NotFoundException(); + } + await _cipherRepository.DeleteByOrganizationIdAsync(organizationId); + await _eventService.LogOrganizationEventAsync(org, Enums.EventType.Organization_PurgedVault); + } + public async Task MoveManyAsync(IEnumerable cipherIds, Guid? destinationFolderId, Guid movingUserId) { if(destinationFolderId.HasValue) diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 3838f26cf2..112871031c 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -231,5 +231,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_DeleteByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Cipher_DeleteByOrganizationId.sql new file mode 100644 index 0000000000..d2324a1d00 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_DeleteByOrganizationId.sql @@ -0,0 +1,49 @@ +CREATE PROCEDURE [dbo].[Cipher_DeleteByOrganizationId] + @OrganizationId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + -- Delete collection ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Cipher_DeleteByOrganizationId_CC + + DELETE TOP(@BatchSize) CC + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] C ON C.[Id] = CC.[CollectionId] + WHERE + C.[OrganizationId] = @OrganizationId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Cipher_DeleteByOrganizationId_CC + END + + -- Reset batch size + SET @BatchSize = 100 + + -- Delete ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Cipher_DeleteByOrganizationId + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [OrganizationId] = @OrganizationId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Cipher_DeleteByOrganizationId + END + + -- Cleanup organization + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId +END \ No newline at end of file diff --git a/util/Setup/DbScripts/2018-09-25_00_OrgPurge.sql b/util/Setup/DbScripts/2018-09-25_00_OrgPurge.sql new file mode 100644 index 0000000000..79acbe4ee0 --- /dev/null +++ b/util/Setup/DbScripts/2018-09-25_00_OrgPurge.sql @@ -0,0 +1,56 @@ +IF OBJECT_ID('[dbo].[Cipher_DeleteByOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_DeleteByOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[Cipher_DeleteByOrganizationId] + @OrganizationId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + -- Delete collection ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Cipher_DeleteByOrganizationId_CC + + DELETE TOP(@BatchSize) CC + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] C ON C.[Id] = CC.[CollectionId] + WHERE + C.[OrganizationId] = @OrganizationId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Cipher_DeleteByOrganizationId_CC + END + + -- Reset batch size + SET @BatchSize = 100 + + -- Delete ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Cipher_DeleteByOrganizationId + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [OrganizationId] = @OrganizationId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Cipher_DeleteByOrganizationId + END + + -- Cleanup organization + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId +END +GO diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj index 088733d22c..9533f9af01 100644 --- a/util/Setup/Setup.csproj +++ b/util/Setup/Setup.csproj @@ -12,6 +12,10 @@ + + + +