mirror of
https://github.com/bitwarden/server.git
synced 2025-06-14 06:50:47 -05:00
[PM-22204] - update cipher/share endpoint to return revision date (#5900)
* return ciper response model in cipher share endpoint. add tests * return dict instead of full cipher response. adjust specs * rename vars * rename vars * rename vars * reinsert braces * add specs * return CipherMiniResponseModel
This commit is contained in:
parent
8c14630481
commit
2c4393cc16
@ -1064,7 +1064,7 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
[HttpPut("share")]
|
[HttpPut("share")]
|
||||||
[HttpPost("share")]
|
[HttpPost("share")]
|
||||||
public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model)
|
public async Task<CipherMiniResponseModel[]> PutShareMany([FromBody] CipherBulkShareRequestModel model)
|
||||||
{
|
{
|
||||||
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
|
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
|
||||||
if (!await _currentContext.OrganizationUser(organizationId))
|
if (!await _currentContext.OrganizationUser(organizationId))
|
||||||
@ -1073,38 +1073,40 @@ public class CiphersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
|
||||||
var ciphersDict = ciphers.ToDictionary(c => c.Id);
|
var ciphersDict = ciphers.ToDictionary(c => c.Id);
|
||||||
|
|
||||||
// Validate the model was encrypted for the posting user
|
// Validate the model was encrypted for the posting user
|
||||||
foreach (var cipher in model.Ciphers)
|
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?)>();
|
var shareCiphers = new List<(Cipher, DateTime?)>();
|
||||||
foreach (var cipher in model.Ciphers)
|
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);
|
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
||||||
|
|
||||||
shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate));
|
shareCiphers.Add(((Cipher)existingCipher, cipher.LastKnownRevisionDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cipherService.ShareManyAsync(shareCiphers, organizationId,
|
var updated = await _cipherService.ShareManyAsync(
|
||||||
model.CollectionIds.Select(c => new Guid(c)), userId);
|
shareCiphers,
|
||||||
|
organizationId,
|
||||||
|
model.CollectionIds.Select(Guid.Parse),
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
return updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, false)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("purge")]
|
[HttpPost("purge")]
|
||||||
|
@ -24,7 +24,7 @@ public interface ICipherService
|
|||||||
Task DeleteFolderAsync(Folder folder);
|
Task DeleteFolderAsync(Folder folder);
|
||||||
Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds,
|
Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||||
Guid userId, DateTime? lastKnownRevisionDate);
|
Guid userId, DateTime? lastKnownRevisionDate);
|
||||||
Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,
|
Task<IEnumerable<Cipher>> ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,
|
||||||
IEnumerable<Guid> collectionIds, Guid sharingUserId);
|
IEnumerable<Guid> collectionIds, Guid sharingUserId);
|
||||||
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
|
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
|
||||||
Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
|
Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
|
||||||
|
@ -628,7 +628,7 @@ public class CipherService : ICipherService
|
|||||||
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
|
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos,
|
public async Task<IEnumerable<Cipher>> ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos,
|
||||||
Guid organizationId, IEnumerable<Guid> collectionIds, Guid sharingUserId)
|
Guid organizationId, IEnumerable<Guid> collectionIds, Guid sharingUserId)
|
||||||
{
|
{
|
||||||
var cipherIds = new List<Guid>();
|
var cipherIds = new List<Guid>();
|
||||||
@ -655,6 +655,7 @@ public class CipherService : ICipherService
|
|||||||
|
|
||||||
// push
|
// push
|
||||||
await _pushService.PushSyncCiphersAsync(sharingUserId);
|
await _pushService.PushSyncCiphersAsync(sharingUserId);
|
||||||
|
return cipherInfos.Select(c => c.cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId,
|
public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId,
|
||||||
|
@ -1741,8 +1741,180 @@ public class CiphersControllerTests
|
|||||||
{
|
{
|
||||||
model.OrganizationId = Guid.NewGuid();
|
model.OrganizationId = Guid.NewGuid();
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(model.OrganizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.ProviderUserForOrgAsync(new Guid(model.OrganizationId.ToString()))
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreManyAdmin(model));
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.PutRestoreManyAdmin(model)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PutShareMany_ShouldShareCiphersAndReturnRevisionDateMap(
|
||||||
|
User user,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
SutProvider<CiphersController> 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<CipherDetails> { 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<ICurrentContext>()
|
||||||
|
.OrganizationUser(organizationId)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(Task.FromResult(user));
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(default!)
|
||||||
|
.ReturnsForAnyArgs(userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||||
|
.Returns(Task.FromResult((ICollection<CipherDetails>)preloadedDetails));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherService>()
|
||||||
|
.ShareManyAsync(
|
||||||
|
Arg.Any<IEnumerable<(Cipher, DateTime?)>>(),
|
||||||
|
organizationId,
|
||||||
|
Arg.Any<IEnumerable<Guid>>(),
|
||||||
|
userId
|
||||||
|
)
|
||||||
|
.Returns(Task.FromResult<IEnumerable<Cipher>>(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<ICipherService>()
|
||||||
|
.Received(1)
|
||||||
|
.ShareManyAsync(
|
||||||
|
Arg.Is<IEnumerable<(Cipher, DateTime?)>>(list =>
|
||||||
|
list.Select(x => x.Item1.Id).OrderBy(id => id)
|
||||||
|
.SequenceEqual(new[] { detail1.Id, detail2.Id }.OrderBy(id => id))
|
||||||
|
),
|
||||||
|
organizationId,
|
||||||
|
Arg.Any<IEnumerable<Guid>>(),
|
||||||
|
userId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutShareMany_OrganizationUserFalse_ThrowsNotFound(
|
||||||
|
CipherBulkShareRequestModel model,
|
||||||
|
SutProvider<CiphersController> sut)
|
||||||
|
{
|
||||||
|
model.Ciphers = new[] {
|
||||||
|
new CipherWithIdRequestModel { Id = Guid.NewGuid(), OrganizationId = Guid.NewGuid().ToString() }
|
||||||
|
};
|
||||||
|
sut.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationUser(Arg.Any<Guid>())
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sut.Sut.PutShareMany(model));
|
||||||
|
}
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutShareMany_CipherNotOwned_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
CipherWithIdRequestModel request,
|
||||||
|
SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
request.EncryptedFor = userId;
|
||||||
|
var model = new CipherBulkShareRequestModel
|
||||||
|
{
|
||||||
|
Ciphers = new[] { request },
|
||||||
|
CollectionIds = new[] { Guid.NewGuid().ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationUser(organizationId)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(default)
|
||||||
|
.ReturnsForAnyArgs(userId);
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||||
|
.Returns(Task.FromResult((ICollection<CipherDetails>)new List<CipherDetails>()));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.PutShareMany(model)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutShareMany_EncryptedForWrongUser_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
CipherWithIdRequestModel request,
|
||||||
|
SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
request.EncryptedFor = Guid.NewGuid(); // not equal to userId
|
||||||
|
var model = new CipherBulkShareRequestModel
|
||||||
|
{
|
||||||
|
Ciphers = new[] { request },
|
||||||
|
CollectionIds = new[] { Guid.NewGuid().ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationUser(organizationId)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(default)
|
||||||
|
.ReturnsForAnyArgs(userId);
|
||||||
|
|
||||||
|
var existing = new CipherDetails { Id = request.Id.Value };
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||||
|
.Returns(Task.FromResult((ICollection<CipherDetails>)(new[] { existing })));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.PutShareMany(model)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user