mirror of
https://github.com/bitwarden/server.git
synced 2024-11-29 13:25:17 +01:00
share login with attachments
This commit is contained in:
parent
fbc189544b
commit
f8c749bab5
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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,12 +296,49 @@ namespace Bit.Core.Services
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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].[Organization_UpdateStorage] @OrganizationId, @Size
|
||||
EXEC [dbo].[User_UpdateStorage] @UserId, @SizeDec
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
|
||||
END
|
||||
ELSE IF @UserId IS NOT NULL
|
||||
BEGIN
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||
END
|
||||
END
|
Loading…
Reference in New Issue
Block a user