2022-10-25 21:23:49 +02:00
|
|
|
|
using System.Security.Claims;
|
2023-03-02 19:23:38 +01:00
|
|
|
|
using Bit.Api.Vault.Controllers;
|
|
|
|
|
using Bit.Api.Vault.Models.Request;
|
2024-04-30 19:28:16 +02:00
|
|
|
|
using Bit.Core;
|
|
|
|
|
using Bit.Core.Context;
|
2024-05-21 17:31:22 +02:00
|
|
|
|
using Bit.Core.Entities;
|
2024-04-30 19:28:16 +02:00
|
|
|
|
using Bit.Core.Enums;
|
|
|
|
|
using Bit.Core.Exceptions;
|
|
|
|
|
using Bit.Core.Models.Data.Organizations;
|
2024-05-21 17:31:22 +02:00
|
|
|
|
using Bit.Core.Repositories;
|
2022-10-25 21:23:49 +02:00
|
|
|
|
using Bit.Core.Services;
|
2024-04-30 19:28:16 +02:00
|
|
|
|
using Bit.Core.Vault.Entities;
|
2023-03-02 19:23:38 +01:00
|
|
|
|
using Bit.Core.Vault.Models.Data;
|
|
|
|
|
using Bit.Core.Vault.Repositories;
|
2024-04-30 19:28:16 +02:00
|
|
|
|
using Bit.Core.Vault.Services;
|
2022-10-25 21:23:49 +02:00
|
|
|
|
using Bit.Test.Common.AutoFixture;
|
|
|
|
|
using Bit.Test.Common.AutoFixture.Attributes;
|
|
|
|
|
using NSubstitute;
|
2024-05-21 17:31:22 +02:00
|
|
|
|
using NSubstitute.ReturnsExtensions;
|
2022-10-25 21:23:49 +02:00
|
|
|
|
using Xunit;
|
2024-05-21 17:31:22 +02:00
|
|
|
|
using CipherType = Bit.Core.Vault.Enums.CipherType;
|
2022-10-25 21:23:49 +02:00
|
|
|
|
|
|
|
|
|
namespace Bit.Api.Test.Controllers;
|
|
|
|
|
|
|
|
|
|
[ControllerCustomize(typeof(CiphersController))]
|
|
|
|
|
[SutProviderCustomize]
|
|
|
|
|
public class CiphersControllerTests
|
|
|
|
|
{
|
|
|
|
|
[Theory, BitAutoData]
|
|
|
|
|
public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(Guid userId, Guid folderId, SutProvider<CiphersController> sutProvider)
|
|
|
|
|
{
|
|
|
|
|
var isFavorite = true;
|
|
|
|
|
var cipherId = Guid.NewGuid();
|
|
|
|
|
|
|
|
|
|
sutProvider.GetDependency<IUserService>()
|
|
|
|
|
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
|
|
|
|
.Returns(userId);
|
|
|
|
|
|
|
|
|
|
var cipherDetails = new CipherDetails
|
|
|
|
|
{
|
|
|
|
|
Favorite = isFavorite,
|
|
|
|
|
FolderId = folderId,
|
2023-03-02 19:23:38 +01:00
|
|
|
|
Type = Core.Vault.Enums.CipherType.SecureNote,
|
2022-10-25 21:23:49 +02:00
|
|
|
|
Data = "{}"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sutProvider.GetDependency<ICipherRepository>()
|
2023-11-28 02:14:33 +01:00
|
|
|
|
.GetByIdAsync(cipherId, userId, Arg.Any<bool>())
|
2022-10-25 21:23:49 +02:00
|
|
|
|
.Returns(Task.FromResult(cipherDetails));
|
|
|
|
|
|
2023-11-28 02:14:33 +01:00
|
|
|
|
var result = await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() });
|
2022-10-25 21:23:49 +02:00
|
|
|
|
|
2023-07-14 17:18:26 +02:00
|
|
|
|
Assert.Equal(folderId, result.FolderId);
|
2022-10-25 21:23:49 +02:00
|
|
|
|
Assert.Equal(isFavorite, result.Favorite);
|
|
|
|
|
}
|
2024-04-30 19:28:16 +02:00
|
|
|
|
|
2024-05-21 17:31:22 +02:00
|
|
|
|
[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""
|
|
|
|
|
}"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-30 19:28:16 +02:00
|
|
|
|
[Theory]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Admin, true, true)]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Custom, false, true)]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Custom, true, true)]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Admin, false, false)]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Owner, false, false)]
|
|
|
|
|
[BitAutoData(OrganizationUserType.Custom, false, false)]
|
|
|
|
|
public async Task CanEditCiphersAsAdminAsync_FlexibleCollections_Success(
|
|
|
|
|
OrganizationUserType userType, bool allowAdminsAccessToAllItems, bool shouldSucceed,
|
2024-05-07 21:30:48 +02:00
|
|
|
|
CurrentContextOrganization organization, Guid userId, Cipher cipher, SutProvider<CiphersController> sutProvider
|
2024-04-30 19:28:16 +02:00
|
|
|
|
)
|
|
|
|
|
{
|
2024-05-07 21:30:48 +02:00
|
|
|
|
cipher.OrganizationId = organization.Id;
|
2024-04-30 19:28:16 +02:00
|
|
|
|
organization.Type = userType;
|
|
|
|
|
if (userType == OrganizationUserType.Custom)
|
|
|
|
|
{
|
|
|
|
|
// Assume custom users have EditAnyCollections for success case
|
|
|
|
|
organization.Permissions.EditAnyCollection = shouldSucceed;
|
|
|
|
|
}
|
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
|
|
|
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
|
|
|
|
|
2024-05-07 21:30:48 +02:00
|
|
|
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
|
|
|
|
|
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipher });
|
|
|
|
|
|
2024-04-30 19:28:16 +02:00
|
|
|
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
|
|
|
|
|
{
|
|
|
|
|
Id = organization.Id,
|
|
|
|
|
FlexibleCollections = true,
|
|
|
|
|
AllowAdminAccessToAllCollectionItems = allowAdminsAccessToAllItems
|
|
|
|
|
});
|
|
|
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
|
|
|
|
|
|
|
|
|
if (shouldSucceed)
|
|
|
|
|
{
|
2024-05-07 21:30:48 +02:00
|
|
|
|
await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString());
|
2024-04-30 19:28:16 +02:00
|
|
|
|
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
|
2024-05-07 21:30:48 +02:00
|
|
|
|
.DeleteAsync(default, default);
|
2024-04-30 19:28:16 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2024-05-07 21:30:48 +02:00
|
|
|
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()));
|
2024-04-30 19:28:16 +02:00
|
|
|
|
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()
|
2024-05-07 21:30:48 +02:00
|
|
|
|
.DeleteAsync(default, default);
|
2024-04-30 19:28:16 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// To be removed after FlexibleCollections is fully released
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Theory]
|
|
|
|
|
[BitAutoData(true, true)]
|
|
|
|
|
[BitAutoData(false, true)]
|
|
|
|
|
[BitAutoData(true, false)]
|
|
|
|
|
[BitAutoData(false, false)]
|
|
|
|
|
public async Task CanEditCiphersAsAdminAsync_NonFlexibleCollections(
|
|
|
|
|
bool v1Enabled, bool shouldSucceed, CurrentContextOrganization organization, Guid userId, Cipher cipher, SutProvider<CiphersController> sutProvider
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
cipher.OrganizationId = organization.Id;
|
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().EditAnyCollection(organization.Id).Returns(shouldSucceed);
|
|
|
|
|
|
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
|
|
|
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
|
|
|
|
|
|
|
|
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
|
|
|
|
|
{
|
|
|
|
|
Id = organization.Id,
|
|
|
|
|
FlexibleCollections = false,
|
|
|
|
|
AllowAdminAccessToAllCollectionItems = false
|
|
|
|
|
});
|
|
|
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(v1Enabled);
|
|
|
|
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
|
|
|
|
|
|
|
|
|
|
if (shouldSucceed)
|
|
|
|
|
{
|
|
|
|
|
await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString());
|
|
|
|
|
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
|
|
|
|
|
.DeleteAsync(default, default);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()));
|
|
|
|
|
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()
|
|
|
|
|
.DeleteAsync(default, default);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[BitAutoData(false, false)]
|
|
|
|
|
[BitAutoData(true, false)]
|
2024-05-07 21:30:48 +02:00
|
|
|
|
[BitAutoData(true, true)]
|
2024-04-30 19:28:16 +02:00
|
|
|
|
public async Task CanEditCiphersAsAdminAsync_Providers(
|
2024-05-07 21:30:48 +02:00
|
|
|
|
bool fcV1Enabled, bool restrictProviders, Cipher cipher, CurrentContextOrganization organization, Guid userId, SutProvider<CiphersController> sutProvider
|
2024-04-30 19:28:16 +02:00
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
cipher.OrganizationId = organization.Id;
|
2024-05-07 21:30:48 +02:00
|
|
|
|
|
|
|
|
|
// Simulate that the user is a provider for the organization
|
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().EditAnyCollection(organization.Id).Returns(true);
|
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organization.Id).Returns(true);
|
|
|
|
|
|
2024-04-30 19:28:16 +02:00
|
|
|
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
|
|
|
|
|
|
|
|
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
|
|
|
|
|
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipher });
|
|
|
|
|
|
|
|
|
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
|
|
|
|
|
{
|
|
|
|
|
Id = organization.Id,
|
|
|
|
|
FlexibleCollections = fcV1Enabled, // Assume FlexibleCollections is enabled if v1 is enabled
|
|
|
|
|
AllowAdminAccessToAllCollectionItems = false
|
|
|
|
|
});
|
|
|
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(fcV1Enabled);
|
2024-05-07 21:30:48 +02:00
|
|
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(restrictProviders);
|
2024-04-30 19:28:16 +02:00
|
|
|
|
|
2024-05-07 21:30:48 +02:00
|
|
|
|
// Non V1 FC or non restricted providers should succeed
|
|
|
|
|
if (!fcV1Enabled || !restrictProviders)
|
2024-04-30 19:28:16 +02:00
|
|
|
|
{
|
|
|
|
|
await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString());
|
|
|
|
|
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
|
|
|
|
|
.DeleteAsync(default, default);
|
|
|
|
|
}
|
2024-05-07 21:30:48 +02:00
|
|
|
|
else // Otherwise, they should fail
|
2024-04-30 19:28:16 +02:00
|
|
|
|
{
|
|
|
|
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()));
|
|
|
|
|
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()
|
|
|
|
|
.DeleteAsync(default, default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fcV1Enabled)
|
|
|
|
|
{
|
|
|
|
|
await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(organization.Id);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await sutProvider.GetDependency<ICurrentContext>().Received().EditAnyCollection(organization.Id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-25 21:23:49 +02:00
|
|
|
|
}
|