1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

share login with attachments

This commit is contained in:
Kyle Spearrin 2017-07-10 14:30:12 -04:00
parent fbc189544b
commit f8c749bab5
9 changed files with 264 additions and 48 deletions

View File

@ -9,6 +9,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core;
using Bit.Api.Utilities;
using Bit.Core.Utilities;
namespace Bit.Api.Controllers
{
@ -133,14 +134,15 @@ namespace Bit.Api.Controllers
public async Task PutShare(string id, [FromBody]CipherShareRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if(cipher == null || cipher.UserId != userId ||
!_currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId)))
{
throw new NotFoundException();
}
await _cipherService.ShareAsync(model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
var original = CoreHelpers.CloneObject(cipher);
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
model.CollectionIds.Select(c => new Guid(c)), userId);
}
@ -224,15 +226,7 @@ namespace Bit.Api.Controllers
[DisableFormValueModelBinding]
public async Task PostAttachment(string id)
{
if(!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
if(Request.ContentLength > 105906176) // 101 MB, give em' 1 extra MB for cushion
{
throw new BadRequestException("Max file size is 100 MB.");
}
ValidateAttachment();
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
@ -244,7 +238,28 @@ namespace Bit.Api.Controllers
await Request.GetFileAsync(async (stream, fileName) =>
{
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, Request.ContentLength.GetValueOrDefault(0), userId);
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName,
Request.ContentLength.GetValueOrDefault(0), userId);
});
}
[HttpPost("{id}/attachment/{attachmentId}/share")]
[DisableFormValueModelBinding]
public async Task PostAttachmentShare(string id, string attachmentId, Guid organizationId)
{
ValidateAttachment();
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if(cipher == null || cipher.UserId != userId || !_currentContext.OrganizationUser(organizationId))
{
throw new NotFoundException();
}
await Request.GetFileAsync(async (stream, fileName) =>
{
await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName,
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
});
}
@ -262,5 +277,18 @@ namespace Bit.Api.Controllers
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
}
private void ValidateAttachment()
{
if(!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
if(Request.ContentLength > 105906176) // 101 MB, give em' 1 extra MB for cushion
{
throw new BadRequestException("Max file size is 100 MB.");
}
}
}
}

View File

@ -48,7 +48,7 @@ namespace Bit.Core.Models.Api
});
}
public Cipher ToCipher(Cipher existingCipher)
public virtual Cipher ToCipher(Cipher existingCipher)
{
switch(existingCipher.Type)
{
@ -64,12 +64,35 @@ namespace Bit.Core.Models.Api
}
}
public class CipherAttachmentRequestModel : CipherRequestModel
{
public Dictionary<string, string> Attachments { get; set; }
public override Cipher ToCipher(Cipher existingCipher)
{
base.ToCipher(existingCipher);
var attachments = existingCipher.GetAttachments();
if((Attachments?.Count ?? 0) > 0 && (attachments?.Count ?? 0) > 0)
{
foreach(var attachment in existingCipher.GetAttachments().Where(a => Attachments.ContainsKey(a.Key)))
{
attachment.Value.FileName = Attachments[attachment.Key];
}
existingCipher.SetAttachments(attachments);
}
return existingCipher;
}
}
public class CipherShareRequestModel : IValidatableObject
{
[Required]
public IEnumerable<string> CollectionIds { get; set; }
[Required]
public CipherRequestModel Cipher { get; set; }
public CipherAttachmentRequestModel Cipher { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{

View File

@ -1,11 +1,16 @@
using System.IO;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public interface IAttachmentStorageService
{
Task UploadAttachmentAsync(Stream stream, string name);
Task DeleteAttachmentAsync(string name);
Task UploadNewAttachmentAsync(Stream stream, Guid cipherId, string attachmentId);
Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, string attachmentId);
Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId);
Task CommitShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId);
Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId);
Task DeleteAttachmentAsync(Guid cipherId, string attachmentId);
}
}

View File

@ -13,13 +13,15 @@ namespace Bit.Core.Services
Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId);
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,
Guid organizationShareId);
Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId);
Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId);
Task SaveFolderAsync(Folder folder);
Task DeleteFolderAsync(Folder folder);
Task ShareAsync(Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds, Guid userId);
Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds, Guid userId);
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships);

View File

@ -2,6 +2,7 @@
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using System.IO;
using System;
namespace Bit.Core.Services
{
@ -19,20 +20,80 @@ namespace Bit.Core.Services
_blobClient = storageAccount.CreateCloudBlobClient();
}
public async Task UploadAttachmentAsync(Stream stream, string name)
public async Task UploadNewAttachmentAsync(Stream stream, Guid cipherId, string attachmentId)
{
await InitAsync();
var blob = _attachmentsContainer.GetBlockBlobReference(name);
await blob.UploadFromStreamAsync(stream);
await UploadAttachmentAsync(stream, $"{cipherId}/{attachmentId}");
}
public async Task DeleteAttachmentAsync(string name)
public async Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, string attachmentId)
{
await UploadAttachmentAsync(stream, $"{cipherId}/share/{organizationId}/{attachmentId}");
}
public async Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{
await InitAsync();
var blob = _attachmentsContainer.GetBlockBlobReference(name);
var source = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/share/{organizationId}/{attachmentId}");
if(!await source.ExistsAsync())
{
return;
}
var dest = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/{attachmentId}");
if(!await dest.ExistsAsync())
{
return;
}
var original = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/temp/{attachmentId}");
await original.DeleteIfExistsAsync();
await original.StartCopyAsync(dest);
await dest.DeleteIfExistsAsync();
await dest.StartCopyAsync(source);
}
public async Task CommitShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{
await InitAsync();
var source = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/share/{organizationId}/{attachmentId}");
var original = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/temp/{attachmentId}");
await original.DeleteIfExistsAsync();
await source.DeleteIfExistsAsync();
}
public async Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{
await InitAsync();
var source = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/share/{organizationId}/{attachmentId}");
var dest = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/{attachmentId}");
var original = _attachmentsContainer.GetBlockBlobReference($"{cipherId}/temp/{attachmentId}");
if(!await original.ExistsAsync())
{
return;
}
await dest.DeleteIfExistsAsync();
await dest.StartCopyAsync(original);
await original.DeleteIfExistsAsync();
await source.DeleteIfExistsAsync();
}
public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId)
{
await InitAsync();
var blobName = $"{cipherId}/{attachmentId}";
var blob = _attachmentsContainer.GetBlockBlobReference(blobName);
await blob.DeleteIfExistsAsync();
}
private async Task UploadAttachmentAsync(Stream stream, string blobName)
{
await InitAsync();
var blob = _attachmentsContainer.GetBlockBlobReference(blobName);
await blob.UploadFromStreamAsync(stream);
}
private async Task InitAsync()
{
if(_attachmentsContainer == null)

View File

@ -139,8 +139,7 @@ namespace Bit.Core.Services
}
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
var storageId = $"{cipher.Id}/{attachmentId}";
await _attachmentStorageService.UploadAttachmentAsync(stream, storageId);
await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher.Id, attachmentId);
try
{
@ -165,7 +164,7 @@ namespace Bit.Core.Services
catch
{
// Clean up since this is not transactional
await _attachmentStorageService.DeleteAttachmentAsync(storageId);
await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentId);
throw;
}
@ -173,6 +172,29 @@ namespace Bit.Core.Services
await _pushService.PushSyncCipherUpdateAsync(cipher);
}
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, long requestLength,
string attachmentId, Guid organizationId)
{
if(requestLength < 1)
{
throw new BadRequestException("No data to attach.");
}
var org = await _organizationRepository.GetByIdAsync(organizationId);
if(!org.MaxStorageGb.HasValue)
{
throw new BadRequestException("This organization cannot use attachments.");
}
var storageBytesRemaining = org.StorageBytesRemaining();
if(storageBytesRemaining < requestLength)
{
throw new BadRequestException("Not enough storage available for this organization.");
}
await _attachmentStorageService.UploadShareAttachmentAsync(stream, cipher.Id, organizationId, attachmentId);
}
public async Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
{
if(!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
@ -207,9 +229,7 @@ namespace Bit.Core.Services
await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentId);
cipher.DeleteAttachment(attachmentId);
var storedFilename = $"{cipher.Id}/{attachmentId}";
await _attachmentStorageService.DeleteAttachmentAsync(storedFilename);
await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentId);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher);
@ -258,7 +278,8 @@ namespace Bit.Core.Services
await _pushService.PushSyncFolderDeleteAsync(folder);
}
public async Task ShareAsync(Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds, Guid sharingUserId)
public async Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId,
IEnumerable<Guid> collectionIds, Guid sharingUserId)
{
if(cipher.Id == default(Guid))
{
@ -275,11 +296,48 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
// Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.
cipher.UserId = sharingUserId;
cipher.OrganizationId = organizationId;
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher, collectionIds);
var attachments = cipher.GetAttachments();
var hasAttachments = (attachments?.Count ?? 0) > 0;
try
{
// Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.
cipher.UserId = sharingUserId;
cipher.OrganizationId = organizationId;
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher, collectionIds);
if(hasAttachments)
{
// migrate attachments
foreach(var attachment in attachments)
{
await _attachmentStorageService.StartShareAttachmentAsync(cipher.Id, organizationId, attachment.Key);
}
}
}
catch
{
// roll everything back
await _cipherRepository.ReplaceAsync(originalCipher);
if(!hasAttachments)
{
throw;
}
foreach(var attachment in attachments)
{
await _attachmentStorageService.RollbackShareAttachmentAsync(cipher.Id, organizationId, attachment.Key);
}
throw;
}
// commit attachment migration
foreach(var attachment in attachments)
{
await _attachmentStorageService.CommitShareAttachmentAsync(cipher.Id, organizationId, attachment.Key);
}
// push
await _pushService.PushSyncCipherUpdateAsync(cipher);

View File

@ -1,16 +1,37 @@
using System.IO;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class NoopAttachmentStorageService : IAttachmentStorageService
{
public Task DeleteAttachmentAsync(string name)
public Task CommitShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{
return Task.FromResult(0);
}
public Task UploadAttachmentAsync(Stream stream, string name)
public Task DeleteAttachmentAsync(Guid cipherId, string attachmentId)
{
return Task.FromResult(0);
}
public Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{
return Task.FromResult(0);
}
public Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{
return Task.FromResult(0);
}
public Task UploadNewAttachmentAsync(Stream stream, Guid cipherId, string attachmentId)
{
return Task.FromResult(0);
}
public Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, string attachmentId)
{
return Task.FromResult(0);
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Dapper;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
@ -242,5 +243,10 @@ namespace Bit.Core.Utilities
// Return formatted number with suffix
return readable.ToString("0.## ") + suffix;
}
public static T CloneObject<T>(T obj)
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
}
}
}

View File

@ -14,14 +14,31 @@ AS
BEGIN
SET NOCOUNT ON
DECLARE @CipherAttachments NVARCHAR(MAX)
SELECT
@CipherAttachments = [Attachments]
FROM
[dbo].[Cipher]
WHERE [Id] = @Id
DECLARE @Size BIGINT
SELECT
@Size = SUM(CAST(JSON_VALUE(value,'$.Size') AS BIGINT))
FROM
OPENJSON(@CipherAttachments)
DECLARE @SizeDec BIGINT = @Size * -1
UPDATE
[dbo].[Cipher]
SET
[UserId] = NULL,
[OrganizationId] = @OrganizationId,
[Data] = @Data,
[Attachments] = @Attachments,
[RevisionDate] = @RevisionDate
-- No need to update Attachments, CreationDate, Favorites, Folders, or Type since that data will not change
-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change
WHERE
[Id] = @Id
@ -66,12 +83,7 @@ BEGIN
WHERE
[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId, @Size
EXEC [dbo].[User_UpdateStorage] @UserId, @SizeDec
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
END