diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index a0fabf1db9..1b06db2d97 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -20,7 +20,6 @@ namespace Bit.Api.Controllers private readonly ICollectionCipherRepository _collectionCipherRepository; private readonly ICipherService _cipherService; private readonly IUserService _userService; - private readonly IAttachmentStorageService _attachmentStorageService; private readonly CurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -29,7 +28,6 @@ namespace Bit.Api.Controllers ICollectionCipherRepository collectionCipherRepository, ICipherService cipherService, IUserService userService, - IAttachmentStorageService attachmentStorageService, CurrentContext currentContext, GlobalSettings globalSettings) { @@ -37,7 +35,6 @@ namespace Bit.Api.Controllers _collectionCipherRepository = collectionCipherRepository; _cipherService = cipherService; _userService = userService; - _attachmentStorageService = attachmentStorageService; _currentContext = currentContext; _globalSettings = globalSettings; } @@ -263,10 +260,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - // TODO: check and remove attachmentId from cipher in database - - var storedFilename = $"{idGuid}/{attachmentId}"; - await _attachmentStorageService.DeleteAttachmentAsync(storedFilename); + await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false); } } } diff --git a/src/Core/Models/Api/Response/AttachmentResponseModel.cs b/src/Core/Models/Api/Response/AttachmentResponseModel.cs index 5bc8d38ff4..b3b74b734b 100644 --- a/src/Core/Models/Api/Response/AttachmentResponseModel.cs +++ b/src/Core/Models/Api/Response/AttachmentResponseModel.cs @@ -26,21 +26,13 @@ namespace Bit.Core.Models.Api public static IEnumerable FromCipher(Cipher cipher, GlobalSettings globalSettings) { - if(string.IsNullOrWhiteSpace(cipher.Attachments)) + var attachments = cipher.GetAttachments(); + if(attachments == null) { return null; } - try - { - var attachments = - JsonConvert.DeserializeObject>(cipher.Attachments); - return attachments.Select(a => new AttachmentResponseModel(a.Key, a.Value, cipher, globalSettings)); - } - catch - { - return null; - } + return attachments.Select(a => new AttachmentResponseModel(a.Key, a.Value, cipher, globalSettings)); } } } diff --git a/src/Core/Models/Table/Cipher.cs b/src/Core/Models/Table/Cipher.cs index ffbb59e786..ecbb7af3fa 100644 --- a/src/Core/Models/Table/Cipher.cs +++ b/src/Core/Models/Table/Cipher.cs @@ -1,10 +1,15 @@ using System; using Bit.Core.Utilities; +using Bit.Core.Models.Data; +using Newtonsoft.Json; +using System.Collections.Generic; namespace Bit.Core.Models.Table { public class Cipher : IDataObject { + private Dictionary _attachmentData; + public Guid Id { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } @@ -20,5 +25,71 @@ namespace Bit.Core.Models.Table { Id = CoreHelpers.GenerateComb(); } + + public Dictionary GetAttachments() + { + if(string.IsNullOrWhiteSpace(Attachments)) + { + return null; + } + + if(_attachmentData != null) + { + return _attachmentData; + } + + try + { + _attachmentData = JsonConvert.DeserializeObject>(Attachments); + return _attachmentData; + } + catch + { + return null; + } + } + + public void SetAttachments(Dictionary data) + { + if(data == null || data.Count == 0) + { + _attachmentData = null; + Attachments = null; + return; + } + + _attachmentData = data; + Attachments = JsonConvert.SerializeObject(_attachmentData); + } + + public void AddAttachment(string id, CipherAttachment.MetaData data) + { + var attachments = GetAttachments(); + if(attachments == null) + { + attachments = new Dictionary(); + } + + attachments.Add(id, data); + SetAttachments(attachments); + } + + public void DeleteAttachment(string id) + { + var attachments = GetAttachments(); + if(!attachments?.ContainsKey(id) ?? true) + { + return; + } + + attachments.Remove(id); + SetAttachments(attachments); + } + + public bool ContainsAttachment(string id) + { + var attachments = GetAttachments(); + return attachments?.ContainsKey(id) ?? false; + } } } diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 8a06e159b9..9d7a3cb18b 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -21,6 +21,7 @@ namespace Bit.Core.Repositories Task ReplaceAsync(Cipher obj, IEnumerable collectionIds); Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); Task UpdateAttachmentAsync(CipherAttachment attachment); + Task DeleteAttachmentAsync(Guid cipherId, string attachmentId); Task DeleteAsync(IEnumerable ids, Guid userId); Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId); Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders); diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index 13a60a3880..68341b2191 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -188,6 +188,17 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Cipher_DeleteAttachment]", + new { Id = cipherId, AttachmentId = attachmentId }, + commandType: CommandType.StoredProcedure); + } + } + public async Task DeleteAsync(IEnumerable ids, Guid userId) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index c28bf4970c..2c4943c5ec 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -15,6 +15,7 @@ namespace Bit.Core.Services bool orgAdmin = false); 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 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 a5f414af01..064c17462e 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -154,6 +154,7 @@ namespace Bit.Core.Services }; await _cipherRepository.UpdateAttachmentAsync(attachment); + cipher.AddAttachment(attachmentId, data); } catch { @@ -186,6 +187,28 @@ namespace Bit.Core.Services await _pushService.PushSyncCiphersAsync(deletingUserId); } + public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false) + { + if(!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId))) + { + throw new BadRequestException("You do not have permissions to delete this."); + } + + if(!cipher.ContainsAttachment(attachmentId)) + { + throw new NotFoundException(); + } + + await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentId); + cipher.DeleteAttachment(attachmentId); + + var storedFilename = $"{cipher.Id}/{attachmentId}"; + await _attachmentStorageService.DeleteAttachmentAsync(storedFilename); + + // push + await _pushService.PushSyncCipherUpdateAsync(cipher); + } + 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 89c6561ccd..fb5a8c04b1 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -203,5 +203,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_DeleteAttachment.sql b/src/Sql/dbo/Stored Procedures/Cipher_DeleteAttachment.sql new file mode 100644 index 0000000000..f3b2f9781d --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_DeleteAttachment.sql @@ -0,0 +1,43 @@ +CREATE PROCEDURE [dbo].[Cipher_DeleteAttachment] + @Id UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + + DECLARE @Attachments NVARCHAR(MAX) + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + + SELECT + @UserId = [UserId], + @OrganizationId = [OrganizationId], + @Attachments = [Attachments] + FROM + [dbo].[Cipher] + WHERE [Id] = @Id + + DECLARE @AttachmentData NVARCHAR(MAX) = JSON_QUERY(@Attachments, @AttachmentIdPath) + DECLARE @Size BIGINT = (CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT) * -1) + + UPDATE + [dbo].[Cipher] + SET + [Attachments] = JSON_MODIFY([Attachments], @AttachmentIdPath, NULL) + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId, @Size + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId, @Size + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END \ No newline at end of file