diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index e4010d001810..9a5a12cff95d 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -1,12 +1,14 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -23,6 +25,7 @@ public class CollectionsController : Controller private readonly IAuthorizationService _authorizationService; private readonly ICurrentContext _currentContext; private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand; + private readonly IFeatureService _featureService; public CollectionsController( ICollectionRepository collectionRepository, @@ -31,7 +34,8 @@ public CollectionsController( IUserService userService, IAuthorizationService authorizationService, ICurrentContext currentContext, - IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand) + IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand, + IFeatureService featureService) { _collectionRepository = collectionRepository; _collectionService = collectionService; @@ -40,8 +44,11 @@ public CollectionsController( _authorizationService = authorizationService; _currentContext = currentContext; _bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand; + _featureService = featureService; } + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + [HttpGet("{id}")] public async Task Get(Guid orgId, Guid id) { @@ -152,8 +159,10 @@ public async Task Post(Guid orgId, [FromBody] Collectio { var collection = model.ToCollection(orgId); - var result = await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Create); - if (!result.Succeeded) + var authorized = FlexibleCollectionsIsEnabled + ? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Create)).Succeeded + : await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id); + if (!authorized) { throw new NotFoundException(); } @@ -194,6 +203,10 @@ public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable model.Ids.Contains(c.Id) && c.OrganizationId == orgId); + + if (!filteredCollections.Any()) + { + throw new BadRequestException("No collections found."); + } + + await _deleteCollectionCommand.DeleteManyAsync(filteredCollections); } [HttpDelete("{id}/user/{orgUserId}")] @@ -272,6 +310,28 @@ private async Task GetCollectionAsync(Guid id, Guid orgId) return collection; } + private void DeprecatedPermissionsGuard() + { + if (FlexibleCollectionsIsEnabled) + { + throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF."); + } + } + + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] + private async Task CanCreateCollection(Guid orgId, Guid collectionId) + { + DeprecatedPermissionsGuard(); + + if (collectionId != default) + { + return false; + } + + return await _currentContext.OrganizationManager(orgId) || (_currentContext.Organizations?.Any(o => o.Id == orgId && + (o.Permissions?.CreateNewCollections ?? false)) ?? false); + } + private async Task CanEditCollectionAsync(Guid orgId, Guid collectionId) { if (collectionId == default) @@ -294,6 +354,41 @@ private async Task CanEditCollectionAsync(Guid orgId, Guid collectionId) return false; } + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] + private async Task CanDeleteCollectionAsync(Guid orgId, Guid collectionId) + { + DeprecatedPermissionsGuard(); + + if (collectionId == default) + { + return false; + } + + if (await DeleteAnyCollection(orgId)) + { + return true; + } + + if (await _currentContext.DeleteAssignedCollections(orgId)) + { + var collectionDetails = + await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value); + return collectionDetails != null; + } + + return false; + } + + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] + private async Task DeleteAnyCollection(Guid orgId) + { + DeprecatedPermissionsGuard(); + + return await _currentContext.OrganizationAdmin(orgId) || + (_currentContext.Organizations?.Any(o => o.Id == orgId + && (o.Permissions?.DeleteAnyCollection ?? false)) ?? false); + } + private async Task CanViewCollectionAsync(Guid orgId, Guid collectionId) { if (collectionId == default) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 6fb1d6efe9cf..ccf9e1700960 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -765,6 +765,7 @@ public async Task PostSso(Guid id, [FromBody] Orga } [HttpPut("{id}/collection-management")] + [RequireFeature(FeatureFlagKeys.FlexibleCollections)] public async Task PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) { var organization = await _organizationRepository.GetByIdAsync(id); diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs index b3e15e281f8d..626402f715b3 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs @@ -1,27 +1,42 @@ -using Bit.Core.Context; +using Bit.Core; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; namespace Bit.Api.Vault.AuthorizationHandlers.Collections; +/// +/// Handles authorization logic for Collection objects, including access permissions for users and groups. +/// This uses new logic implemented in the Flexible Collections initiative. +/// public class CollectionAuthorizationHandler : BulkAuthorizationHandler { private readonly ICurrentContext _currentContext; private readonly ICollectionRepository _collectionRepository; + private readonly IFeatureService _featureService; - public CollectionAuthorizationHandler(ICurrentContext currentContext, ICollectionRepository collectionRepository) + public CollectionAuthorizationHandler(ICurrentContext currentContext, ICollectionRepository collectionRepository, + IFeatureService featureService) { _currentContext = currentContext; _collectionRepository = collectionRepository; + _featureService = featureService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement, ICollection resources) { + if (!_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext)) + { + // Flexible collections is OFF, should not be using this handler + throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON."); + } + // Establish pattern of authorization handler null checking passed resources if (resources == null || !resources.Any()) { diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8dea0561f901..5861bde022fb 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -40,6 +40,8 @@ public static class FeatureFlagKeys public const string TrustedDeviceEncryption = "trusted-device-encryption"; public const string AutofillV2 = "autofill-v2"; public const string BrowserFilelessImport = "browser-fileless-import"; + public const string FlexibleCollections = "flexible-collections"; + public const string BulkCollectionAccess = "bulk-collection-access"; public static List GetAllKeys() { diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index b2beccbbce87..5e907e927cb9 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -19,6 +19,7 @@ public class CollectionService : ICollectionService private readonly IMailService _mailService; private readonly IReferenceEventService _referenceEventService; private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; public CollectionService( IEventService eventService, @@ -28,7 +29,8 @@ public CollectionService( IUserRepository userRepository, IMailService mailService, IReferenceEventService referenceEventService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IFeatureService featureService) { _eventService = eventService; _organizationRepository = organizationRepository; @@ -38,6 +40,7 @@ public CollectionService( _mailService = mailService; _referenceEventService = referenceEventService; _currentContext = currentContext; + _featureService = featureService; } public async Task SaveAsync(Collection collection, IEnumerable groups = null, @@ -51,12 +54,17 @@ public async Task SaveAsync(Collection collection, IEnumerable g.Manage) ?? false; - var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false; - if (!groupHasManageAccess && !userHasManageAccess) + + // If using Flexible Collections - a collection should always have someone with Can Manage permissions + if (_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext)) { - throw new BadRequestException( - "At least one member or group must have can manage permission."); + var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false; + var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false; + if (!groupHasManageAccess && !userHasManageAccess) + { + throw new BadRequestException( + "At least one member or group must have can manage permission."); + } } if (collection.Id == default(Guid)) diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index 3bfaa8b02c30..5bd5bfcb4c14 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -2,6 +2,7 @@ using Bit.Api.Controllers; using Bit.Api.Models.Request; using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -9,6 +10,7 @@ using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Authorization; @@ -19,6 +21,7 @@ namespace Bit.Api.Test.Controllers; [ControllerCustomize(typeof(CollectionsController))] [SutProviderCustomize] +[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)] public class CollectionsControllerTests { [Theory, BitAutoData] @@ -172,7 +175,7 @@ public async Task DeleteMany_Success(Guid orgId, Collection collection1, Collect .Returns(AuthorizationResult.Success()); // Act - await sutProvider.Sut.DeleteMany(model); + await sutProvider.Sut.DeleteMany(orgId, model); // Assert await sutProvider.GetDependency() @@ -215,7 +218,7 @@ public async Task DeleteMany_PermissionDenied_ThrowsNotFound(Guid orgId, Collect // Assert await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteMany(model)); + sutProvider.Sut.DeleteMany(orgId, model)); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() diff --git a/test/Api.Test/Controllers/LegacyCollectionsControllerTests.cs b/test/Api.Test/Controllers/LegacyCollectionsControllerTests.cs new file mode 100644 index 000000000000..68683837d1aa --- /dev/null +++ b/test/Api.Test/Controllers/LegacyCollectionsControllerTests.cs @@ -0,0 +1,254 @@ +using Bit.Api.Controllers; +using Bit.Api.Models.Request; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Controllers; + +/// +/// CollectionsController tests that use pre-Flexible Collections logic. To be removed when the feature flag is removed. +/// Note the feature flag defaults to OFF so it is not explicitly set in these tests. +/// +[ControllerCustomize(typeof(CollectionsController))] +[SutProviderCustomize] +public class LegacyCollectionsControllerTests +{ + [Theory, BitAutoData] + public async Task Post_Success(Guid orgId, SutProvider sutProvider) + { + sutProvider.GetDependency() + .OrganizationManager(orgId) + .Returns(true); + + sutProvider.GetDependency() + .EditAnyCollection(orgId) + .Returns(false); + + var collectionRequest = new CollectionRequestModel + { + Name = "encrypted_string", + ExternalId = "my_external_id" + }; + + _ = await sutProvider.Sut.Post(orgId, collectionRequest); + + await sutProvider.GetDependency() + .Received(1) + .SaveAsync(Arg.Any(), Arg.Any>(), null); + } + + [Theory, BitAutoData] + public async Task Put_Success(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .ViewAssignedCollections(orgId) + .Returns(true); + + sutProvider.GetDependency() + .EditAssignedCollections(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + + sutProvider.GetDependency() + .GetByIdAsync(collectionId, userId) + .Returns(new CollectionDetails + { + OrganizationId = orgId, + }); + + _ = await sutProvider.Sut.Put(orgId, collectionId, collectionRequest); + } + + [Theory, BitAutoData] + public async Task Put_CanNotEditAssignedCollection_ThrowsNotFound(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .EditAssignedCollections(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + + sutProvider.GetDependency() + .GetByIdAsync(collectionId, userId) + .Returns(Task.FromResult(null)); + + _ = await Assert.ThrowsAsync(async () => await sutProvider.Sut.Put(orgId, collectionId, collectionRequest)); + } + + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsWithGroups_NoManagerPermissions_ThrowsNotFound(Organization organization, SutProvider sutProvider) + { + sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetManyWithDetails(organization.Id)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdWithAccessAsync(default, default); + } + + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsWithGroups_AdminPermissions_GetsAllCollections(Organization organization, User user, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().ViewAllCollections(organization.Id).Returns(true); + sutProvider.GetDependency().OrganizationAdmin(organization.Id).Returns(true); + + await sutProvider.Sut.GetManyWithDetails(organization.Id); + + await sutProvider.GetDependency().Received().GetManyByOrganizationIdWithAccessAsync(organization.Id); + await sutProvider.GetDependency().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id); + } + + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsWithGroups_MissingViewAllPermissions_GetsAssignedCollections(Organization organization, User user, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(true); + sutProvider.GetDependency().OrganizationManager(organization.Id).Returns(true); + + await sutProvider.Sut.GetManyWithDetails(organization.Id); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default); + await sutProvider.GetDependency().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id); + } + + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsWithGroups_CustomUserWithManagerPermissions_GetsAssignedCollections(Organization organization, User user, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(true); + sutProvider.GetDependency().EditAssignedCollections(organization.Id).Returns(true); + + + await sutProvider.Sut.GetManyWithDetails(organization.Id); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default); + await sutProvider.GetDependency().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id); + } + + + [Theory, BitAutoData] + public async Task DeleteMany_Success(Guid orgId, User user, Collection collection1, Collection collection2, SutProvider sutProvider) + { + // Arrange + var model = new CollectionBulkDeleteRequestModel + { + Ids = new[] { collection1.Id, collection2.Id }, + }; + + var collections = new List + { + new CollectionDetails + { + Id = collection1.Id, + OrganizationId = orgId, + }, + new CollectionDetails + { + Id = collection2.Id, + OrganizationId = orgId, + }, + }; + + sutProvider.GetDependency() + .DeleteAssignedCollections(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UserId + .Returns(user.Id); + + sutProvider.GetDependency() + .GetOrganizationCollectionsAsync(orgId) + .Returns(collections); + + // Act + await sutProvider.Sut.DeleteMany(orgId, model); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(coll => coll.Select(c => c.Id).SequenceEqual(collections.Select(c => c.Id)))); + + } + + [Theory, BitAutoData] + public async Task DeleteMany_CanNotDeleteAssignedCollection_ThrowsNotFound(Guid orgId, Collection collection1, Collection collection2, SutProvider sutProvider) + { + // Arrange + var model = new CollectionBulkDeleteRequestModel + { + Ids = new[] { collection1.Id, collection2.Id }, + }; + + sutProvider.GetDependency() + .DeleteAssignedCollections(orgId) + .Returns(false); + + // Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.DeleteMany(orgId, model)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync((IEnumerable)default); + + } + + + [Theory, BitAutoData] + public async Task DeleteMany_UserCanNotAccessCollections_FiltersOutInvalid(Guid orgId, User user, Collection collection1, Collection collection2, SutProvider sutProvider) + { + // Arrange + var model = new CollectionBulkDeleteRequestModel + { + Ids = new[] { collection1.Id, collection2.Id }, + }; + + var collections = new List + { + new CollectionDetails + { + Id = collection2.Id, + OrganizationId = orgId, + }, + }; + + sutProvider.GetDependency() + .DeleteAssignedCollections(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UserId + .Returns(user.Id); + + sutProvider.GetDependency() + .GetOrganizationCollectionsAsync(orgId) + .Returns(collections); + + // Act + await sutProvider.Sut.DeleteMany(orgId, model); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(coll => coll.Select(c => c.Id).SequenceEqual(collections.Select(c => c.Id)))); + } + + +} diff --git a/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs index 1f294217182d..aaaa3076b029 100644 --- a/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs +++ b/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs @@ -1,11 +1,13 @@ using System.Security.Claims; using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture; using Bit.Core.Test.Vault.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -16,6 +18,7 @@ namespace Bit.Api.Test.Vault.AuthorizationHandlers; [SutProviderCustomize] +[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)] public class CollectionAuthorizationHandlerTests { [Theory, CollectionCustomization] diff --git a/test/Core.Test/AutoFixture/FeatureServiceFixtures.cs b/test/Core.Test/AutoFixture/FeatureServiceFixtures.cs new file mode 100644 index 000000000000..69f771e321a9 --- /dev/null +++ b/test/Core.Test/AutoFixture/FeatureServiceFixtures.cs @@ -0,0 +1,75 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Kernel; +using Bit.Core.Context; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; + +namespace Bit.Core.Test.AutoFixture; + +internal class FeatureServiceBuilder : ISpecimenBuilder +{ + private readonly string _enabledFeatureFlag; + + public FeatureServiceBuilder(string enabledFeatureFlag) + { + _enabledFeatureFlag = enabledFeatureFlag; + } + + public object Create(object request, ISpecimenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (request is not ParameterInfo pi) + { + return new NoSpecimen(); + } + + if (pi.ParameterType == typeof(IFeatureService)) + { + var fixture = new Fixture(); + var featureService = fixture.WithAutoNSubstitutions().Create(); + featureService + .IsEnabled(_enabledFeatureFlag, Arg.Any(), Arg.Any()) + .Returns(true); + return featureService; + } + + return new NoSpecimen(); + } +} + +internal class FeatureServiceCustomization : ICustomization +{ + private readonly string _enabledFeatureFlag; + + public FeatureServiceCustomization(string enabledFeatureFlag) + { + _enabledFeatureFlag = enabledFeatureFlag; + } + + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new FeatureServiceBuilder(_enabledFeatureFlag)); + } +} + +/// +/// Arranges the IFeatureService mock to enable the specified feature flag +/// +public class FeatureServiceCustomizeAttribute : BitCustomizeAttribute +{ + private readonly string _enabledFeatureFlag; + + public FeatureServiceCustomizeAttribute(string enabledFeatureFlag) + { + _enabledFeatureFlag = enabledFeatureFlag; + } + + public override ICustomization GetCustomization() => new FeatureServiceCustomization(_enabledFeatureFlag); +} diff --git a/test/Core.Test/Services/CollectionServiceTests.cs b/test/Core.Test/Services/CollectionServiceTests.cs index 0ce0a90dc458..97565bba4be9 100644 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ b/test/Core.Test/Services/CollectionServiceTests.cs @@ -112,6 +112,9 @@ public async Task SaveAsync_NoManageAccess_ThrowsBadRequest(Collection collectio { collection.Id = default; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any(), Arg.Any()) + .Returns(true); var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveAsync(collection, null, users)); Assert.Contains("At least one member or group must have can manage permission.", ex.Message);