mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[AC-2447] Update PutCollection to return Unavailable cipher when last Can Manage Access is Removed (#4074)
* update CiphersController to return a unavailable value to the client so it can determine if the user removed the final Can Manage access of an item
This commit is contained in:
parent
f2242186d0
commit
87865e8f5c
@ -619,9 +619,39 @@ public class CiphersController : Controller
|
||||
|
||||
var updatedCipher = await GetByIdAsync(id, userId);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
||||
|
||||
return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections_v2")]
|
||||
[HttpPost("{id}/collections_v2")]
|
||||
public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _cipherService.SaveCollectionsAsync(cipher,
|
||||
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
||||
|
||||
var updatedCipher = await GetByIdAsync(id, userId);
|
||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
||||
// If a user removes the last Can Manage access of a cipher, the "updatedCipher" will return null
|
||||
// We will be returning an "Unavailable" property so the client knows the user can no longer access this
|
||||
var response = new OptionalCipherDetailsResponseModel()
|
||||
{
|
||||
Unavailable = updatedCipher is null,
|
||||
Cipher = updatedCipher is null
|
||||
? null
|
||||
: new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers)
|
||||
};
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections-admin")]
|
||||
[HttpPost("{id}/collections-admin")]
|
||||
public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
||||
|
@ -0,0 +1,13 @@
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
public class OptionalCipherDetailsResponseModel : ResponseModel
|
||||
{
|
||||
public bool Unavailable { get; set; }
|
||||
|
||||
public CipherDetailsResponseModel? Cipher { get; set; }
|
||||
|
||||
public OptionalCipherDetailsResponseModel()
|
||||
: base("optionalCipherDetails")
|
||||
{ }
|
||||
}
|
@ -3,9 +3,11 @@ using Bit.Api.Vault.Controllers;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
@ -14,7 +16,9 @@ using Bit.Core.Vault.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
using CipherType = Bit.Core.Vault.Enums.CipherType;
|
||||
|
||||
namespace Bit.Api.Test.Controllers;
|
||||
|
||||
@ -50,6 +54,92 @@ public class CiphersControllerTests
|
||||
Assert.Equal(isFavorite, result.Favorite);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, Guid userId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).Returns(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(Guid.NewGuid()).Returns(false);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsNull();
|
||||
|
||||
var requestAction = async () => await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(requestAction);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutCollections_vNextShouldSaveUpdatedCipher(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
SetupUserAndOrgMocks(id, userId, sutProvider);
|
||||
var cipherDetails = CreateCipherDetailsMock(id, userId);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsForAnyArgs(cipherDetails);
|
||||
|
||||
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
|
||||
var cipherService = sutProvider.GetDependency<ICipherService>();
|
||||
|
||||
await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||
|
||||
await cipherService.ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableFalse(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
SetupUserAndOrgMocks(id, userId, sutProvider);
|
||||
var cipherDetails = CreateCipherDetailsMock(id, userId);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsForAnyArgs(cipherDetails);
|
||||
|
||||
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
|
||||
|
||||
var result = await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||
|
||||
Assert.IsType<OptionalCipherDetailsResponseModel>(result);
|
||||
Assert.False(result.Unavailable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableTrue(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
SetupUserAndOrgMocks(id, userId, sutProvider);
|
||||
var cipherDetails = CreateCipherDetailsMock(id, userId);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsForAnyArgs(cipherDetails, [(CipherDetails)null]);
|
||||
|
||||
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
|
||||
|
||||
var result = await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||
|
||||
Assert.IsType<OptionalCipherDetailsResponseModel>(result);
|
||||
Assert.True(result.Unavailable);
|
||||
}
|
||||
|
||||
private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns(new List<CollectionCipher>());
|
||||
}
|
||||
|
||||
private CipherDetails CreateCipherDetailsMock(Guid id, Guid userId)
|
||||
{
|
||||
return new CipherDetails
|
||||
{
|
||||
Id = id,
|
||||
UserId = userId,
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Type = CipherType.Login,
|
||||
Data = @"
|
||||
{
|
||||
""Uris"": [
|
||||
{
|
||||
""Uri"": ""https://bitwarden.com""
|
||||
}
|
||||
],
|
||||
""Username"": ""testuser"",
|
||||
""Password"": ""securepassword123""
|
||||
}"
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Admin, true, true)]
|
||||
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
||||
|
Loading…
Reference in New Issue
Block a user