1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-17 11:08:16 -05: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.Services;
using Bit.Core; using Bit.Core;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Utilities;
namespace Bit.Api.Controllers namespace Bit.Api.Controllers
{ {
@ -133,14 +134,15 @@ namespace Bit.Api.Controllers
public async Task PutShare(string id, [FromBody]CipherShareRequestModel model) public async Task PutShare(string id, [FromBody]CipherShareRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; 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 || if(cipher == null || cipher.UserId != userId ||
!_currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId))) !_currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId)))
{ {
throw new NotFoundException(); 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); model.CollectionIds.Select(c => new Guid(c)), userId);
} }
@ -224,15 +226,7 @@ namespace Bit.Api.Controllers
[DisableFormValueModelBinding] [DisableFormValueModelBinding]
public async Task PostAttachment(string id) public async Task PostAttachment(string id)
{ {
if(!Request?.ContentType.Contains("multipart/") ?? true) ValidateAttachment();
{
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.");
}
var idGuid = new Guid(id); var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
@ -244,7 +238,28 @@ namespace Bit.Api.Controllers
await Request.GetFileAsync(async (stream, fileName) => 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); 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) 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 public class CipherShareRequestModel : IValidatableObject
{ {
[Required] [Required]
public IEnumerable<string> CollectionIds { get; set; } public IEnumerable<string> CollectionIds { get; set; }
[Required] [Required]
public CipherRequestModel Cipher { get; set; } public CipherAttachmentRequestModel Cipher { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {

View File

@ -1,11 +1,16 @@
using System.IO; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public interface IAttachmentStorageService public interface IAttachmentStorageService
{ {
Task UploadAttachmentAsync(Stream stream, string name); Task UploadNewAttachmentAsync(Stream stream, Guid cipherId, string attachmentId);
Task DeleteAttachmentAsync(string name); 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 SaveDetailsAsync(CipherDetails cipher, Guid savingUserId);
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId, Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, long requestLength, Guid savingUserId,
bool orgAdmin = false); 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 DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId); Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId);
Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId); Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId);
Task SaveFolderAsync(Folder folder); Task SaveFolderAsync(Folder folder);
Task DeleteFolderAsync(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 SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers, Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships); IEnumerable<KeyValuePair<int, int>> folderRelationships);

View File

@ -2,6 +2,7 @@
using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.Blob;
using System.IO; using System.IO;
using System;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -19,20 +20,80 @@ namespace Bit.Core.Services
_blobClient = storageAccount.CreateCloudBlobClient(); _blobClient = storageAccount.CreateCloudBlobClient();
} }
public async Task UploadAttachmentAsync(Stream stream, string name) public async Task UploadNewAttachmentAsync(Stream stream, Guid cipherId, string attachmentId)
{ {
await InitAsync(); await UploadAttachmentAsync(stream, $"{cipherId}/{attachmentId}");
var blob = _attachmentsContainer.GetBlockBlobReference(name);
await blob.UploadFromStreamAsync(stream);
} }
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(); 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(); 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() private async Task InitAsync()
{ {
if(_attachmentsContainer == null) if(_attachmentsContainer == null)

View File

@ -139,8 +139,7 @@ namespace Bit.Core.Services
} }
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
var storageId = $"{cipher.Id}/{attachmentId}"; await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher.Id, attachmentId);
await _attachmentStorageService.UploadAttachmentAsync(stream, storageId);
try try
{ {
@ -165,7 +164,7 @@ namespace Bit.Core.Services
catch catch
{ {
// Clean up since this is not transactional // Clean up since this is not transactional
await _attachmentStorageService.DeleteAttachmentAsync(storageId); await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentId);
throw; throw;
} }
@ -173,6 +172,29 @@ namespace Bit.Core.Services
await _pushService.PushSyncCipherUpdateAsync(cipher); 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) public async Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
{ {
if(!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId))) if(!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
@ -207,9 +229,7 @@ namespace Bit.Core.Services
await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentId); await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentId);
cipher.DeleteAttachment(attachmentId); cipher.DeleteAttachment(attachmentId);
await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentId);
var storedFilename = $"{cipher.Id}/{attachmentId}";
await _attachmentStorageService.DeleteAttachmentAsync(storedFilename);
// push // push
await _pushService.PushSyncCipherUpdateAsync(cipher); await _pushService.PushSyncCipherUpdateAsync(cipher);
@ -258,7 +278,8 @@ namespace Bit.Core.Services
await _pushService.PushSyncFolderDeleteAsync(folder); 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)) if(cipher.Id == default(Guid))
{ {
@ -275,12 +296,49 @@ namespace Bit.Core.Services
throw new NotFoundException(); throw new NotFoundException();
} }
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. // Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.
cipher.UserId = sharingUserId; cipher.UserId = sharingUserId;
cipher.OrganizationId = organizationId; cipher.OrganizationId = organizationId;
cipher.RevisionDate = DateTime.UtcNow; cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher, collectionIds); 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 // push
await _pushService.PushSyncCipherUpdateAsync(cipher); await _pushService.PushSyncCipherUpdateAsync(cipher);
} }

View File

@ -1,16 +1,37 @@
using System.IO; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class NoopAttachmentStorageService : IAttachmentStorageService public class NoopAttachmentStorageService : IAttachmentStorageService
{ {
public Task DeleteAttachmentAsync(string name) public Task CommitShareAttachmentAsync(Guid cipherId, Guid organizationId, string attachmentId)
{ {
return Task.FromResult(0); 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); return Task.FromResult(0);
} }

View File

@ -1,6 +1,7 @@
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Dapper; using Dapper;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
@ -242,5 +243,10 @@ namespace Bit.Core.Utilities
// Return formatted number with suffix // Return formatted number with suffix
return readable.ToString("0.## ") + 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 BEGIN
SET NOCOUNT ON 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 UPDATE
[dbo].[Cipher] [dbo].[Cipher]
SET SET
[UserId] = NULL, [UserId] = NULL,
[OrganizationId] = @OrganizationId, [OrganizationId] = @OrganizationId,
[Data] = @Data, [Data] = @Data,
[Attachments] = @Attachments,
[RevisionDate] = @RevisionDate [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 WHERE
[Id] = @Id [Id] = @Id
@ -66,12 +83,7 @@ BEGIN
WHERE WHERE
[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) [Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
IF @OrganizationId IS NOT NULL EXEC [dbo].[Organization_UpdateStorage] @OrganizationId, @Size
BEGIN EXEC [dbo].[User_UpdateStorage] @UserId, @SizeDec
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END END