Skip to content

[PM-41448] refactor: add OrganizationUserAuthorizationService and wire it into OrganizationUsersController - #8274

Draft
r-tome wants to merge 1 commit into
ac/pm-41270/groups-authorization-servicefrom
ac/pm-41448/organization-user-authorization-service
Draft

[PM-41448] refactor: add OrganizationUserAuthorizationService and wire it into OrganizationUsersController#8274
r-tome wants to merge 1 commit into
ac/pm-41270/groups-authorization-servicefrom
ac/pm-41448/organization-user-authorization-service

Conversation

@r-tome

@r-tome r-tome commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-41448

Last of four, after #8211 (PM-12473), #8268 (PM-42605) and #8270 (PM-41270).

📔 Objective

OrganizationUsersController authorized collection access with
BulkCollectionAuthorizationHandler 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 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 reads
survive on the flag-on path, moved into BuildCollectionAccessToSaveAsync.

Behavior changes on the flag-on path

  • The route orgId is now authoritative. The old handler set
    _targetOrganizationId = resources.First().OrganizationId, authorizing against the
    resource's organization rather than the route's. This closes a cross-organization
    authorization gap and is the most valuable change here.
  • Partially unknown posted collection ids now return 404. They used to be dropped
    silently while the rest were authorized. Invite gains the same check. The web
    client builds its list from collections fetched for that organization, so it cannot
    trip this. The public API MembersController does not use this controller.
  • AllowAdminAccessToAllCollectionItems now comes from the organization ability
    cache rather than the bound Organization entity. The old path already read it
    from 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, and Put throwing
NotFoundException had 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

…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.
@r-tome r-tome added the ai-review Request a Claude code review label Aug 27, 2026
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the new OrganizationUserAuthorizationService (interface, result record, implementation, DI registration) and its wiring into OrganizationUsersController.Invite and Put behind pm-35160-authorization-services, plus the accompanying service and controller tests. I traced the flag-on path against the flag-off path it replaces: the self-add block, the "cannot edit your own groups" rule, the posted-collection authorization, the preservation of current collections the caller cannot change, and the default-collection filtering all resolve to the same outcomes, with the documented deltas (route orgId is authoritative, unknown posted collection ids now 404, AllowAdminAccessToAllCollectionItems sourced from the ability cache) behaving as described. IsEditingOwnMembershipAsync short-circuits before the repository read when the org allows admin access and fails closed when the ability is missing or CurrentContext.UserId is unset, and the empty-list guard in CollectionAuthorizationService.AuthorizeAsync makes the invite path's empty currentCollectionIds a no-op. The structure matches the sibling GroupsAuthorizationService from the preceding PR in the stack, and the flag-off characterization tests pin the previously uncovered paths before the new behavior is introduced.

No findings met the confidence bar for posting.

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.75281% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.41%. Comparing base (0e07257) to head (6f127c0).

Files with missing lines Patch % Lines
...ationUsers/OrganizationUserAuthorizationService.cs 92.85% 0 Missing and 2 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@eliykat eliykat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it follows the same structure as #8270. The same feedback applies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants