1
0
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:
Jason Ng 2024-05-21 11:31:22 -04:00 committed by GitHub
parent f2242186d0
commit 87865e8f5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 133 additions and 0 deletions

View File

@ -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)

View File

@ -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")
{ }
}

View File

@ -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)]