diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 251362589e..d603c75505 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1064,7 +1064,7 @@ public class CiphersController : Controller [HttpPut("share")] [HttpPost("share")] - public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model) + public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model) { var organizationId = new Guid(model.Ciphers.First().OrganizationId); if (!await _currentContext.OrganizationUser(organizationId)) @@ -1073,38 +1073,40 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphersDict = ciphers.ToDictionary(c => c.Id); // Validate the model was encrypted for the posting user foreach (var cipher in model.Ciphers) { - if (cipher.EncryptedFor != null) + if (cipher.EncryptedFor.HasValue && cipher.EncryptedFor.Value != userId) { - if (cipher.EncryptedFor != userId) - { - throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); - } + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } var shareCiphers = new List<(Cipher, DateTime?)>(); foreach (var cipher in model.Ciphers) { - if (!ciphersDict.ContainsKey(cipher.Id.Value)) + if (!ciphersDict.TryGetValue(cipher.Id.Value, out var existingCipher)) { - throw new BadRequestException("Trying to move ciphers that you do not own."); + throw new BadRequestException("Trying to share ciphers that you do not own."); } - var existingCipher = ciphersDict[cipher.Id.Value]; - ValidateClientVersionForFido2CredentialSupport(existingCipher); - shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate)); + shareCiphers.Add(((Cipher)existingCipher, cipher.LastKnownRevisionDate)); } - await _cipherService.ShareManyAsync(shareCiphers, organizationId, - model.CollectionIds.Select(c => new Guid(c)), userId); + var updated = await _cipherService.ShareManyAsync( + shareCiphers, + organizationId, + model.CollectionIds.Select(Guid.Parse), + userId + ); + + return updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, false)).ToArray(); } [HttpPost("purge")] diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 7eeb6d2463..3721f592ba 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -24,7 +24,7 @@ public interface ICipherService Task DeleteFolderAsync(Folder folder); Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable collectionIds, Guid userId, DateTime? lastKnownRevisionDate); - Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, + Task> ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId); Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, bool orgAdmin); Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index e170cc8769..605061a98e 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -628,7 +628,7 @@ public class CipherService : ICipherService 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, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId) { var cipherIds = new List(); @@ -655,6 +655,7 @@ public class CipherService : ICipherService // push await _pushService.PushSyncCiphersAsync(sharingUserId); + return cipherInfos.Select(c => c.cipher); } public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index e4643f3185..14f795e571 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1741,8 +1741,180 @@ public class CiphersControllerTests { model.OrganizationId = Guid.NewGuid(); - sutProvider.GetDependency().ProviderUserForOrgAsync(model.OrganizationId).Returns(true); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(new Guid(model.OrganizationId.ToString())) + .Returns(Task.FromResult(true)); - await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreManyAdmin(model)); + await Assert.ThrowsAsync( + () => sutProvider.Sut.PutRestoreManyAdmin(model) + ); } + + [Theory] + [BitAutoData] + public async Task PutShareMany_ShouldShareCiphersAndReturnRevisionDateMap( + User user, + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + var oldDate1 = DateTime.UtcNow.AddDays(-1); + var oldDate2 = DateTime.UtcNow.AddDays(-2); + var detail1 = new CipherDetails + { + Id = Guid.NewGuid(), + UserId = userId, + OrganizationId = organizationId, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()), + RevisionDate = oldDate1 + }; + var detail2 = new CipherDetails + { + Id = Guid.NewGuid(), + UserId = userId, + OrganizationId = organizationId, + Type = CipherType.SecureNote, + Data = JsonSerializer.Serialize(new CipherSecureNoteData()), + RevisionDate = oldDate2 + }; + var preloadedDetails = new List { detail1, detail2 }; + + var newDate1 = oldDate1.AddMinutes(5); + var newDate2 = oldDate2.AddMinutes(5); + var updatedCipher1 = new Cipher { Id = detail1.Id, RevisionDate = newDate1, Type = detail1.Type, Data = detail1.Data }; + var updatedCipher2 = new Cipher { Id = detail2.Id, RevisionDate = newDate2, Type = detail2.Type, Data = detail2.Data }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(Task.FromResult(user)); + sutProvider.GetDependency() + .GetProperUserId(default!) + .ReturnsForAnyArgs(userId); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)preloadedDetails)); + + sutProvider.GetDependency() + .ShareManyAsync( + Arg.Any>(), + organizationId, + Arg.Any>(), + userId + ) + .Returns(Task.FromResult>(new[] { updatedCipher1, updatedCipher2 })); + + var cipherRequests = preloadedDetails.Select(d => new CipherWithIdRequestModel + { + Id = d.Id, + OrganizationId = d.OrganizationId!.Value.ToString(), + LastKnownRevisionDate = d.RevisionDate, + Type = d.Type + }).ToList(); + + var model = new CipherBulkShareRequestModel + { + Ciphers = cipherRequests, + CollectionIds = new[] { Guid.NewGuid().ToString() } + }; + + var result = await sutProvider.Sut.PutShareMany(model); + + Assert.Equal(2, result.Length); + var revisionDates = result.Select(r => r.RevisionDate).ToList(); + Assert.Contains(newDate1, revisionDates); + Assert.Contains(newDate2, revisionDates); + + await sutProvider.GetDependency() + .Received(1) + .ShareManyAsync( + Arg.Is>(list => + list.Select(x => x.Item1.Id).OrderBy(id => id) + .SequenceEqual(new[] { detail1.Id, detail2.Id }.OrderBy(id => id)) + ), + organizationId, + Arg.Any>(), + userId + ); + } + + [Theory, BitAutoData] + public async Task PutShareMany_OrganizationUserFalse_ThrowsNotFound( + CipherBulkShareRequestModel model, + SutProvider sut) + { + model.Ciphers = new[] { + new CipherWithIdRequestModel { Id = Guid.NewGuid(), OrganizationId = Guid.NewGuid().ToString() } + }; + sut.GetDependency() + .OrganizationUser(Arg.Any()) + .Returns(Task.FromResult(false)); + + await Assert.ThrowsAsync(() => sut.Sut.PutShareMany(model)); + } + [Theory, BitAutoData] + public async Task PutShareMany_CipherNotOwned_ThrowsNotFoundException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + request.EncryptedFor = userId; + var model = new CipherBulkShareRequestModel + { + Ciphers = new[] { request }, + CollectionIds = new[] { Guid.NewGuid().ToString() } + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)new List())); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + } + + [Theory, BitAutoData] + public async Task PutShareMany_EncryptedForWrongUser_ThrowsNotFoundException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + request.EncryptedFor = Guid.NewGuid(); // not equal to userId + var model = new CipherBulkShareRequestModel + { + Ciphers = new[] { request }, + CollectionIds = new[] { Guid.NewGuid().ToString() } + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + + var existing = new CipherDetails { Id = request.Id.Value }; + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)(new[] { existing }))); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + } + } +