[PM-41448] refactor: add OrganizationUserAuthorizationService and wire it into OrganizationUsersController - #8274
Conversation
…OrganizationUsersController OrganizationUsersController authorized collection access with the old handler in three places: a bulk check in Invite, and inside GetAuthorizedCollectionsToSaveAsync a bulk check on the posted collections plus a per-collection loop over the user's current collections. Put also held two inline rules: a caller cannot give themselves access to a new collection, and a caller editing themselves cannot change their own groups. Behind pm-35160-authorization-services all of that moves into OrganizationUserAuthorizationService, which calls ICollectionAuthorizationService.AuthorizeModifyUserAccessManyAsync once for the posted collections and once for the current ones. The service works out for itself whether the caller is editing their own membership. It reads the target organization user rather than taking a flag from the controller, so a controller change cannot silently disable the rule. That costs one extra lookup per update. Default-collection filtering stays in the controller. It is a business rule rather than an authorization check, and it needs Collection.Type, so both collection reads stay on the flag-on path. Two behavior changes on the flag-on path. Unknown and cross-organization posted collection ids now return a 404 instead of being dropped and then written anyway; Invite gains that check too. AllowAdminAccessToAllCollectionItems now comes from the organization ability cache rather than the bound Organization entity, which matches how the collection rules already read it in the same request.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the new No findings met the confidence bar for posting. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## ac/pm-41270/groups-authorization-service #8274 +/- ##
============================================================================
+ Coverage 63.39% 63.41% +0.02%
============================================================================
Files 2434 2436 +2
Lines 105110 105189 +79
Branches 9541 9550 +9
============================================================================
+ Hits 66630 66706 +76
- Misses 36199 36200 +1
- Partials 2281 2283 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-41448
Last of four, after #8211 (PM-12473), #8268 (PM-42605) and #8270 (PM-41270).
📔 Objective
OrganizationUsersControllerauthorized collection access withBulkCollectionAuthorizationHandlerin three places: a bulk check inInvite, andinside
GetAuthorizedCollectionsToSaveAsynca bulk check on the posted collectionsplus a per-collection loop over the user's current collections.
Putalso held twoinline rules — a caller cannot give themselves access to a new collection, and a
caller editing themselves cannot change their own groups.
Behind
pm-35160-authorization-servicesall of that moves intoOrganizationUserAuthorizationService, which callsICollectionAuthorizationService.AuthorizeModifyUserAccessManyAsynconce for theposted collections and once for the current ones.
The service works out for itself whether the caller is editing their own membership.
It reads the target organization user rather than taking a flag from the controller,
so a controller change cannot silently disable the rule. That costs one indexed
primary-key read per update.
Default-collection filtering stays in the controller. It is a business rule rather
than an authorization check and it needs
Collection.Type, so both collection readssurvive on the flag-on path, moved into
BuildCollectionAccessToSaveAsync.Behavior changes on the flag-on path
orgIdis now authoritative. The old handler set_targetOrganizationId = resources.First().OrganizationId, authorizing against theresource's organization rather than the route's. This closes a cross-organization
authorization gap and is the most valuable change here.
silently while the rest were authorized.
Invitegains the same check. The webclient builds its list from collections fetched for that organization, so it cannot
trip this. The public API
MembersControllerdoes not use this controller.AllowAdminAccessToAllCollectionItemsnow comes from the organization abilitycache rather than the bound
Organizationentity. The old path already read itfrom that cache for the collection decision in the same request, so this removes a
mixed source rather than adding risk.
Tests
The self-add
BadRequestException, the null-groups path, andPutthrowingNotFoundExceptionhad no coverage. Flag-off characterization tests pin them first,then eight service tests and nine flag-on controller tests follow. One test covers
the saved access list end to end: posted collections saved, a removed collection
dropped, an unauthorized current collection kept, and default collections filtered
out of both.
📸 Screenshots
N/A