mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
[PM-2682] Fix v0 attachments migration on share cipher with org (#3051)
* PM-2682 Fix v0 attachments migration on share cipher with org * PM-2682 Fix format * PM-2682 Fix tests recursion * Update src/Core/Vault/Models/Data/CipherAttachment.cs Co-authored-by: Matt Gibson <mgibson@bitwarden.com> --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
parent
a61290a3c8
commit
10782d55f3
@ -696,7 +696,7 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
await Request.GetFileAsync(async (stream, fileName, key) =>
|
await Request.GetFileAsync(async (stream, fileName, key) =>
|
||||||
{
|
{
|
||||||
await _cipherService.CreateAttachmentShareAsync(cipher, stream,
|
await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key,
|
||||||
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
|
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,10 @@ public class Cipher : ITableObject<Guid>, ICloneable
|
|||||||
foreach (var kvp in _attachmentData)
|
foreach (var kvp in _attachmentData)
|
||||||
{
|
{
|
||||||
kvp.Value.AttachmentId = kvp.Key;
|
kvp.Value.AttachmentId = kvp.Key;
|
||||||
|
if (kvp.Value.TempMetadata != null)
|
||||||
|
{
|
||||||
|
kvp.Value.TempMetadata.AttachmentId = kvp.Key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _attachmentData;
|
return _attachmentData;
|
||||||
}
|
}
|
||||||
|
@ -31,5 +31,10 @@ public class CipherAttachment
|
|||||||
// This is stored alongside metadata as an identifier. It does not need repeating in serialization
|
// This is stored alongside metadata as an identifier. It does not need repeating in serialization
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string AttachmentId { get; set; }
|
public string AttachmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporary metadata used to store original metadata on migrations from a user-owned attachment to an organization-owned one
|
||||||
|
/// </summary>
|
||||||
|
public MetaData TempMetadata { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ public interface ICipherService
|
|||||||
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId);
|
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId);
|
||||||
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
|
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||||
long requestLength, Guid savingUserId, bool orgAdmin = false);
|
long requestLength, Guid savingUserId, bool orgAdmin = false);
|
||||||
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, string attachmentId,
|
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,
|
||||||
Guid organizationShareId);
|
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, Guid? organizationId = null, bool orgAdmin = false);
|
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||||
Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
|
Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
|
||||||
|
@ -258,8 +258,8 @@ public class CipherService : ICipherService
|
|||||||
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
|
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength,
|
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||||
string attachmentId, Guid organizationId)
|
long requestLength, string attachmentId, Guid organizationId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -296,8 +296,28 @@ public class CipherService : ICipherService
|
|||||||
throw new BadRequestException($"Cipher does not own specified attachment");
|
throw new BadRequestException($"Cipher does not own specified attachment");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var originalAttachmentMetadata = attachments[attachmentId];
|
||||||
|
|
||||||
|
if (originalAttachmentMetadata.TempMetadata != null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Another process is trying to migrate this attachment");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone metadata to be modified and saved into the TempMetadata,
|
||||||
|
// we cannot change the metadata here directly because if the subsequent endpoint fails
|
||||||
|
// to be called, then the metadata would stay corrupted.
|
||||||
|
var attachmentMetadata = CoreHelpers.CloneObject(originalAttachmentMetadata);
|
||||||
|
attachmentMetadata.AttachmentId = originalAttachmentMetadata.AttachmentId;
|
||||||
|
originalAttachmentMetadata.TempMetadata = attachmentMetadata;
|
||||||
|
|
||||||
|
if (key != null)
|
||||||
|
{
|
||||||
|
attachmentMetadata.Key = key;
|
||||||
|
attachmentMetadata.FileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
await _attachmentStorageService.UploadShareAttachmentAsync(stream, cipher.Id, organizationId,
|
await _attachmentStorageService.UploadShareAttachmentAsync(stream, cipher.Id, organizationId,
|
||||||
attachments[attachmentId]);
|
attachmentMetadata);
|
||||||
|
|
||||||
// Previous call may alter metadata
|
// Previous call may alter metadata
|
||||||
var updatedAttachment = new CipherAttachment
|
var updatedAttachment = new CipherAttachment
|
||||||
@ -306,7 +326,7 @@ public class CipherService : ICipherService
|
|||||||
UserId = cipher.UserId,
|
UserId = cipher.UserId,
|
||||||
OrganizationId = cipher.OrganizationId,
|
OrganizationId = cipher.OrganizationId,
|
||||||
AttachmentId = attachmentId,
|
AttachmentId = attachmentId,
|
||||||
AttachmentData = JsonSerializer.Serialize(attachments[attachmentId])
|
AttachmentData = JsonSerializer.Serialize(originalAttachmentMetadata)
|
||||||
};
|
};
|
||||||
|
|
||||||
await _cipherRepository.UpdateAttachmentAsync(updatedAttachment);
|
await _cipherRepository.UpdateAttachmentAsync(updatedAttachment);
|
||||||
@ -489,10 +509,10 @@ public class CipherService : ICipherService
|
|||||||
IEnumerable<Guid> collectionIds, Guid sharingUserId, DateTime? lastKnownRevisionDate)
|
IEnumerable<Guid> collectionIds, Guid sharingUserId, DateTime? lastKnownRevisionDate)
|
||||||
{
|
{
|
||||||
var attachments = cipher.GetAttachments();
|
var attachments = cipher.GetAttachments();
|
||||||
var hasOldAttachments = attachments?.Any(a => a.Key == null) ?? false;
|
var hasOldAttachments = attachments?.Values?.Any(a => a.Key == null) ?? false;
|
||||||
var updatedCipher = false;
|
var updatedCipher = false;
|
||||||
var migratedAttachments = false;
|
var migratedAttachments = false;
|
||||||
var originalAttachments = CoreHelpers.CloneObject(attachments);
|
var originalAttachments = CoreHelpers.CloneObject(originalCipher.GetAttachments());
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -502,6 +522,21 @@ public class CipherService : ICipherService
|
|||||||
cipher.UserId = sharingUserId;
|
cipher.UserId = sharingUserId;
|
||||||
cipher.OrganizationId = organizationId;
|
cipher.OrganizationId = organizationId;
|
||||||
cipher.RevisionDate = DateTime.UtcNow;
|
cipher.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (hasOldAttachments)
|
||||||
|
{
|
||||||
|
var attachmentsWithUpdatedMetadata = originalCipher.GetAttachments();
|
||||||
|
var attachmentsToUpdateMetadata = CoreHelpers.CloneObject(attachments);
|
||||||
|
foreach (var updatedMetadata in attachmentsWithUpdatedMetadata.Where(a => a.Value?.TempMetadata != null))
|
||||||
|
{
|
||||||
|
if (attachmentsToUpdateMetadata.ContainsKey(updatedMetadata.Key))
|
||||||
|
{
|
||||||
|
attachmentsToUpdateMetadata[updatedMetadata.Key] = updatedMetadata.Value.TempMetadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cipher.SetAttachments(attachmentsToUpdateMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
if (!await _cipherRepository.ReplaceAsync(cipher, collectionIds))
|
if (!await _cipherRepository.ReplaceAsync(cipher, collectionIds))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Unable to save.");
|
throw new BadRequestException("Unable to save.");
|
||||||
@ -513,25 +548,36 @@ public class CipherService : ICipherService
|
|||||||
if (hasOldAttachments)
|
if (hasOldAttachments)
|
||||||
{
|
{
|
||||||
// migrate old attachments
|
// migrate old attachments
|
||||||
foreach (var attachment in attachments.Where(a => a.Key == null))
|
foreach (var attachment in attachments.Values.Where(a => a.TempMetadata != null).Select(a => a.TempMetadata))
|
||||||
{
|
{
|
||||||
await _attachmentStorageService.StartShareAttachmentAsync(cipher.Id, organizationId,
|
await _attachmentStorageService.StartShareAttachmentAsync(cipher.Id, organizationId,
|
||||||
attachment.Value);
|
attachment);
|
||||||
migratedAttachments = true;
|
migratedAttachments = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// commit attachment migration
|
// commit attachment migration
|
||||||
await _attachmentStorageService.CleanupAsync(cipher.Id);
|
await _attachmentStorageService.CleanupAsync(cipher.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// push
|
|
||||||
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// roll everything back
|
// roll everything back
|
||||||
if (updatedCipher)
|
if (updatedCipher)
|
||||||
{
|
{
|
||||||
|
if (hasOldAttachments)
|
||||||
|
{
|
||||||
|
foreach (var item in originalAttachments)
|
||||||
|
{
|
||||||
|
item.Value.TempMetadata = null;
|
||||||
|
}
|
||||||
|
originalCipher.SetAttachments(originalAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentCollectionsForCipher = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(sharingUserId, originalCipher.Id);
|
||||||
|
var currentCollectionIdsForCipher = currentCollectionsForCipher.Select(c => c.CollectionId).ToList();
|
||||||
|
currentCollectionIdsForCipher.RemoveAll(id => collectionIds.Contains(id));
|
||||||
|
|
||||||
|
await _collectionCipherRepository.UpdateCollectionsAsync(originalCipher.Id, sharingUserId, currentCollectionIdsForCipher);
|
||||||
await _cipherRepository.ReplaceAsync(originalCipher);
|
await _cipherRepository.ReplaceAsync(originalCipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,7 +592,7 @@ public class CipherService : ICipherService
|
|||||||
await _organizationRepository.UpdateStorageAsync(organizationId);
|
await _organizationRepository.UpdateStorageAsync(organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var attachment in attachments.Where(a => a.Key == null))
|
foreach (var attachment in attachments.Where(a => a.Value.Key == null))
|
||||||
{
|
{
|
||||||
await _attachmentStorageService.RollbackShareAttachmentAsync(cipher.Id, organizationId,
|
await _attachmentStorageService.RollbackShareAttachmentAsync(cipher.Id, organizationId,
|
||||||
attachment.Value, originalAttachments[attachment.Key].ContainerName);
|
attachment.Value, originalAttachments[attachment.Key].ContainerName);
|
||||||
@ -555,6 +601,9 @@ public class CipherService : ICipherService
|
|||||||
await _attachmentStorageService.CleanupAsync(cipher.Id);
|
await _attachmentStorageService.CleanupAsync(cipher.Id);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// push
|
||||||
|
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos,
|
public async Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos,
|
||||||
|
@ -14,6 +14,10 @@ public class MetaData : ICustomization
|
|||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
{
|
{
|
||||||
fixture.Customize<CipherAttachment.MetaData>(composer => ComposerAction(fixture, composer));
|
fixture.Customize<CipherAttachment.MetaData>(composer => ComposerAction(fixture, composer));
|
||||||
|
fixture.Behaviors.OfType<ThrowingRecursionBehavior>()
|
||||||
|
.ToList()
|
||||||
|
.ForEach(b => fixture.Behaviors.Remove(b));
|
||||||
|
fixture.Behaviors.Add(new OmitOnRecursionBehavior(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
@ -44,7 +46,12 @@ public class CipherServiceTests
|
|||||||
Organization organization, List<Guid> collectionIds)
|
Organization organization, List<Guid> collectionIds)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[Guid.NewGuid().ToString()] = new CipherAttachment.MetaData { }
|
||||||
|
});
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
() => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
@ -105,11 +112,438 @@ public class CipherServiceTests
|
|||||||
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true);
|
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[Guid.NewGuid().ToString()] = new CipherAttachment.MetaData { }
|
||||||
|
});
|
||||||
await sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
await sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
lastKnownRevisionDate);
|
lastKnownRevisionDate);
|
||||||
await cipherRepository.Received(1).ReplaceAsync(cipher, collectionIds);
|
await cipherRepository.Received(1).ReplaceAsync(cipher, collectionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData("Correct Time")]
|
||||||
|
public async Task ShareAsync_FailReplace_Throws(string revisionDateString,
|
||||||
|
SutProvider<CipherService> sutProvider, Cipher cipher, Organization organization, List<Guid> collectionIds)
|
||||||
|
{
|
||||||
|
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||||
|
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
|
||||||
|
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(false);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[Guid.NewGuid().ToString()] = new CipherAttachment.MetaData { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
|
lastKnownRevisionDate));
|
||||||
|
Assert.Contains("Unable to save", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData("Correct Time")]
|
||||||
|
public async Task ShareAsync_HasV0Attachments_ReplaceAttachmentMetadataWithNewOneBeforeSavingCipher(string revisionDateString,
|
||||||
|
SutProvider<CipherService> sutProvider, Cipher cipher, Organization organization, List<Guid> collectionIds)
|
||||||
|
{
|
||||||
|
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||||
|
var originalCipher = CoreHelpers.CloneObject(cipher);
|
||||||
|
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
|
||||||
|
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var pushNotificationService = sutProvider.GetDependency<IPushNotificationService>();
|
||||||
|
|
||||||
|
var v0AttachmentId = Guid.NewGuid().ToString();
|
||||||
|
var anotherAttachmentId = Guid.NewGuid().ToString();
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted"
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
originalCipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await sutProvider.Sut.ShareAsync(originalCipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
|
lastKnownRevisionDate);
|
||||||
|
|
||||||
|
await cipherRepository.Received().ReplaceAsync(Arg.Is<Cipher>(c =>
|
||||||
|
c.GetAttachments()[v0AttachmentId].Key == "NewAttachmentKey"
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId].FileName == "AFileNameRe-EncryptedWithOrgKey")
|
||||||
|
, collectionIds);
|
||||||
|
|
||||||
|
await pushNotificationService.Received(1).PushSyncCipherUpdateAsync(cipher, collectionIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData("Correct Time")]
|
||||||
|
public async Task ShareAsync_HasV0Attachments_StartSharingThoseAttachments(string revisionDateString,
|
||||||
|
SutProvider<CipherService> sutProvider, Cipher cipher, Organization organization, List<Guid> collectionIds)
|
||||||
|
{
|
||||||
|
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||||
|
var originalCipher = CoreHelpers.CloneObject(cipher);
|
||||||
|
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
|
||||||
|
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var attachmentStorageService = sutProvider.GetDependency<IAttachmentStorageService>();
|
||||||
|
|
||||||
|
var v0AttachmentId = Guid.NewGuid().ToString();
|
||||||
|
var anotherAttachmentId = Guid.NewGuid().ToString();
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
originalCipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await sutProvider.Sut.ShareAsync(originalCipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
|
lastKnownRevisionDate);
|
||||||
|
|
||||||
|
await attachmentStorageService.Received().StartShareAttachmentAsync(cipher.Id,
|
||||||
|
organization.Id,
|
||||||
|
Arg.Is<CipherAttachment.MetaData>(m => m.Key == "NewAttachmentKey" && m.FileName == "AFileNameRe-EncryptedWithOrgKey"));
|
||||||
|
|
||||||
|
await attachmentStorageService.Received(0).StartShareAttachmentAsync(cipher.Id,
|
||||||
|
organization.Id,
|
||||||
|
Arg.Is<CipherAttachment.MetaData>(m => m.Key == "AwesomeKey" && m.FileName == "AnotherFilename"));
|
||||||
|
|
||||||
|
await attachmentStorageService.Received().CleanupAsync(cipher.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData("Correct Time")]
|
||||||
|
public async Task ShareAsync_HasV0Attachments_StartShareThrows_PerformsRollback_Rethrows(string revisionDateString,
|
||||||
|
SutProvider<CipherService> sutProvider, Cipher cipher, Organization organization, List<Guid> collectionIds)
|
||||||
|
{
|
||||||
|
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||||
|
var originalCipher = CoreHelpers.CloneObject(cipher);
|
||||||
|
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
|
||||||
|
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var attachmentStorageService = sutProvider.GetDependency<IAttachmentStorageService>();
|
||||||
|
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||||
|
collectionCipherRepository.GetManyByUserIdCipherIdAsync(cipher.UserId.Value, cipher.Id).Returns(
|
||||||
|
Task.FromResult((ICollection<CollectionCipher>)new List<CollectionCipher>
|
||||||
|
{
|
||||||
|
new CollectionCipher
|
||||||
|
{
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
CollectionId = collectionIds[0]
|
||||||
|
},
|
||||||
|
new CollectionCipher
|
||||||
|
{
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
CollectionId = Guid.NewGuid()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
var v0AttachmentId = Guid.NewGuid().ToString();
|
||||||
|
var anotherAttachmentId = Guid.NewGuid().ToString();
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
originalCipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentStorageService.StartShareAttachmentAsync(cipher.Id,
|
||||||
|
organization.Id,
|
||||||
|
Arg.Is<CipherAttachment.MetaData>(m => m.AttachmentId == v0AttachmentId))
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("ex from StartShareAttachmentAsync")));
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
|
lastKnownRevisionDate));
|
||||||
|
Assert.Contains("ex from StartShareAttachmentAsync", exception.Message);
|
||||||
|
|
||||||
|
await collectionCipherRepository.Received().UpdateCollectionsAsync(cipher.Id, cipher.UserId.Value,
|
||||||
|
Arg.Is<List<Guid>>(ids => ids.Count == 1 && ids[0] != collectionIds[0]));
|
||||||
|
|
||||||
|
await cipherRepository.Received().ReplaceAsync(Arg.Is<Cipher>(c =>
|
||||||
|
c.GetAttachments()[v0AttachmentId].Key == null
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId].FileName == "AFileNameEncrypted"
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId].TempMetadata == null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData("Correct Time")]
|
||||||
|
public async Task ShareAsync_HasSeveralV0Attachments_StartShareThrowsOnSecondOne_PerformsRollback_Rethrows(string revisionDateString,
|
||||||
|
SutProvider<CipherService> sutProvider, Cipher cipher, Organization organization, List<Guid> collectionIds)
|
||||||
|
{
|
||||||
|
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||||
|
var originalCipher = CoreHelpers.CloneObject(cipher);
|
||||||
|
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
|
||||||
|
cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true);
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var attachmentStorageService = sutProvider.GetDependency<IAttachmentStorageService>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||||
|
collectionCipherRepository.GetManyByUserIdCipherIdAsync(cipher.UserId.Value, cipher.Id).Returns(
|
||||||
|
Task.FromResult((ICollection<CollectionCipher>)new List<CollectionCipher>
|
||||||
|
{
|
||||||
|
new CollectionCipher
|
||||||
|
{
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
CollectionId = collectionIds[0]
|
||||||
|
},
|
||||||
|
new CollectionCipher
|
||||||
|
{
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
CollectionId = Guid.NewGuid()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
var v0AttachmentId1 = Guid.NewGuid().ToString();
|
||||||
|
var v0AttachmentId2 = Guid.NewGuid().ToString();
|
||||||
|
var anotherAttachmentId = Guid.NewGuid().ToString();
|
||||||
|
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId1] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId1,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId1,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[v0AttachmentId2] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId2,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted2",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId2,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey2",
|
||||||
|
Key = "NewAttachmentKey2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
originalCipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||||
|
{
|
||||||
|
[v0AttachmentId1] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId1,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId1,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey",
|
||||||
|
Key = "NewAttachmentKey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[v0AttachmentId2] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId2,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameEncrypted2",
|
||||||
|
TempMetadata = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = v0AttachmentId2,
|
||||||
|
ContainerName = "attachments",
|
||||||
|
FileName = "AFileNameRe-EncryptedWithOrgKey2",
|
||||||
|
Key = "NewAttachmentKey2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anotherAttachmentId] = new CipherAttachment.MetaData
|
||||||
|
{
|
||||||
|
AttachmentId = anotherAttachmentId,
|
||||||
|
Key = "AwesomeKey",
|
||||||
|
FileName = "AnotherFilename",
|
||||||
|
ContainerName = "attachments",
|
||||||
|
Size = 300,
|
||||||
|
Validated = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentStorageService.StartShareAttachmentAsync(cipher.Id,
|
||||||
|
organization.Id,
|
||||||
|
Arg.Is<CipherAttachment.MetaData>(m => m.AttachmentId == v0AttachmentId2))
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("ex from StartShareAttachmentAsync")));
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value,
|
||||||
|
lastKnownRevisionDate));
|
||||||
|
Assert.Contains("ex from StartShareAttachmentAsync", exception.Message);
|
||||||
|
|
||||||
|
await collectionCipherRepository.Received().UpdateCollectionsAsync(cipher.Id, cipher.UserId.Value,
|
||||||
|
Arg.Is<List<Guid>>(ids => ids.Count == 1 && ids[0] != collectionIds[0]));
|
||||||
|
|
||||||
|
await cipherRepository.Received().ReplaceAsync(Arg.Is<Cipher>(c =>
|
||||||
|
c.GetAttachments()[v0AttachmentId1].Key == null
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId1].FileName == "AFileNameEncrypted"
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId1].TempMetadata == null)
|
||||||
|
);
|
||||||
|
|
||||||
|
await cipherRepository.Received().ReplaceAsync(Arg.Is<Cipher>(c =>
|
||||||
|
c.GetAttachments()[v0AttachmentId2].Key == null
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId2].FileName == "AFileNameEncrypted2"
|
||||||
|
&&
|
||||||
|
c.GetAttachments()[v0AttachmentId2].TempMetadata == null)
|
||||||
|
);
|
||||||
|
|
||||||
|
await userRepository.UpdateStorageAsync(cipher.UserId.Value);
|
||||||
|
await organizationRepository.UpdateStorageAsync(organization.Id);
|
||||||
|
|
||||||
|
await attachmentStorageService.Received().RollbackShareAttachmentAsync(cipher.Id, organization.Id,
|
||||||
|
Arg.Is<CipherAttachment.MetaData>(m => m.AttachmentId == v0AttachmentId1), Arg.Any<string>());
|
||||||
|
|
||||||
|
await attachmentStorageService.Received().CleanupAsync(cipher.Id);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData("")]
|
[BitAutoData("")]
|
||||||
[BitAutoData("Correct Time")]
|
[BitAutoData("Correct Time")]
|
||||||
|
Loading…
Reference in New Issue
Block a user