diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 0d78482311..a475918e3b 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -12,6 +12,8 @@ using Bit.Core.Utilities; using Bit.Core; using Bit.Core.Models.Business; using Bit.Api.Utilities; +using Bit.Core.Models.Table; +using System.Collections.Generic; namespace Bit.Api.Controllers { @@ -21,6 +23,8 @@ namespace Bit.Api.Controllers { private readonly IUserService _userService; private readonly IUserRepository _userRepository; + private readonly ICipherRepository _cipherRepository; + private readonly IFolderRepository _folderRepository; private readonly ICipherService _cipherService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ILicensingService _licenseService; @@ -29,6 +33,8 @@ namespace Bit.Api.Controllers public AccountsController( IUserService userService, IUserRepository userRepository, + ICipherRepository cipherRepository, + IFolderRepository folderRepository, ICipherService cipherService, IOrganizationUserRepository organizationUserRepository, ILicensingService licenseService, @@ -36,6 +42,8 @@ namespace Bit.Api.Controllers { _userService = userService; _userRepository = userRepository; + _cipherRepository = cipherRepository; + _folderRepository = folderRepository; _cipherService = cipherService; _organizationUserRepository = organizationUserRepository; _licenseService = licenseService; @@ -219,11 +227,27 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } - // NOTE: It is assumed that the eventual repository call will make sure the updated - // ciphers belong to user making this call. Therefore, no check is done here. + var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id); + var ciphersDict = model.Ciphers?.ToDictionary(c => c.Id.Value); + var ciphers = new List(); + if(existingCiphers.Any() && ciphersDict != null) + { + foreach(var cipher in existingCiphers.Where(c => ciphersDict.ContainsKey(c.Id))) + { + ciphers.Add(ciphersDict[cipher.Id].ToCipher(cipher)); + } + } - var ciphers = model.Ciphers.Select(c => c.ToCipher(user.Id)); - var folders = model.Folders.Select(c => c.ToFolder(user.Id)); + var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id); + var foldersDict = model.Folders?.ToDictionary(f => f.Id); + var folders = new List(); + if(existingFolders.Any() && foldersDict != null) + { + foreach(var folder in existingFolders.Where(f => foldersDict.ContainsKey(f.Id))) + { + folders.Add(foldersDict[folder.Id].ToFolder(folder)); + } + } var result = await _userService.UpdateKeyAsync( user, diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 305d46bce0..8f61629572 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -391,13 +391,12 @@ namespace Bit.Api.Controllers var shareCiphers = new List(); foreach(var cipher in model.Ciphers) { - var cipherGuid = new Guid(cipher.Id); - if(!ciphersDict.ContainsKey(cipherGuid)) + if(!ciphersDict.ContainsKey(cipher.Id.Value)) { throw new BadRequestException("Trying to share ciphers that you do not own."); } - shareCiphers.Add(cipher.ToCipher(ciphersDict[cipherGuid])); + shareCiphers.Add(cipher.ToCipher(ciphersDict[cipher.Id.Value])); } await _cipherService.ShareManyAsync(shareCiphers, organizationId, @@ -450,9 +449,9 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await Request.GetFileAsync(async (stream, fileName) => + await Request.GetFileAsync(async (stream, fileName, key) => { - await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, + await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, Request.ContentLength.GetValueOrDefault(0), userId); }); @@ -475,9 +474,9 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await Request.GetFileAsync(async (stream, fileName) => + await Request.GetFileAsync(async (stream, fileName, key) => { - await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, + await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, Request.ContentLength.GetValueOrDefault(0), userId); }); @@ -498,9 +497,9 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await Request.GetFileAsync(async (stream, fileName) => + await Request.GetFileAsync(async (stream, fileName, key) => { - await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, + await _cipherService.CreateAttachmentShareAsync(cipher, stream, Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId); }); } diff --git a/src/Api/Utilities/MultipartFormDataHelper.cs b/src/Api/Utilities/MultipartFormDataHelper.cs index 69941b70d9..e9ef945b67 100644 --- a/src/Api/Utilities/MultipartFormDataHelper.cs +++ b/src/Api/Utilities/MultipartFormDataHelper.cs @@ -13,40 +13,54 @@ namespace Bit.Api.Utilities { private static readonly FormOptions _defaultFormOptions = new FormOptions(); - public static async Task GetFileAsync(this HttpRequest request, Func callback) - { - await request.GetFilesAsync(1, callback); - } - - private static async Task GetFilesAsync(this HttpRequest request, int? fileCount, Func callback) + public static async Task GetFileAsync(this HttpRequest request, Func callback) { var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, request.Body); - var section = await reader.ReadNextSectionAsync(); - var fileNumber = 1; - while(section != null && fileNumber <= fileCount) + var firstSection = await reader.ReadNextSectionAsync(); + if(firstSection != null) { - if(ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var content) && - HasFileContentDisposition(content)) + if(ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out var firstContent)) { - var fileName = HeaderUtilities.RemoveQuotes(content.FileName).ToString(); - using(section.Body) + if(HasFileContentDisposition(firstContent)) { - await callback(section.Body, fileName); + // Old style with just data + var fileName = HeaderUtilities.RemoveQuotes(firstContent.FileName).ToString(); + using(firstSection.Body) + { + await callback(firstSection.Body, fileName, null); + } + } + else if(HasKeyDisposition(firstContent)) + { + // New style with key, then data + string key = null; + using(var sr = new StreamReader(firstSection.Body)) + { + key = await sr.ReadToEndAsync(); + } + + var secondSection = await reader.ReadNextSectionAsync(); + if(secondSection != null) + { + if(ContentDispositionHeaderValue.TryParse(secondSection.ContentDisposition, + out var secondContent) && HasFileContentDisposition(secondContent)) + { + var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString(); + using(secondSection.Body) + { + await callback(secondSection.Body, fileName, key); + } + } + + secondSection = null; + } } } - if(fileNumber >= fileCount) - { - section = null; - } - else - { - section = await reader.ReadNextSectionAsync(); - fileNumber++; - } + firstSection = null; } } @@ -68,9 +82,15 @@ namespace Bit.Api.Utilities private static bool HasFileContentDisposition(ContentDispositionHeaderValue content) { - // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" + // Content-Disposition: form-data; name="data"; filename="Misc 002.jpg" return content != null && content.DispositionType.Equals("form-data") && (!StringSegment.IsNullOrEmpty(content.FileName) || !StringSegment.IsNullOrEmpty(content.FileNameStar)); } + + private static bool HasKeyDisposition(ContentDispositionHeaderValue content) + { + // Content-Disposition: form-data; name="key"; + return content != null && content.DispositionType.Equals("form-data") && content.Name == "key"; + } } } diff --git a/src/Core/Models/Api/CipherAttachmentModel.cs b/src/Core/Models/Api/CipherAttachmentModel.cs new file mode 100644 index 0000000000..bf148e4d6b --- /dev/null +++ b/src/Core/Models/Api/CipherAttachmentModel.cs @@ -0,0 +1,21 @@ +using Bit.Core.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Api +{ + public class CipherAttachmentModel + { + public CipherAttachmentModel() { } + + public CipherAttachmentModel(CipherAttachment.MetaData data) + { + FileName = data.FileName; + Key = data.Key; + } + + [EncryptedStringLength(1000)] + public string FileName { get; set; } + [EncryptedStringLength(1000)] + public string Key { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/CipherRequestModel.cs b/src/Core/Models/Api/Request/CipherRequestModel.cs index 24abbeced5..7b041073ce 100644 --- a/src/Core/Models/Api/Request/CipherRequestModel.cs +++ b/src/Core/Models/Api/Request/CipherRequestModel.cs @@ -29,7 +29,10 @@ namespace Bit.Core.Models.Api public string Notes { get; set; } public IEnumerable Fields { get; set; } public IEnumerable PasswordHistory { get; set; } + [Obsolete] public Dictionary Attachments { get; set; } + // TODO: Rename to Attachments whenever the above is finally removed. + public Dictionary Attachments2 { get; set; } public CipherLoginModel Login { get; set; } public CipherCardModel Card { get; set; } @@ -84,7 +87,10 @@ namespace Bit.Core.Models.Api throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); } - if((Attachments?.Count ?? 0) == 0) + var hasAttachments2 = (Attachments2?.Count ?? 0) > 0; + var hasAttachments = (Attachments?.Count ?? 0) > 0; + + if(!hasAttachments2 && !hasAttachments) { return existingCipher; } @@ -95,9 +101,22 @@ namespace Bit.Core.Models.Api return existingCipher; } - foreach(var attachment in attachments.Where(a => Attachments.ContainsKey(a.Key))) + if(hasAttachments2) { - attachment.Value.FileName = Attachments[attachment.Key]; + foreach(var attachment in attachments.Where(a => Attachments2.ContainsKey(a.Key))) + { + var attachment2 = Attachments2[attachment.Key]; + attachment.Value.FileName = attachment2.FileName; + attachment.Value.Key = attachment2.Key; + } + } + else if(hasAttachments) + { + foreach(var attachment in attachments.Where(a => Attachments.ContainsKey(a.Key))) + { + attachment.Value.FileName = Attachments[attachment.Key]; + attachment.Value.Key = null; + } } existingCipher.SetAttachments(attachments); @@ -132,15 +151,7 @@ namespace Bit.Core.Models.Api public class CipherWithIdRequestModel : CipherRequestModel { [Required] - [StringLength(36)] - public string Id { get; set; } - - public Cipher ToCipher(Guid userId) - { - var cipher = ToCipherDetails(userId); - cipher.Id = new Guid(Id); - return cipher; - } + public Guid? Id { get; set; } } public class CipherCreateRequestModel : IValidatableObject @@ -224,7 +235,7 @@ namespace Bit.Core.Models.Api organizationIds.Add(c.OrganizationId); if(allHaveIds) { - allHaveIds = !(string.IsNullOrWhiteSpace(c.Id) || string.IsNullOrWhiteSpace(c.OrganizationId)); + allHaveIds = !(!c.Id.HasValue || string.IsNullOrWhiteSpace(c.OrganizationId)); } } diff --git a/src/Core/Models/Api/Request/FolderRequestModel.cs b/src/Core/Models/Api/Request/FolderRequestModel.cs index 6d6d56fd24..7966fa38a5 100644 --- a/src/Core/Models/Api/Request/FolderRequestModel.cs +++ b/src/Core/Models/Api/Request/FolderRequestModel.cs @@ -31,14 +31,5 @@ namespace Bit.Core.Models.Api public class FolderWithIdRequestModel : FolderRequestModel { public Guid Id { get; set; } - - public new Folder ToFolder(Guid userId) - { - return ToFolder(new Folder - { - UserId = userId, - Id = Id - }); - } } } diff --git a/src/Core/Models/Api/Response/AttachmentResponseModel.cs b/src/Core/Models/Api/Response/AttachmentResponseModel.cs index 3c9abb9cb8..077d278c94 100644 --- a/src/Core/Models/Api/Response/AttachmentResponseModel.cs +++ b/src/Core/Models/Api/Response/AttachmentResponseModel.cs @@ -1,6 +1,5 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Table; -using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; @@ -8,12 +7,14 @@ namespace Bit.Core.Models.Api { public class AttachmentResponseModel : ResponseModel { - public AttachmentResponseModel(string id, CipherAttachment.MetaData data, Cipher cipher, GlobalSettings globalSettings) + public AttachmentResponseModel(string id, CipherAttachment.MetaData data, Cipher cipher, + GlobalSettings globalSettings) : base("attachment") { Id = id; Url = $"{globalSettings.Attachment.BaseUrl}/{cipher.Id}/{id}"; FileName = data.FileName; + Key = data.Key; Size = data.SizeString; SizeName = Utilities.CoreHelpers.ReadableBytesSize(data.Size); } @@ -21,6 +22,7 @@ namespace Bit.Core.Models.Api public string Id { get; set; } public string Url { get; set; } public string FileName { get; set; } + public string Key { get; set; } public string Size { get; set; } public string SizeName { get; set; } diff --git a/src/Core/Models/Data/CipherAttachment.cs b/src/Core/Models/Data/CipherAttachment.cs index 20891f273c..03e671fa49 100644 --- a/src/Core/Models/Data/CipherAttachment.cs +++ b/src/Core/Models/Data/CipherAttachment.cs @@ -31,6 +31,7 @@ namespace Bit.Core.Models.Data } public string FileName { get; set; } + public string Key { get; set; } } } } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index 7050ffcf29..d116488ce6 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -340,6 +340,7 @@ namespace Bit.Core.Repositories.SqlServer [dbo].[Cipher] SET [Data] = TC.[Data], + [Attachments] = TC.[Attachments], [RevisionDate] = TC.[RevisionDate] FROM [dbo].[Cipher] C diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 12b53ff37e..37509cc1d3 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -13,10 +13,9 @@ namespace Bit.Core.Services bool skipPermissionCheck = false, bool limitCollectionScope = true); Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, IEnumerable collectionIds = null, bool skipPermissionCheck = false); - Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId, - bool orgAdmin = false); - Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, long requestLength, - string attachmentId, + Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, + long requestLength, Guid savingUserId, bool orgAdmin = false); + Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, string attachmentId, Guid organizationShareId); Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId); diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 8bff0b1f36..2265512585 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -143,8 +143,8 @@ namespace Bit.Core.Services } } - public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, - Guid savingUserId, bool orgAdmin = false) + public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, + long requestLength, Guid savingUserId, bool orgAdmin = false) { if(!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId))) { @@ -201,6 +201,7 @@ namespace Bit.Core.Services var data = new CipherAttachment.MetaData { FileName = fileName, + Key = key, Size = stream.Length }; @@ -228,7 +229,7 @@ namespace Bit.Core.Services await _pushService.PushSyncCipherUpdateAsync(cipher, null); } - public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, long requestLength, + public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, string attachmentId, Guid organizationId) { try