From d014a597ddf719e82739b953fec2c08db1d65d8e Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 1 Apr 2020 13:00:25 -0400 Subject: [PATCH] [Soft Delete] - API updates for soft delete + retrieval --- src/Api/Controllers/CiphersController.cs | 89 +- src/Core/Enums/EventType.cs | 2 + .../Models/Api/Request/CipherRequestModel.cs | 6 + .../Api/Response/CipherResponseModel.cs | 2 + src/Core/Models/Table/Cipher.cs | 1 + src/Core/Repositories/ICipherRepository.cs | 13 +- .../SqlServer/CipherRepository.cs | 79 +- src/Core/Services/ICipherService.cs | 4 + .../Services/Implementations/CipherService.cs | 68 +- src/Sql/Sql.sqlproj | 4 + .../CipherDetails_Create.sql | 3 +- .../CipherDetails_CreateWithCollections.sql | 3 +- .../CipherDetails_ReadByUserId.sql | 6 +- ...tails_ReadWithoutOrganizationsByUserId.sql | 8 +- .../CipherDetails_Update.sql | 3 +- .../CipherOrganizationDetails_ReadById.sql | 8 +- .../dbo/Stored Procedures/Cipher_Create.sql | 3 +- .../Cipher_CreateWithCollections.sql | 3 +- .../dbo/Stored Procedures/Cipher_Delete.sql | 57 +- .../Stored Procedures/Cipher_DeleteById.sql | 31 +- .../Cipher_ReadByOrganizationId.sql | 8 +- .../dbo/Stored Procedures/Cipher_Restore.sql | 58 ++ .../Stored Procedures/Cipher_RestoreById.sql | 34 + .../Stored Procedures/Cipher_SoftDelete.sql | 58 ++ .../Cipher_SoftDeleteById.sql | 35 + .../dbo/Stored Procedures/Cipher_Update.sql | 3 +- .../Cipher_UpdateWithCollections.sql | 2 + .../2020-04-01_00_CipherSoftDelete.sql | 810 ++++++++++++++++++ 28 files changed, 1279 insertions(+), 122 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/Cipher_Restore.sql create mode 100644 src/Sql/dbo/Stored Procedures/Cipher_RestoreById.sql create mode 100644 src/Sql/dbo/Stored Procedures/Cipher_SoftDelete.sql create mode 100644 src/Sql/dbo/Stored Procedures/Cipher_SoftDeleteById.sql create mode 100644 util/Migrator/DbScripts/2020-04-01_00_CipherSoftDelete.sql diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index eb0bed8828..12a933c65c 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -94,8 +94,12 @@ namespace Bit.Api.Controllers Dictionary> collectionCiphersGroupDict = null; if (hasOrgs) { + var keyMatches = ciphers.Select(c => c.Id); // Soft deletes not filtered in tweener "Get" methods var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId); - collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + collectionCiphersGroupDict = collectionCiphers + .Where(c => keyMatches.Contains(c.CipherId)) + .GroupBy(c => c.CipherId) + .ToDictionary(s => s.Key); } var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings, @@ -206,9 +210,13 @@ namespace Bit.Api.Controllers } var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); + var cipherMatchKeys = ciphers.Select(c => c.Id); var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); - var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + var collectionCiphersGroupDict = collectionCiphers + .Where(c => cipherMatchKeys.Contains(c.CipherId)) + .GroupBy(c => c.CipherId) + .ToDictionary(s => s.Key); var responses = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, collectionCiphersGroupDict)); @@ -360,6 +368,83 @@ namespace Bit.Api.Controllers await _cipherService.DeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId); } + [HttpPut("{id}/delete")] + public async Task PutDelete(string id) + { + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + if (cipher == null) + { + throw new NotFoundException(); + } + await _cipherService.SoftDeleteAsync(cipher, userId); + } + + [HttpPut("{id}/delete-admin")] + public async Task PutDeleteAdmin(string id) + { + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id)); + if (cipher == null || !cipher.OrganizationId.HasValue || + !_currentContext.OrganizationAdmin(cipher.OrganizationId.Value)) + { + throw new NotFoundException(); + } + + await _cipherService.SoftDeleteAsync(cipher, userId, true); + } + + [HttpPut("delete")] + public async Task PutDeleteMany([FromBody]CipherBulkRestoreRequestModel model) + { + if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) + { + throw new BadRequestException("You can only restore up to 500 items at a time."); + } + + var userId = _userService.GetProperUserId(User).Value; + await _cipherService.SoftDeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId); + } + + [HttpPut("{id}/restore")] + public async Task PutRestore(string id) + { + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + if (cipher == null) + { + throw new NotFoundException(); + } + + await _cipherService.RestoreAsync(cipher, userId); + } + + [HttpPut("{id}/restore-admin")] + public async Task PutRestoreAdmin(string id) + { + var userId = _userService.GetProperUserId(User).Value; + var cipher = await _cipherRepository.GetByIdAsync(new Guid(id)); + if (cipher == null || !cipher.OrganizationId.HasValue || + !_currentContext.OrganizationAdmin(cipher.OrganizationId.Value)) + { + throw new NotFoundException(); + } + + await _cipherService.RestoreAsync(cipher, userId, true); + } + + [HttpPut("restore")] + public async Task PutRestoreMany([FromBody]CipherBulkRestoreRequestModel model) + { + if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) + { + throw new BadRequestException("You can only restore up to 500 items at a time."); + } + + var userId = _userService.GetProperUserId(User).Value; + await _cipherService.RestoreManyAsync(model.Ids.Select(i => new Guid(i)), userId); + } + [HttpPut("move")] [HttpPost("move")] public async Task MoveMany([FromBody]CipherBulkMoveRequestModel model) diff --git a/src/Core/Enums/EventType.cs b/src/Core/Enums/EventType.cs index 51e06ea962..e2e2088f72 100644 --- a/src/Core/Enums/EventType.cs +++ b/src/Core/Enums/EventType.cs @@ -26,6 +26,8 @@ Cipher_ClientCopiedHiddenField = 1112, Cipher_ClientCopiedCardCode = 1113, Cipher_ClientAutofilled = 1114, + Cipher_SoftDeleted = 1115, + Cipher_Restored = 1116, Collection_Created = 1300, Collection_Updated = 1301, diff --git a/src/Core/Models/Api/Request/CipherRequestModel.cs b/src/Core/Models/Api/Request/CipherRequestModel.cs index 6958334c2b..4f3c044901 100644 --- a/src/Core/Models/Api/Request/CipherRequestModel.cs +++ b/src/Core/Models/Api/Request/CipherRequestModel.cs @@ -205,6 +205,12 @@ namespace Bit.Core.Models.Api public IEnumerable Ids { get; set; } } + public class CipherBulkRestoreRequestModel + { + [Required] + public IEnumerable Ids { get; set; } + } + public class CipherBulkMoveRequestModel { [Required] diff --git a/src/Core/Models/Api/Response/CipherResponseModel.cs b/src/Core/Models/Api/Response/CipherResponseModel.cs index 2a632e4c4d..348fa551c6 100644 --- a/src/Core/Models/Api/Response/CipherResponseModel.cs +++ b/src/Core/Models/Api/Response/CipherResponseModel.cs @@ -60,6 +60,7 @@ namespace Bit.Core.Models.Api OrganizationId = cipher.OrganizationId?.ToString(); Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings); OrganizationUseTotp = orgUseTotp; + DeletedDate = cipher.DeletedDate; } public string Id { get; set; } @@ -77,6 +78,7 @@ namespace Bit.Core.Models.Api public IEnumerable Attachments { get; set; } public bool OrganizationUseTotp { get; set; } public DateTime RevisionDate { get; set; } + public DateTime? DeletedDate { get; set; } } public class CipherResponseModel : CipherMiniResponseModel diff --git a/src/Core/Models/Table/Cipher.cs b/src/Core/Models/Table/Cipher.cs index 68b637cdbb..af553e859a 100644 --- a/src/Core/Models/Table/Cipher.cs +++ b/src/Core/Models/Table/Cipher.cs @@ -20,6 +20,7 @@ namespace Bit.Core.Models.Table public string Attachments { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; + public DateTime? DeletedDate { get; internal set; } public void SetNewId() { diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 960ff23a4d..2a8f3d467e 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -10,10 +10,10 @@ namespace Bit.Core.Repositories public interface ICipherRepository : IRepository { Task GetByIdAsync(Guid id, Guid userId); - Task GetOrganizationDetailsByIdAsync(Guid id, bool deleted = false); + Task GetOrganizationDetailsByIdAsync(Guid id); Task GetCanEditByIdAsync(Guid userId, Guid cipherId); - Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true, bool deleted = false); - Task> GetManyByOrganizationIdAsync(Guid organizationId, bool deleted = false); + Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true); + Task> GetManyByOrganizationIdAsync(Guid organizationId); Task CreateAsync(Cipher cipher, IEnumerable collectionIds); Task CreateAsync(CipherDetails cipher); Task CreateAsync(CipherDetails cipher, IEnumerable collectionIds); @@ -23,8 +23,7 @@ namespace Bit.Core.Repositories Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); Task UpdateAttachmentAsync(CipherAttachment attachment); Task DeleteAttachmentAsync(Guid cipherId, string attachmentId); - Task DeleteAsync(Cipher obj, bool permanent = true); - Task DeleteAsync(IEnumerable ids, Guid userId, bool permanent = true); + Task DeleteAsync(IEnumerable ids, Guid userId); Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId); Task DeleteByUserIdAsync(Guid userId); Task DeleteByOrganizationIdAsync(Guid organizationId); @@ -33,5 +32,9 @@ namespace Bit.Core.Repositories Task CreateAsync(IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers); + Task SoftDeleteAsync(Cipher obj); + Task SoftDeleteAsync(IEnumerable ids, Guid userId); + Task RestoreAsync(Cipher obj); + Task RestoreAsync(IEnumerable ids, Guid userId); } } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index e5cf98649b..9cc07ba909 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -36,13 +36,13 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task GetOrganizationDetailsByIdAsync(Guid id, bool deleted = false) + public async Task GetOrganizationDetailsByIdAsync(Guid id) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[CipherOrganizationDetails_ReadById]", - new { Id = id, Deleted = deleted }, + new { Id = id }, commandType: CommandType.StoredProcedure); return results.FirstOrDefault(); @@ -62,7 +62,7 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true, bool deleted = false) + public async Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true) { string sprocName = null; if (withOrganizations) @@ -78,7 +78,7 @@ namespace Bit.Core.Repositories.SqlServer { var results = await connection.QueryAsync( sprocName, - new { UserId = userId, Deleted = deleted }, + new { UserId = userId }, commandType: CommandType.StoredProcedure); return results @@ -88,13 +88,13 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task> GetManyByOrganizationIdAsync(Guid organizationId, bool deleted = false) + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[Cipher_ReadByOrganizationId]", - new { OrganizationId = organizationId, Deleted = deleted }, + new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); return results.ToList(); @@ -156,7 +156,7 @@ namespace Bit.Core.Repositories.SqlServer public async Task UpsertAsync(CipherDetails cipher) { - if (cipher.Id.Equals(default(Guid))) + if (cipher.Id.Equals(default)) { await CreateAsync(cipher); } @@ -215,24 +215,13 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task DeleteAsync(Cipher obj, bool permanent = true) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.ExecuteAsync( - $"[{Schema}].[Cipher_DeleteById]", - new { obj.Id, Permanent = permanent }, - commandType: CommandType.StoredProcedure); - } - } - - public async Task DeleteAsync(IEnumerable ids, Guid userId, bool permanent = true) + public async Task DeleteAsync(IEnumerable ids, Guid userId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( $"[{Schema}].[Cipher_Delete]", - new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId, Permanent = permanent }, + new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, commandType: CommandType.StoredProcedure); } } @@ -448,7 +437,8 @@ namespace Bit.Core.Repositories.SqlServer [Type] = TC.[Type], [Data] = TC.[Data], [Attachments] = TC.[Attachments], - [RevisionDate] = TC.[RevisionDate] + [RevisionDate] = TC.[RevisionDate], + [DeletedDate] = TC.[DeletedDate] FROM [dbo].[Cipher] C INNER JOIN @@ -587,6 +577,50 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task SoftDeleteAsync(Cipher obj) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_SoftDeleteById]", + new { obj.Id }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task SoftDeleteAsync(IEnumerable ids, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_SoftDelete]", + new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task RestoreAsync(Cipher obj) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_RestoreById]", + new { obj.Id }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task RestoreAsync(IEnumerable ids, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_Restore]", + new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, + commandType: CommandType.StoredProcedure); + } + } + private DataTable BuildCiphersTable(SqlBulkCopy bulkCopy, IEnumerable ciphers) { var c = ciphers.FirstOrDefault(); @@ -617,6 +651,8 @@ namespace Bit.Core.Repositories.SqlServer ciphersTable.Columns.Add(creationDateColumn); var revisionDateColumn = new DataColumn(nameof(c.RevisionDate), c.RevisionDate.GetType()); ciphersTable.Columns.Add(revisionDateColumn); + var deletedDateColumn = new DataColumn(nameof(c.DeletedDate), c.DeletedDate.GetType()); + ciphersTable.Columns.Add(deletedDateColumn); foreach (DataColumn col in ciphersTable.Columns) { @@ -641,6 +677,7 @@ namespace Bit.Core.Repositories.SqlServer row[attachmentsColumn] = cipher.Attachments; row[creationDateColumn] = cipher.CreationDate; row[revisionDateColumn] = cipher.RevisionDate; + row[deletedDateColumn] = cipher.DeletedDate; ciphersTable.Rows.Add(row); } diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 37509cc1d3..0090f34e38 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -33,5 +33,9 @@ namespace Bit.Core.Services IEnumerable> folderRelationships); Task ImportCiphersAsync(List collections, List ciphers, IEnumerable> collectionRelationships, Guid importingUserId); + Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); + Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId); + Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false); + Task RestoreManyAsync(IEnumerable cipherIds, Guid restoringUserId); } } diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index fbd86c944e..0a955b1d0e 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -282,7 +282,7 @@ namespace Bit.Core.Services await _cipherRepository.DeleteAsync(cipher); await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipher.Id); - await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Deleted); + await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_Deleted); // push await _pushService.PushSyncCipherDeleteAsync(cipher); @@ -664,6 +664,72 @@ namespace Bit.Core.Services await _pushService.PushSyncVaultAsync(importingUserId); } + public async Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false) + { + if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId))) + { + throw new BadRequestException("You do not have permissions to soft delete this."); + } + + await _cipherRepository.SoftDeleteAsync(cipher); + await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted); + + // push + await _pushService.PushSyncCipherUpdateAsync(cipher, null); + } + + public async Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId) + { + var cipherIdsSet = new HashSet(cipherIds); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId); + var deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit); + + await _cipherRepository.SoftDeleteAsync(cipherIds, deletingUserId); + + var events = deletingCiphers.Select(c => + new Tuple(c, EventType.Cipher_SoftDeleted, null)); + foreach (var eventsBatch in events.Batch(100)) + { + await _eventService.LogCipherEventsAsync(eventsBatch); + } + + // push + await _pushService.PushSyncCiphersAsync(deletingUserId); + } + + public async Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false) + { + if (!orgAdmin && !(await UserCanEditAsync(cipher, restoringUserId))) + { + throw new BadRequestException("You do not have permissions to delete this."); + } + + await _cipherRepository.RestoreAsync(cipher); + await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_Restored); + + // push + await _pushService.PushSyncCipherUpdateAsync(cipher, null); + } + + public async Task RestoreManyAsync(IEnumerable cipherIds, Guid restoringUserId) + { + var cipherIdsSet = new HashSet(cipherIds); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId); + var restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit); + + await _cipherRepository.RestoreAsync(cipherIds, restoringUserId); + + var events = restoringCiphers.Select(c => + new Tuple(c, EventType.Cipher_Restored, null)); + foreach (var eventsBatch in events.Batch(100)) + { + await _eventService.LogCipherEventsAsync(eventsBatch); + } + + // push + await _pushService.PushSyncCiphersAsync(restoringUserId); + } + private async Task UserCanEditAsync(Cipher cipher, Guid userId) { if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId) diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 4f00de05aa..aa35c11596 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -261,5 +261,9 @@ + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql index 706372cab7..b0c8cfcd27 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql @@ -12,7 +12,8 @@ @FolderId UNIQUEIDENTIFIER, @Favorite BIT, @Edit BIT, -- not used - @OrganizationUseTotp BIT -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7) -- not used AS BEGIN SET NOCOUNT ON diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_CreateWithCollections.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_CreateWithCollections.sql index 3c391bbb02..92bc88079d 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_CreateWithCollections.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_CreateWithCollections.sql @@ -13,13 +13,14 @@ @Favorite BIT, @Edit BIT, -- not used @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7), -- not used @CollectionIds AS [dbo].[GuidIdArray] READONLY AS BEGIN SET NOCOUNT ON EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, - @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @OrganizationUseTotp + @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @OrganizationUseTotp, @DeletedDate DECLARE @UpdateCollectionsSuccess INT EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql index 54f69d169b..00f265b3e7 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql @@ -1,6 +1,5 @@ CREATE PROCEDURE [dbo].[CipherDetails_ReadByUserId] - @UserId UNIQUEIDENTIFIER, - @Deleted BIT + @UserId UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON @@ -9,7 +8,4 @@ BEGIN * FROM [dbo].[UserCipherDetails](@UserId) - WHERE - (@Deleted = 1 AND [DeletedDate] IS NOT NULL) - OR (@Deleted = 0 AND [DeletedDate] IS NULL) END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadWithoutOrganizationsByUserId.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadWithoutOrganizationsByUserId.sql index 3154e632c1..6f2f86498c 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadWithoutOrganizationsByUserId.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadWithoutOrganizationsByUserId.sql @@ -1,6 +1,5 @@ CREATE PROCEDURE [dbo].[CipherDetails_ReadWithoutOrganizationsByUserId] - @UserId UNIQUEIDENTIFIER, - @Deleted BIT + @UserId UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON @@ -13,9 +12,4 @@ BEGIN [dbo].[CipherDetails](@UserId) WHERE [UserId] = @UserId - AND - ( - (@Deleted = 1 AND [DeletedDate] IS NOT NULL) - OR (@Deleted = 0 AND [DeletedDate] IS NULL) - ) END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql index 0dc6009c83..fc34416bc7 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql @@ -12,7 +12,8 @@ @FolderId UNIQUEIDENTIFIER, @Favorite BIT, @Edit BIT, -- not used - @OrganizationUseTotp BIT -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(2) -- not used AS BEGIN SET NOCOUNT ON diff --git a/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadById.sql b/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadById.sql index e0b5ffb578..1f76fc783a 100644 --- a/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadById.sql +++ b/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadById.sql @@ -1,6 +1,5 @@ CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadById] - @Id UNIQUEIDENTIFIER, - @Deleted BIT + @Id UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON @@ -17,9 +16,4 @@ BEGIN [dbo].[Organization] O ON O.[Id] = C.[OrganizationId] WHERE C.[Id] = @Id - AND - ( - (@Deleted = 1 AND [DeletedDate] IS NOT NULL) - OR (@Deleted = 0 AND [DeletedDate] IS NULL) - ) END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_Create.sql b/src/Sql/dbo/Stored Procedures/Cipher_Create.sql index 2116cc2797..e047e33c64 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_Create.sql @@ -8,7 +8,8 @@ @Folders NVARCHAR(MAX), @Attachments NVARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7) -- not used AS BEGIN SET NOCOUNT ON diff --git a/src/Sql/dbo/Stored Procedures/Cipher_CreateWithCollections.sql b/src/Sql/dbo/Stored Procedures/Cipher_CreateWithCollections.sql index 260bbeb25d..b02a07c7cb 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_CreateWithCollections.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_CreateWithCollections.sql @@ -9,13 +9,14 @@ @Attachments NVARCHAR(MAX), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), -- not used @CollectionIds AS [dbo].[GuidIdArray] READONLY AS BEGIN SET NOCOUNT ON EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, - @Attachments, @CreationDate, @RevisionDate + @Attachments, @CreationDate, @RevisionDate, @DeletedDate DECLARE @UpdateCollectionsSuccess INT EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds diff --git a/src/Sql/dbo/Stored Procedures/Cipher_Delete.sql b/src/Sql/dbo/Stored Procedures/Cipher_Delete.sql index 8f0b410674..91ed28e396 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_Delete.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_Delete.sql @@ -1,7 +1,6 @@ CREATE PROCEDURE [dbo].[Cipher_Delete] @Ids AS [dbo].[GuidIdArray] READONLY, - @UserId AS UNIQUEIDENTIFIER, - @Permanent AS BIT + @UserId AS UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON @@ -27,23 +26,11 @@ BEGIN AND [Id] IN (SELECT * FROM @Ids) -- Delete ciphers - IF @Permanent = 1 - BEGIN - DELETE - FROM - [dbo].[Cipher] - WHERE - [Id] IN (SELECT [Id] FROM #Temp) - END - ELSE - BEGIN - UPDATE - [dbo].[Cipher] - SET - [DeletedDate] = SYSUTCDATETIME() - WHERE - [Id] IN (SELECT [Id] FROM #Temp) - END + DELETE + FROM + [dbo].[Cipher] + WHERE + [Id] IN (SELECT [Id] FROM #Temp) -- Cleanup orgs DECLARE @OrgId UNIQUEIDENTIFIER @@ -59,11 +46,7 @@ BEGIN OPEN [OrgCursor] FETCH NEXT FROM [OrgCursor] INTO @OrgId WHILE @@FETCH_STATUS = 0 BEGIN - -- Storage cleanup for groups only matters if we're permanently deleting - IF @Permanent = 1 - BEGIN - EXEC [dbo].[Organization_UpdateStorage] @OrgId - END + EXEC [dbo].[Organization_UpdateStorage] @OrgId EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId FETCH NEXT FROM [OrgCursor] INTO @OrgId END @@ -71,22 +54,18 @@ BEGIN DEALLOCATE [OrgCursor] -- Cleanup user - IF @Permanent = 1 - BEGIN - -- Storage cleanup for users only matters if we're permanently deleting - DECLARE @UserCiphersWithStorageCount INT - SELECT - @UserCiphersWithStorageCount = COUNT(1) - FROM - #Temp - WHERE - [UserId] IS NOT NULL - AND [Attachments] = 1 + DECLARE @UserCiphersWithStorageCount INT + SELECT + @UserCiphersWithStorageCount = COUNT(1) + FROM + #Temp + WHERE + [UserId] IS NOT NULL + AND [Attachments] = 1 - IF @UserCiphersWithStorageCount > 0 - BEGIN - EXEC [dbo].[User_UpdateStorage] @UserId - END + IF @UserCiphersWithStorageCount > 0 + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId END EXEC [dbo].[User_BumpAccountRevisionDate] @UserId diff --git a/src/Sql/dbo/Stored Procedures/Cipher_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Cipher_DeleteById.sql index be361755c4..b9b8f35558 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_DeleteById.sql @@ -1,6 +1,5 @@ CREATE PROCEDURE [dbo].[Cipher_DeleteById] - @Id UNIQUEIDENTIFIER, - @Permanent AS BIT + @Id UNIQUEIDENTIFIER WITH RECOMPILE AS BEGIN @@ -18,28 +17,16 @@ BEGIN [dbo].[Cipher] WHERE [Id] = @Id - - IF @Permanent = 1 - BEGIN - DELETE - FROM - [dbo].[Cipher] - WHERE - [Id] = @Id - END - ELSE - BEGIN - UPDATE - [dbo].[Cipher] - SET - [DeletedDate] = SYSUTCDATETIME() - WHERE - [Id] = @Id - END + + DELETE + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id IF @OrganizationId IS NOT NULL BEGIN - IF @Attachments = 1 AND @Permanent = 1 + IF @Attachments = 1 BEGIN EXEC [dbo].[Organization_UpdateStorage] @OrganizationId END @@ -47,7 +34,7 @@ BEGIN END ELSE IF @UserId IS NOT NULL BEGIN - IF @Attachments = 1 AND @Permanent = 1 + IF @Attachments = 1 BEGIN EXEC [dbo].[User_UpdateStorage] @UserId END diff --git a/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql index 8d91863fc5..9049e91e77 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql @@ -1,6 +1,5 @@ CREATE PROCEDURE [dbo].[Cipher_ReadByOrganizationId] - @OrganizationId UNIQUEIDENTIFIER, - @Deleted BIT + @OrganizationId UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON @@ -12,9 +11,4 @@ BEGIN WHERE [UserId] IS NULL AND [OrganizationId] = @OrganizationId - AND - ( - (@Deleted = 1 AND [DeletedDate] IS NOT NULL) - OR (@Deleted = 0 AND [DeletedDate] IS NULL) - ) END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_Restore.sql b/src/Sql/dbo/Stored Procedures/Cipher_Restore.sql new file mode 100644 index 0000000000..fe127e262a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_Restore.sql @@ -0,0 +1,58 @@ +CREATE PROCEDURE [dbo].[Cipher_Restore] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = NULL, + [RevisionDate] = GETUTCDATE() + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Bump orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Bump user + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_RestoreById.sql b/src/Sql/dbo/Stored Procedures/Cipher_RestoreById.sql new file mode 100644 index 0000000000..72604f9138 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_RestoreById.sql @@ -0,0 +1,34 @@ +CREATE PROCEDURE [dbo].[Cipher_RestoreById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT TOP 1 + @UserId = [UserId], + @OrganizationId = [OrganizationId] + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id + + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = NULL, + [RevisionDate] = GETUTCDATE() + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_SoftDelete.sql b/src/Sql/dbo/Stored Procedures/Cipher_SoftDelete.sql new file mode 100644 index 0000000000..035ebf8b12 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_SoftDelete.sql @@ -0,0 +1,58 @@ +CREATE PROCEDURE [dbo].[Cipher_SoftDelete] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = SYSUTCDATETIME(), + [RevisionDate] = GETUTCDATE() + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_SoftDeleteById.sql b/src/Sql/dbo/Stored Procedures/Cipher_SoftDeleteById.sql new file mode 100644 index 0000000000..bc90027929 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_SoftDeleteById.sql @@ -0,0 +1,35 @@ +CREATE PROCEDURE [dbo].[Cipher_SoftDeleteById] + @Id UNIQUEIDENTIFIER +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT TOP 1 + @UserId = [UserId], + @OrganizationId = [OrganizationId] + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id + + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = SYSUTCDATETIME(), + [RevisionDate] = GETUTCDATE() + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_Update.sql b/src/Sql/dbo/Stored Procedures/Cipher_Update.sql index 823d2a711b..df35f97278 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_Update.sql @@ -8,7 +8,8 @@ @Folders NVARCHAR(MAX), @Attachments NVARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7) -- not used AS BEGIN SET NOCOUNT ON diff --git a/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithCollections.sql b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithCollections.sql index 5c576d6338..5ce797a839 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithCollections.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithCollections.sql @@ -9,6 +9,7 @@ @Attachments NVARCHAR(MAX), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), -- not used @CollectionIds AS [dbo].[GuidIdArray] READONLY AS BEGIN @@ -35,6 +36,7 @@ BEGIN [Attachments] = @Attachments, [RevisionDate] = @RevisionDate -- No need to update CreationDate, Favorites, Folders, or Type since that data will not change + -- Do not update DeletedDate because that is a separate atomic action WHERE [Id] = @Id diff --git a/util/Migrator/DbScripts/2020-04-01_00_CipherSoftDelete.sql b/util/Migrator/DbScripts/2020-04-01_00_CipherSoftDelete.sql new file mode 100644 index 0000000000..031276ec31 --- /dev/null +++ b/util/Migrator/DbScripts/2020-04-01_00_CipherSoftDelete.sql @@ -0,0 +1,810 @@ +/** + * Revert [Cipher] deletes/gets to original versions + * - No longer needs to have the deleted flag on reads (always read all) + * - No longer needs to have the permanent flag on deletes (they just are) + * + Added ability to restore a soft-deleted cipher + * + Added DeletedDate value to updates/create sprocs + */ +IF OBJECT_ID('[dbo].[Cipher_Restore]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_Restore]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_Restore] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = NULL, + [RevisionDate] = GETUTCDATE() + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Bump orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Bump user + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END +GO + +IF OBJECT_ID('[dbo].[Cipher_RestoreById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_RestoreById]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_RestoreById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT TOP 1 + @UserId = [UserId], + @OrganizationId = [OrganizationId] + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id + + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = NULL, + [RevisionDate] = GETUTCDATE() + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +IF OBJECT_ID('[dbo].[CipherDetails_ReadByUserId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherDetails_ReadByUserId]; +END +GO +CREATE PROCEDURE [dbo].[CipherDetails_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[UserCipherDetails](@UserId) +END +GO + +IF OBJECT_ID('[dbo].[CipherDetails_ReadWithoutOrganizationsByUserId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherDetails_ReadWithoutOrganizationsByUserId]; +END +GO +CREATE PROCEDURE [dbo].[CipherDetails_ReadWithoutOrganizationsByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + *, + 1 [Edit], + 0 [OrganizationUseTotp] + FROM + [dbo].[CipherDetails](@UserId) + WHERE + [UserId] = @UserId +END +GO + +IF OBJECT_ID('[dbo].[CipherOrganizationDetails_ReadById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherOrganizationDetails_ReadById]; +END +GO +CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + CASE + WHEN O.[UseTotp] = 1 THEN 1 + ELSE 0 + END [OrganizationUseTotp] + FROM + [dbo].[CipherView] C + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = C.[OrganizationId] + WHERE + C.[Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[Cipher_Delete]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_Delete]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_Delete] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL, + [Attachments] BIT NOT NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId], + CASE WHEN [Attachments] IS NULL THEN 0 ELSE 1 END + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + DELETE + FROM + [dbo].[Cipher] + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrgId + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Cleanup user + DECLARE @UserCiphersWithStorageCount INT + SELECT + @UserCiphersWithStorageCount = COUNT(1) + FROM + #Temp + WHERE + [UserId] IS NOT NULL + AND [Attachments] = 1 + + IF @UserCiphersWithStorageCount > 0 + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END +GO + +IF OBJECT_ID('[dbo].[Cipher_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_DeleteById]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_DeleteById] + @Id UNIQUEIDENTIFIER +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @Attachments BIT + + SELECT TOP 1 + @UserId = [UserId], + @OrganizationId = [OrganizationId], + @Attachments = CASE WHEN [Attachments] IS NOT NULL THEN 1 ELSE 0 END + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id + + DELETE + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + IF @Attachments = 1 + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + END + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + IF @Attachments = 1 + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +IF OBJECT_ID('[dbo].[Cipher_ReadByOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_ReadByOrganizationId]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CipherView] + WHERE + [UserId] IS NULL + AND [OrganizationId] = @OrganizationId +END +GO + +IF OBJECT_ID('[dbo].[Cipher_SoftDelete]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_SoftDelete]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_SoftDelete] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = SYSUTCDATETIME(), + [RevisionDate] = GETUTCDATE() + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END +GO + +IF OBJECT_ID('[dbo].[Cipher_SoftDeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_SoftDeleteById]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_SoftDeleteById] + @Id UNIQUEIDENTIFIER +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT TOP 1 + @UserId = [UserId], + @OrganizationId = [OrganizationId] + FROM + [dbo].[Cipher] + WHERE + [Id] = @Id + + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = SYSUTCDATETIME(), + [RevisionDate] = GETUTCDATE() + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +IF OBJECT_ID('[dbo].[Cipher_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_Create]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7) -- not used +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Favorites], + [Folders], + [Attachments], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + @OrganizationId, + @Type, + @Data, + @Favorites, + @Folders, + @Attachments, + @CreationDate, + @RevisionDate + ) + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +IF OBJECT_ID('[dbo].[CipherDetails_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherDetails_Create]; +END +GO +CREATE PROCEDURE [dbo].[CipherDetails_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7) -- not used +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Favorites], + [Folders], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + @OrganizationId, + @Type, + @Data, + CASE WHEN @Favorite = 1 THEN CONCAT('{', @UserIdKey, ':true}') ELSE NULL END, + CASE WHEN @FolderId IS NOT NULL THEN CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') ELSE NULL END, + @CreationDate, + @RevisionDate + ) + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +IF OBJECT_ID('[dbo].[Cipher_CreateWithCollections]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_CreateWithCollections]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_CreateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), -- not used + @CollectionIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, + @Attachments, @CreationDate, @RevisionDate, @DeletedDate + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds +END +GO + +IF OBJECT_ID('[dbo].[CipherDetails_CreateWithCollections]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherDetails_CreateWithCollections]; +END +GO +CREATE PROCEDURE [dbo].[CipherDetails_CreateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7), -- not used + @CollectionIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, + @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @OrganizationUseTotp, @DeletedDate + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds +END +GO + +IF OBJECT_ID('[dbo].[Cipher_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_Update]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7) -- not used +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Favorites] = @Favorites, + [Folders] = @Folders, + [Attachments] = @Attachments, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +IF OBJECT_ID('[dbo].[Cipher_UpdateWithCollections]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Cipher_UpdateWithCollections]; +END +GO +CREATE PROCEDURE [dbo].[Cipher_UpdateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), -- not used + @CollectionIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION Cipher_UpdateWithCollections + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds + + IF @UpdateCollectionsSuccess < 0 + BEGIN + COMMIT TRANSACTION Cipher_UpdateWithCollections + SELECT -1 -- -1 = Failure + RETURN + END + + UPDATE + [dbo].[Cipher] + SET + [UserId] = NULL, + [OrganizationId] = @OrganizationId, + [Data] = @Data, + [Attachments] = @Attachments, + [RevisionDate] = @RevisionDate + -- No need to update CreationDate, Favorites, Folders, or Type since that data will not change + -- Do not update DeletedDate because that is a separate atomic action + WHERE + [Id] = @Id + + COMMIT TRANSACTION Cipher_UpdateWithCollections + + IF @Attachments IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_UpdateStorage] @UserId + END + + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + + SELECT 0 -- 0 = Success +END +GO + +IF OBJECT_ID('[dbo].[CipherDetails_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherDetails_Update]; +END +GO +CREATE PROCEDURE [dbo].[CipherDetails_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(2) -- not used +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Folders] = + CASE + WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN + CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') + WHEN @FolderId IS NOT NULL THEN + JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50))) + ELSE + JSON_MODIFY([Folders], @UserIdPath, NULL) + END, + [Favorites] = + CASE + WHEN @Favorite = 1 AND [Favorites] IS NULL THEN + CONCAT('{', @UserIdKey, ':true}') + WHEN @Favorite = 1 THEN + JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT)) + ELSE + JSON_MODIFY([Favorites], @UserIdPath, NULL) + END, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +/* +IF OBJECT_ID('[dbo].') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].; +END +GO + +GO +*/ \ No newline at end of file