mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +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 updatedCipher = await GetByIdAsync(id, userId);
|
||||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
||||||
|
|
||||||
return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers);
|
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")]
|
[HttpPut("{id}/collections-admin")]
|
||||||
[HttpPost("{id}/collections-admin")]
|
[HttpPost("{id}/collections-admin")]
|
||||||
public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
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.Api.Vault.Models.Request;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
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;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using CipherType = Bit.Core.Vault.Enums.CipherType;
|
||||||
|
|
||||||
namespace Bit.Api.Test.Controllers;
|
namespace Bit.Api.Test.Controllers;
|
||||||
|
|
||||||
@ -50,6 +54,92 @@ public class CiphersControllerTests
|
|||||||
Assert.Equal(isFavorite, result.Favorite);
|
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]
|
[Theory]
|
||||||
[BitAutoData(OrganizationUserType.Admin, true, true)]
|
[BitAutoData(OrganizationUserType.Admin, true, true)]
|
||||||
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
||||||
|
Loading…
Reference in New Issue
Block a user