1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-26 12:55:17 +01:00

support for attachments keys

load existing items and set attachments on key update
This commit is contained in:
Kyle Spearrin 2018-11-14 17:19:04 -05:00
parent 73cc221deb
commit 7cda459127
11 changed files with 138 additions and 68 deletions

View File

@ -12,6 +12,8 @@ using Bit.Core.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Models.Table;
using System.Collections.Generic;
namespace Bit.Api.Controllers namespace Bit.Api.Controllers
{ {
@ -21,6 +23,8 @@ namespace Bit.Api.Controllers
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ILicensingService _licenseService; private readonly ILicensingService _licenseService;
@ -29,6 +33,8 @@ namespace Bit.Api.Controllers
public AccountsController( public AccountsController(
IUserService userService, IUserService userService,
IUserRepository userRepository, IUserRepository userRepository,
ICipherRepository cipherRepository,
IFolderRepository folderRepository,
ICipherService cipherService, ICipherService cipherService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ILicensingService licenseService, ILicensingService licenseService,
@ -36,6 +42,8 @@ namespace Bit.Api.Controllers
{ {
_userService = userService; _userService = userService;
_userRepository = userRepository; _userRepository = userRepository;
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_cipherService = cipherService; _cipherService = cipherService;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_licenseService = licenseService; _licenseService = licenseService;
@ -219,11 +227,27 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
} }
// NOTE: It is assumed that the eventual repository call will make sure the updated var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id);
// ciphers belong to user making this call. Therefore, no check is done here. var ciphersDict = model.Ciphers?.ToDictionary(c => c.Id.Value);
var ciphers = new List<Cipher>();
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 existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id);
var folders = model.Folders.Select(c => c.ToFolder(user.Id)); var foldersDict = model.Folders?.ToDictionary(f => f.Id);
var folders = new List<Folder>();
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( var result = await _userService.UpdateKeyAsync(
user, user,

View File

@ -391,13 +391,12 @@ namespace Bit.Api.Controllers
var shareCiphers = new List<Cipher>(); var shareCiphers = new List<Cipher>();
foreach(var cipher in model.Ciphers) foreach(var cipher in model.Ciphers)
{ {
var cipherGuid = new Guid(cipher.Id); if(!ciphersDict.ContainsKey(cipher.Id.Value))
if(!ciphersDict.ContainsKey(cipherGuid))
{ {
throw new BadRequestException("Trying to share ciphers that you do not own."); 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, await _cipherService.ShareManyAsync(shareCiphers, organizationId,
@ -450,9 +449,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); 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); Request.ContentLength.GetValueOrDefault(0), userId);
}); });
@ -475,9 +474,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); 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); Request.ContentLength.GetValueOrDefault(0), userId);
}); });
@ -498,9 +497,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); 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); Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
}); });
} }

View File

@ -13,40 +13,54 @@ namespace Bit.Api.Utilities
{ {
private static readonly FormOptions _defaultFormOptions = new FormOptions(); private static readonly FormOptions _defaultFormOptions = new FormOptions();
public static async Task GetFileAsync(this HttpRequest request, Func<Stream, string, Task> callback) public static async Task GetFileAsync(this HttpRequest request, Func<Stream, string, string, Task> callback)
{
await request.GetFilesAsync(1, callback);
}
private static async Task GetFilesAsync(this HttpRequest request, int? fileCount, Func<Stream, string, Task> callback)
{ {
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit); _defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body); var reader = new MultipartReader(boundary, request.Body);
var section = await reader.ReadNextSectionAsync(); var firstSection = await reader.ReadNextSectionAsync();
var fileNumber = 1; if(firstSection != null)
while(section != null && fileNumber <= fileCount)
{ {
if(ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var content) && if(ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out var firstContent))
HasFileContentDisposition(content))
{ {
var fileName = HeaderUtilities.RemoveQuotes(content.FileName).ToString(); if(HasFileContentDisposition(firstContent))
using(section.Body)
{ {
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) firstSection = null;
{
section = null;
}
else
{
section = await reader.ReadNextSectionAsync();
fileNumber++;
}
} }
} }
@ -68,9 +82,15 @@ namespace Bit.Api.Utilities
private static bool HasFileContentDisposition(ContentDispositionHeaderValue content) 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") && return content != null && content.DispositionType.Equals("form-data") &&
(!StringSegment.IsNullOrEmpty(content.FileName) || !StringSegment.IsNullOrEmpty(content.FileNameStar)); (!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";
}
} }
} }

View File

@ -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; }
}
}

View File

@ -29,7 +29,10 @@ namespace Bit.Core.Models.Api
public string Notes { get; set; } public string Notes { get; set; }
public IEnumerable<CipherFieldModel> Fields { get; set; } public IEnumerable<CipherFieldModel> Fields { get; set; }
public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; } public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }
[Obsolete]
public Dictionary<string, string> Attachments { get; set; } public Dictionary<string, string> Attachments { get; set; }
// TODO: Rename to Attachments whenever the above is finally removed.
public Dictionary<string, CipherAttachmentModel> Attachments2 { get; set; }
public CipherLoginModel Login { get; set; } public CipherLoginModel Login { get; set; }
public CipherCardModel Card { get; set; } public CipherCardModel Card { get; set; }
@ -84,7 +87,10 @@ namespace Bit.Core.Models.Api
throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); 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; return existingCipher;
} }
@ -95,9 +101,22 @@ namespace Bit.Core.Models.Api
return existingCipher; 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); existingCipher.SetAttachments(attachments);
@ -132,15 +151,7 @@ namespace Bit.Core.Models.Api
public class CipherWithIdRequestModel : CipherRequestModel public class CipherWithIdRequestModel : CipherRequestModel
{ {
[Required] [Required]
[StringLength(36)] public Guid? Id { get; set; }
public string Id { get; set; }
public Cipher ToCipher(Guid userId)
{
var cipher = ToCipherDetails(userId);
cipher.Id = new Guid(Id);
return cipher;
}
} }
public class CipherCreateRequestModel : IValidatableObject public class CipherCreateRequestModel : IValidatableObject
@ -224,7 +235,7 @@ namespace Bit.Core.Models.Api
organizationIds.Add(c.OrganizationId); organizationIds.Add(c.OrganizationId);
if(allHaveIds) if(allHaveIds)
{ {
allHaveIds = !(string.IsNullOrWhiteSpace(c.Id) || string.IsNullOrWhiteSpace(c.OrganizationId)); allHaveIds = !(!c.Id.HasValue || string.IsNullOrWhiteSpace(c.OrganizationId));
} }
} }

View File

@ -31,14 +31,5 @@ namespace Bit.Core.Models.Api
public class FolderWithIdRequestModel : FolderRequestModel public class FolderWithIdRequestModel : FolderRequestModel
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public new Folder ToFolder(Guid userId)
{
return ToFolder(new Folder
{
UserId = userId,
Id = Id
});
}
} }
} }

View File

@ -1,6 +1,5 @@
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -8,12 +7,14 @@ namespace Bit.Core.Models.Api
{ {
public class AttachmentResponseModel : ResponseModel 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") : base("attachment")
{ {
Id = id; Id = id;
Url = $"{globalSettings.Attachment.BaseUrl}/{cipher.Id}/{id}"; Url = $"{globalSettings.Attachment.BaseUrl}/{cipher.Id}/{id}";
FileName = data.FileName; FileName = data.FileName;
Key = data.Key;
Size = data.SizeString; Size = data.SizeString;
SizeName = Utilities.CoreHelpers.ReadableBytesSize(data.Size); SizeName = Utilities.CoreHelpers.ReadableBytesSize(data.Size);
} }
@ -21,6 +22,7 @@ namespace Bit.Core.Models.Api
public string Id { get; set; } public string Id { get; set; }
public string Url { get; set; } public string Url { get; set; }
public string FileName { get; set; } public string FileName { get; set; }
public string Key { get; set; }
public string Size { get; set; } public string Size { get; set; }
public string SizeName { get; set; } public string SizeName { get; set; }

View File

@ -31,6 +31,7 @@ namespace Bit.Core.Models.Data
} }
public string FileName { get; set; } public string FileName { get; set; }
public string Key { get; set; }
} }
} }
} }

View File

@ -340,6 +340,7 @@ namespace Bit.Core.Repositories.SqlServer
[dbo].[Cipher] [dbo].[Cipher]
SET SET
[Data] = TC.[Data], [Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate] [RevisionDate] = TC.[RevisionDate]
FROM FROM
[dbo].[Cipher] C [dbo].[Cipher] C

View File

@ -13,10 +13,9 @@ namespace Bit.Core.Services
bool skipPermissionCheck = false, bool limitCollectionScope = true); bool skipPermissionCheck = false, bool limitCollectionScope = true);
Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, IEnumerable<Guid> collectionIds = null, Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, IEnumerable<Guid> collectionIds = null,
bool skipPermissionCheck = false); bool skipPermissionCheck = false);
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId, Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
bool orgAdmin = false); long requestLength, Guid savingUserId, bool orgAdmin = false);
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, string attachmentId,
string attachmentId,
Guid organizationShareId); Guid organizationShareId);
Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId); Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId);

View File

@ -143,8 +143,8 @@ namespace Bit.Core.Services
} }
} }
public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
Guid savingUserId, bool orgAdmin = false) long requestLength, Guid savingUserId, bool orgAdmin = false)
{ {
if(!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId))) if(!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId)))
{ {
@ -201,6 +201,7 @@ namespace Bit.Core.Services
var data = new CipherAttachment.MetaData var data = new CipherAttachment.MetaData
{ {
FileName = fileName, FileName = fileName,
Key = key,
Size = stream.Length Size = stream.Length
}; };
@ -228,7 +229,7 @@ namespace Bit.Core.Services
await _pushService.PushSyncCipherUpdateAsync(cipher, null); 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) string attachmentId, Guid organizationId)
{ {
try try