diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs
new file mode 100644
index 0000000000..73e3e19619
--- /dev/null
+++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs
@@ -0,0 +1,205 @@
+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 BulkCollectionAuthorizationHandler : BulkAuthorizationHandler
+{
+ private readonly ICurrentContext _currentContext;
+ private readonly ICollectionRepository _collectionRepository;
+ private readonly IFeatureService _featureService;
+
+ private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
+
+ public BulkCollectionAuthorizationHandler(
+ ICurrentContext currentContext,
+ ICollectionRepository collectionRepository,
+ IFeatureService featureService)
+ {
+ _currentContext = currentContext;
+ _collectionRepository = collectionRepository;
+ _featureService = featureService;
+ }
+
+ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
+ CollectionOperationRequirement requirement, ICollection resources)
+ {
+ if (!FlexibleCollectionsIsEnabled)
+ {
+ // 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)
+ {
+ context.Fail();
+ return;
+ }
+
+ if (!_currentContext.UserId.HasValue)
+ {
+ context.Fail();
+ return;
+ }
+
+ var targetOrganizationId = requirement.OrganizationId != default
+ ? requirement.OrganizationId : resources.FirstOrDefault()?.OrganizationId ?? default;
+
+ if (targetOrganizationId == default)
+ {
+ context.Fail();
+ return;
+ }
+
+ // Ensure all target collections belong to the same organization
+ if (resources.Any(tc => tc.OrganizationId != targetOrganizationId))
+ {
+ throw new BadRequestException("Requested collections must belong to the same organization.");
+ }
+
+ // Acting user is not a member of the target organization, fail
+ var org = _currentContext.GetOrganization(targetOrganizationId);
+ if (org == null)
+ {
+ context.Fail();
+ return;
+ }
+
+ switch (requirement)
+ {
+ case not null when requirement == CollectionOperations.Create:
+ await CanCreateAsync(context, requirement, org);
+ break;
+
+ case not null when requirement == CollectionOperations.Read:
+ await CanReadAsync(context, requirement, resources, org);
+ break;
+
+ case not null when requirement == CollectionOperations.Delete:
+ await CanDeleteAsync(context, requirement, resources, org);
+ break;
+
+ case not null when requirement == CollectionOperations.Update:
+ case not null when requirement == CollectionOperations.ModifyAccess:
+ await CanManageCollectionAccessAsync(context, requirement, resources, org);
+ break;
+ }
+ }
+
+ private async Task CanCreateAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
+ CurrentContextOrganization org)
+ {
+ // If false, all organization members are allowed to create collections
+ if (!org.LimitCollectionCreationDeletion)
+ {
+ context.Succeed(requirement);
+ return;
+ }
+
+ // Owners, Admins, Providers, and users with CreateNewCollections permission can always create collections
+ if (
+ org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
+ org.Permissions is { CreateNewCollections: true } ||
+ await _currentContext.ProviderUserForOrgAsync(org.Id))
+ {
+ context.Succeed(requirement);
+ return;
+ }
+
+ context.Fail();
+ }
+
+ private async Task CanReadAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
+ ICollection targetCollections, CurrentContextOrganization org)
+ {
+ if (org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
+ await _currentContext.ProviderUserForOrgAsync(org.Id))
+ {
+ context.Succeed(requirement);
+ return;
+ }
+
+ await CheckCollectionPermissionsAsync(context, requirement, targetCollections, org, requireManagePermission: false);
+ }
+
+ private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
+ ICollection targetCollections, CurrentContextOrganization org)
+ {
+ // Owners, Admins, Providers, and users with DeleteAnyCollection permission can always delete collections
+ if (
+ org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
+ org.Permissions is { DeleteAnyCollection: true } ||
+ await _currentContext.ProviderUserForOrgAsync(org.Id))
+ {
+ context.Succeed(requirement);
+ return;
+ }
+
+ // The limit collection management setting is enabled and we are not an Admin (above condition), fail
+ if (org.LimitCollectionCreationDeletion)
+ {
+ context.Fail();
+ return;
+ }
+
+ await CheckCollectionPermissionsAsync(context, requirement, targetCollections, org, requireManagePermission: true);
+ }
+
+ ///
+ /// Ensures the acting user is allowed to manage access permissions for the target collections.
+ ///
+ private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context,
+ IAuthorizationRequirement requirement, ICollection targetCollections, CurrentContextOrganization org)
+ {
+ // Owners, Admins, Providers, and users with EditAnyCollection permission can always manage collection access
+ if (
+ org.Permissions is { EditAnyCollection: true } ||
+ org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
+ await _currentContext.ProviderUserForOrgAsync(org.Id))
+ {
+ context.Succeed(requirement);
+ return;
+ }
+
+ await CheckCollectionPermissionsAsync(context, requirement, targetCollections, org, requireManagePermission: true);
+ }
+
+ private async Task CheckCollectionPermissionsAsync(
+ AuthorizationHandlerContext context,
+ IAuthorizationRequirement requirement,
+ ICollection targetCollections,
+ CurrentContextOrganization org,
+ bool requireManagePermission)
+ {
+ // List of collection Ids the acting user has access to
+ var manageableCollectionIds =
+ (await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value))
+ .Where(c =>
+ // If requireManagePermission is true, check Collections with Manage permission
+ (!requireManagePermission || c.Manage)
+ && c.OrganizationId == org.Id)
+ .Select(c => c.Id)
+ .ToHashSet();
+
+ // The acting user does not have permissions for all target collections, fail
+ if (targetCollections.Any(tc => !manageableCollectionIds.Contains(tc.Id)))
+ {
+ context.Fail();
+ return;
+ }
+
+ context.Succeed(requirement);
+ }
+}
diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs
index 509d2fe433..5cde10d0c1 100644
--- a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs
+++ b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs
@@ -1,11 +1,8 @@
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;
@@ -51,13 +48,7 @@ public class CollectionAuthorizationHandler : AuthorizationHandler
-/// 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 BulkCollectionAuthorizationHandler : BulkAuthorizationHandler
-{
- private readonly ICurrentContext _currentContext;
- private readonly ICollectionRepository _collectionRepository;
- private readonly IFeatureService _featureService;
-
- private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
-
- public BulkCollectionAuthorizationHandler(
- ICurrentContext currentContext,
- ICollectionRepository collectionRepository,
- IFeatureService featureService)
- {
- _currentContext = currentContext;
- _collectionRepository = collectionRepository;
- _featureService = featureService;
- }
-
- protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
- CollectionOperationRequirement requirement, ICollection resources)
- {
- if (!FlexibleCollectionsIsEnabled)
+ else
{
- // 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)
- {
- context.Fail();
- return;
- }
-
- if (!_currentContext.UserId.HasValue)
- {
- context.Fail();
- return;
- }
-
- var targetOrganizationId = requirement.OrganizationId != default
- ? requirement.OrganizationId : resources.FirstOrDefault()?.OrganizationId ?? default;
-
- if (targetOrganizationId == default)
- {
- context.Fail();
- return;
- }
-
- // Ensure all target collections belong to the same organization
- if (resources.Any(tc => tc.OrganizationId != targetOrganizationId))
- {
- throw new BadRequestException("Requested collections must belong to the same organization.");
- }
-
- // Acting user is not a member of the target organization, fail
- var org = _currentContext.GetOrganization(targetOrganizationId);
- if (org == null)
- {
- context.Fail();
- return;
- }
-
- switch (requirement)
- {
- case not null when requirement == CollectionOperations.Create:
- await CanCreateAsync(context, requirement, org);
- break;
-
- case not null when requirement == CollectionOperations.Read:
- await CanReadAsync(context, requirement, resources, org);
- break;
-
- case not null when requirement == CollectionOperations.Delete:
- await CanDeleteAsync(context, requirement, resources, org);
- break;
-
- case not null when requirement == CollectionOperations.Update:
- case not null when requirement == CollectionOperations.ModifyAccess:
- await CanManageCollectionAccessAsync(context, requirement, resources, org);
- break;
- }
- }
-
- private async Task CanCreateAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
- CurrentContextOrganization org)
- {
- // If false, all organization members are allowed to create collections
- if (!org.LimitCollectionCreationDeletion)
- {
- context.Succeed(requirement);
- return;
- }
-
- // Owners, Admins, Providers, and users with CreateNewCollections permission can always create collections
- if (
- org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
- org.Permissions is { CreateNewCollections: true } ||
- await _currentContext.ProviderUserForOrgAsync(org.Id))
- {
- context.Succeed(requirement);
- return;
+ // Check if acting user is a provider user for the target organization
+ if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
+ {
+ context.Succeed(requirement);
+ return;
+ }
}
+ // Acting user is neither a member of the target organization or a provider user, fail
context.Fail();
}
-
- private async Task CanReadAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
- ICollection targetCollections, CurrentContextOrganization org)
- {
- if (org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
- await _currentContext.ProviderUserForOrgAsync(org.Id))
- {
- context.Succeed(requirement);
- return;
- }
-
- await CheckCollectionPermissionsAsync(context, requirement, targetCollections, org, requireManagePermission: false);
- }
-
- private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
- ICollection targetCollections, CurrentContextOrganization org)
- {
- // Owners, Admins, Providers, and users with DeleteAnyCollection permission can always delete collections
- if (
- org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
- org.Permissions is { DeleteAnyCollection: true } ||
- await _currentContext.ProviderUserForOrgAsync(org.Id))
- {
- context.Succeed(requirement);
- return;
- }
-
- // The limit collection management setting is enabled and we are not an Admin (above condition), fail
- if (org.LimitCollectionCreationDeletion)
- {
- context.Fail();
- return;
- }
-
- await CheckCollectionPermissionsAsync(context, requirement, targetCollections, org, requireManagePermission: true);
- }
-
- ///
- /// Ensures the acting user is allowed to manage access permissions for the target collections.
- ///
- private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context,
- IAuthorizationRequirement requirement, ICollection targetCollections, CurrentContextOrganization org)
- {
- // Owners, Admins, Providers, and users with EditAnyCollection permission can always manage collection access
- if (
- org.Permissions is { EditAnyCollection: true } ||
- org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
- await _currentContext.ProviderUserForOrgAsync(org.Id))
- {
- context.Succeed(requirement);
- return;
- }
-
- await CheckCollectionPermissionsAsync(context, requirement, targetCollections, org, requireManagePermission: true);
- }
-
- private async Task CheckCollectionPermissionsAsync(
- AuthorizationHandlerContext context,
- IAuthorizationRequirement requirement,
- ICollection targetCollections,
- CurrentContextOrganization org,
- bool requireManagePermission)
- {
- // List of collection Ids the acting user has access to
- var manageableCollectionIds =
- (await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value))
- .Where(c =>
- // If requireManagePermission is true, check Collections with Manage permission
- (!requireManagePermission || c.Manage)
- && c.OrganizationId == org.Id)
- .Select(c => c.Id)
- .ToHashSet();
-
- // The acting user does not have permissions for all target collections, fail
- if (targetCollections.Any(tc => !manageableCollectionIds.Contains(tc.Id)))
- {
- context.Fail();
- return;
- }
-
- context.Succeed(requirement);
- }
}
diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs
index 5bd5bfcb4c..04a31a7eb4 100644
--- a/test/Api.Test/Controllers/CollectionsControllerTests.cs
+++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs
@@ -47,101 +47,90 @@ public class CollectionsControllerTests
}
[Theory, BitAutoData]
- public async Task Put_Success(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest,
+ public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest,
SutProvider sutProvider)
{
- sutProvider.GetDependency()
- .ViewAssignedCollections(orgId)
- .Returns(true);
-
- sutProvider.GetDependency()
- .EditAssignedCollections(orgId)
- .Returns(true);
-
- sutProvider.GetDependency()
- .UserId
- .Returns(userId);
+ Collection ExpectedCollection() => Arg.Is(c => c.Id == collection.Id &&
+ c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
+ c.OrganizationId == collection.OrganizationId);
sutProvider.GetDependency()
- .GetByIdAsync(collectionId, userId)
- .Returns(new CollectionDetails
- {
- OrganizationId = orgId,
- });
+ .GetByIdAsync(collection.Id)
+ .Returns(collection);
- _ = await sutProvider.Sut.Put(orgId, collectionId, collectionRequest);
+ sutProvider.GetDependency()
+ .AuthorizeAsync(Arg.Any(),
+ collection,
+ Arg.Is>(r => r.Contains(CollectionOperations.Update)))
+ .Returns(AuthorizationResult.Success());
+
+ _ = await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .SaveAsync(ExpectedCollection(), Arg.Any>(),
+ Arg.Any>());
}
[Theory, BitAutoData]
- public async Task Put_CanNotEditAssignedCollection_ThrowsNotFound(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest,
+ public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest,
SutProvider sutProvider)
{
- sutProvider.GetDependency()
- .EditAssignedCollections(orgId)
- .Returns(true);
-
- sutProvider.GetDependency()
- .UserId
- .Returns(userId);
+ sutProvider.GetDependency()
+ .AuthorizeAsync(Arg.Any(),
+ collection,
+ Arg.Is>(r => r.Contains(CollectionOperations.Update)))
+ .Returns(AuthorizationResult.Failed());
sutProvider.GetDependency()
- .GetByIdAsync(collectionId, userId)
- .Returns(Task.FromResult(null));
+ .GetByIdAsync(collection.Id)
+ .Returns(collection);
- _ = await Assert.ThrowsAsync(async () => await sutProvider.Sut.Put(orgId, collectionId, collectionRequest));
+ _ = await Assert.ThrowsAsync(async () => await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest));
}
[Theory, BitAutoData]
- public async Task GetOrganizationCollectionsWithGroups_NoManagerPermissions_ThrowsNotFound(Organization organization, SutProvider sutProvider)
+ public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(Organization organization, Guid userId, SutProvider sutProvider)
{
- sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(false);
+ sutProvider.GetDependency().UserId.Returns(userId);
- 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);
+ sutProvider.GetDependency()
+ .AuthorizeAsync(
+ Arg.Any(),
+ Arg.Any