[PM-12473] refactor: add CollectionAuthorizationService and wire it into the Update endpoint - #8211
[PM-12473] refactor: add CollectionAuthorizationService and wire it into the Update endpoint#8211r-tome wants to merge 8 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8211 +/- ##
==========================================
+ Coverage 63.22% 63.33% +0.10%
==========================================
Files 2381 2385 +4
Lines 103757 104002 +245
Branches 9385 9426 +41
==========================================
+ Hits 65604 65867 +263
+ Misses 35924 35888 -36
- Partials 2229 2247 +18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3a2cd25 to
56c0576
Compare
Introduces ICollectionAuthorizationService/CollectionAuthorizationService and the shared static CollectionRules class, replacing ASP.NET's IAuthorizationHandler for fine-grained collection-access authorization with a plain constructor-injected service that fetches its own data and calls static rules, per architecture review on PR #8075. Ships as groundwork only -- no controller or endpoint wires it in yet. Also fixes a latent bug where CollectionRules.CanModifyGroupAccess checked Permissions.ManageUsers instead of Permissions.ManageGroups.
Use organization.Type is Owner or Admin directly at call sites instead of the wrapper property. Also trims XML doc comments in the new authorization files down to single-line summaries, matching sibling files like CollectionPermissions.cs.
56c0576 to
896ca86
Compare
CanModifyUserAccess/CanModifyGroupAccess are structural supersets of CanUpdate in CollectionRules, so their AND (IsSuccess) was always exactly equal to CanUpdateCollection alone, and nothing read the individual flags. Drops CollectionAuthorizationResult and returns Task<bool> directly, matching CollectionPermissions.CanCreate's existing bool-returning precedent. Still computes all three checks internally, since they remain three distinct permissions even though today's rule definitions happen to collapse their AND.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Re-reviewed at head commit No findings. Code Review DetailsVerification notes from this pass:
|
Nullable reference types are already enabled project-wide via
Directory.Build.props, so the per-file directive was redundant. Also fixes
CollectionAuthorizationServiceTests names to match the established
{Method}_When{Condition}_{Success|NoSuccess} convention used by sibling
files (BulkCollectionAuthorizationHandlerTests, CollectionPermissionsTests).
…ides AuthorizeUpdateAsync always fetched the caller's managed collections before evaluating any rule, even when EditAnyCollection or an admin-access bypass already made the result true. Try the rules with callerManagesCollection false first and only fetch the real value when that pass isn't enough.
…nc into Put behind a flag
…, harden and memoize CollectionAuthorizationService Adds per-request memoization of managed/orphaned/organization-id lookups, short-circuits non-members to the provider bypass without extra queries, and trims the interface to the three methods with real consumers.
| var orphanedCollectionIds = CollectionRules.PerCollection.CanManageOrphanedCollections(organization) | ||
| ? await GetOrphanedCollectionIdsAsync(organizationId) | ||
| : new HashSet<Guid>(); |
There was a problem hiding this comment.
🎨 SUGGESTED: The orphaned-collections query runs for every Owner/Admin, even when callerManagedCollectionIds already authorizes all requested collections.
Details and fix
GetOrphanedCollectionIdsAsync calls Collection_ReadWithGroupsAndUsersByOrganizationId, which returns every collection in the organization plus every CollectionGroup and CollectionUser row for it — three result sets, then an O(collections × groups) FirstOrDefault join in Infrastructure.Dapper/AdminConsole/Repositories/CollectionRepository.cs:115.
BulkCollectionAuthorizationHandler.CanManageCollectionsAsync only reached that query after the managed-collection check had already failed:
var canManageTargetCollections = targetCollections.All(tc => _managedCollectionsIds.Contains(tc.Id));
if (canManageTargetCollections)
{
return true; // orphaned query never runs
}Here the CanManageOrphanedCollections gate only filters out non-Owner/Admin callers, so the most common admin path — an Owner or Admin in an organization with AllowAdminAccessToAllCollectionItems = false editing a collection they are directly assigned to manage — now pays for the full-organization access read on every call, where it previously paid nothing.
Adding the managed-collection check to the gate restores the old short-circuit:
var callerManagedCollectionIds = await GetCallerManagedCollectionIdsAsync(currentContext.UserId.Value);
var hasUnmanagedCollections = requestedCollectionIds.Any(id => !callerManagedCollectionIds.Contains(id));
// Only Owners and Admins can manage orphaned collections, and only unmanaged collections need the check.
var orphanedCollectionIds = hasUnmanagedCollections && CollectionRules.PerCollection.CanManageOrphanedCollections(organization)
? await GetOrphanedCollectionIdsAsync(organizationId)
: new HashSet<Guid>();This matters more once GroupsController and OrganizationUsersController are wired to the batch methods, since those save paths are higher volume than a collection PUT.
…very requested collection
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-12473
📔 Objective
We are replacing
BulkCollectionAuthorizationHandlerwith per-resource[Resource]AuthorizationServiceclasses. Each service fetches its own data. Each service calls a shared staticRulesclass.An earlier design used ASP.NET's
IAuthorizationHandler. It made the controller fetch data first, then pass it to the handler. A reviewer flagged this problem on PR #8075. We dropped that design.This PR adds the first service:
ICollectionAuthorizationService. It has three methods:AuthorizeUpdateAsyncchecks one collection.PUT organizations/{orgId}/collections/{id}calls it now, behind thepm-35160-authorization-servicesflag. The old handler check stays as the fallback.AuthorizeModifyUserAccessManyAsyncandAuthorizeModifyGroupAccessManyAsynccheck a batch of collections. No controller calls them yet. A follow-up PR will wire them intoCollectionsController.PostBulkCollectionAccess. Later PRs will wire them intoGroupsControllerandOrganizationUsersController.The service calls a static
CollectionRulesclass, split into two tiers:OrganizationWide: rules that apply to every collection in the org (CanUpdate,CanModifyUserAccess,CanModifyGroupAccess).PerCollection: the fallback for one collection. The caller manages that collection, or the caller is an Owner/Admin and the collection is orphaned.This PR also fixes a bug.
CanModifyGroupAccesscheckedPermissions.ManageUsers. It now checksPermissions.ManageGroups.CollectionAuthorizationServicematches the old handler in three ways:📸 Screenshots
N/A