Repository navigation
Conversation
barborico
force-pushed
the
brynna/request_reviewer_routing
branch
3 times, most recently
from
October 8, 2026 23:23
ebf73a0 to
8ad6999
Compare
barborico
marked this pull request as ready for review
October 9, 2026 03:38
barborico
force-pushed
the
brynna/request_reviewer_routing
branch
from
October 16, 2026 07:06
8ad6999 to
a0fb96b
Compare
Add one definition of a request's reviewers. Each kind of request has owner levels, nearest first: the requested group's owners, then its app's owners for an app group, then Access admins; a group request starts at the requested app's owners. An eligible reviewer owns at a level and is not the requester, not deleted, and not blocked by a disallow_self_add tag the way CheckForSelfAdd would block their approval. The assigned reviewers are the eligible ones at the nearest level that has any. Each level is a SQL condition on the request row, so the same rule answers both "who reviews this request" and "which requests does this user review". Nothing calls it yet; the tests pin the rule down with a scenario per routing case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Creating an access, role, or group request now notifies the request's assigned reviewers. Each create operation had its own fallback chain, and they disagreed: an access or role request still listed its requester when the group had other owners, and only a role request skipped owners that a disallow_self_add tag would stop from approving. All three now follow the one rule. Close notifications are unchanged here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A close notification went to every possible approver at every level, including Access admins and sometimes the requester. It now goes to the request's assigned reviewers, so the people told a request is done are the people who were asked to review it. Some operations close requests as a side effect of changing ownership: adding users or roles to a group, and deleting or unmanaging a group or app. That change can move who is assigned, so these operations snapshot each request's assigned reviewers before making it and notify from the snapshot; a request the snapshot misses has its reviewers computed when it closes. Deleting an app deletes its owners group last, so its other groups' requests still resolve to the app owners. The notification plugin docs describe what approvers holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Assigned to Me" lists now show exactly the requests whose assigned reviewers include the user, as one SQL condition per request row. Each list had its own approximation: the access list showed every request for a group or app the user owned, whatever the level; the group list showed admins every pending request; and the role list loaded pending requests into Python to work out the disallow_self_add cases. A test checks that the list, the notified reviewers, and the resolve routes' permission checks agree for every routing scenario. A disallow_self_add tag that reaches a role only by propagation no longer hides a request, since it does not block the approval either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app owners level looks up an app's owners group by app_group.app_id for every request row it checks. Postgres does not index foreign key columns, so each check scanned app_group; the new index removes that scan. On Postgres with 100k access requests and 20k role requests, an admin's "Assigned to Me" went from 11 s to 57 ms (access, pending), 99 s to 515 ms (access, all), 79 s to 24 ms (role, pending), and over 300 s to 218 ms (role, all). Most of the gain comes from the reviewer rule keeping each owner level a single flat EXISTS, which Postgres runs as a semi-join; the index removes the per-row scan of app_group that remained. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each kind of request gets a GET /reviewers route that returns its eligible reviewers grouped by owner level, nearest first, with each user at their nearest level. The first level is the one the request is assigned to; the rest are who a requester can contact to escalate. Like the other request reads, the routes need authentication but no further permission. The generated API client is regenerated to include them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pending request's page lists its eligible reviewers from the new route, one table per owner level, nearest first, with the assigned level marked. The access and role request pages rebuilt the owner chain in the browser from group and app ownership queries, so what they showed could differ from who was notified; they now show the same reviewers the backend assigns. The list is shown to viewers who cannot resolve the request, and to the requester beside their own request. Someone who can approve gets the resolve form instead, and the page skips fetching the reviewers for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user guide gains a "Who reviews a request?" entry describing the owner levels, the requester and disallow_self_add exclusions, and the reviewer list on each request page; the entries on creating and responding to requests link to it. The "Assigned to Me" tooltips say the requests are assigned to you for review rather than awaiting your approval. The contributor guide points the approver routing section at the one definition of reviewers and the snapshot taken before ownership changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
barborico
force-pushed
the
brynna/request_reviewer_routing
branch
from
October 16, 2026 22:36
a0fb96b to
3054a29
Compare
…er_routing # Conflicts: # README.md
…er_routing # Conflicts: # src/pages/role_requests/Read.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Who's responsible for reviewing a request (all kinds) is now defined once, in
api/models/request_reviewers.py: the eligible reviewers at the nearest owner level that has any. Notifications (open and close), all three "Assigned to Me" views, and an updated reviewer panel on the request detail pages use this definition.Urgency: 2 weeks?
Expected review effort: HIGH (~1h)
Motivation
I noticed that, as an Access admin, I'm assigned a number of "confused deputy" reviews. This led me to discover that the reviewer chain was implemented separately in seven places that disagreed:
My intent here is one principle, reflected identically in "Assigned to Me", notification routing, and the UI. The one place that should probably be brought into alignment but was left out of scope for now is endpoint authZ.
The Routing Principle
The owner levels, nearest first:
Who counts as an eligible reviewer at a level:
disallow_self_add_*tag. A role member approving a role-based request would be adding themselves; this mirrorsCheckForSelfAdd.execute_for_role.Screenshots
The pending request details view includes a list of possible reviewers, e.g.:
Description of Changes
api/models/request_reviewers.pyget_assigned_reviewers(request)("who reviews this") andis_assigned_reviewer(Model, user_id)("which requests does this user review").get_eligible_reviewers_by_levellists the eligible reviewers per level, nearest first, each user once at their nearest level; the first level is the assigned one.get_all_possible_request_approversandapi/models/access_request.pyare removed.ModifyGroupUsers,ModifyRoleGroups,DeleteGroup, andUnmanageGroupcapture them before changing ownership.RejectAccessRequestandRejectRoleRequestgain an optionalassigned_reviewerskeyword.DeleteAppdeletes the app's owners group last, so the other groups' close notifications still see the app owners.is_assigned_reviewer. This replaces three divergent blocks, including a loop over every app.GET /api/{requests,role-requests,group-requests}/{id}/reviewersreturnreviewers_by_level: the eligible reviewers by owner level, nearest first. The first level is the assigned one; an empty list means no one can review the request.src/components/RequestReviewers.tsxrenders them on all three request pages: one table per level, with an "Assigned" chip on the first. Only people who can act on the request are listed, so the requester and tag-blocked owners don't appear.EXISTS, so Postgres can use a semi-join.app_group.app_id.NotificationPluginSpec, the README, and.claude/CLAUDE.md("Approver routing") are updated.Behavior changes operators will see
approvers, and tag-blocked app owners are no longer notified of role requests.approversin*_completedhooks is alist(it was aset).app_group.app_id, built non-concurrently on a small table.approversparameter name, and thegroup_ownersaudit-log field are unchanged. API changes are additive.Validation of Changes
tests/test_request_reviewers.py/reviewers: shape, 404, 401.Every commit passes on its own: ruff, ty, pytest (1235 at the tip, rebased on main), vitest (206),
tsc --noEmit, and prettier.Postgres: reviewer tests pass on a local 15.6 with
SAWarningas errors, as does the full suite; the request and reviewer test files also pass on Postgres 16. Migrations pass on SQLite and Postgres: upgrade,alembic check, then downgrade -1 and upgrade again.Benchmark: an admin's "Assigned to Me" count queries on local Postgres. Dataset: 20k users, 234k memberships, 100k access and 20k role requests.
Non-admin lists stay under 16 ms.
Manual: with seed data, the requester viewed an access, role, and group request page. Each panel listed only eligible reviewers, nearest level first and marked as assigned (see the screenshots).
I ran Matt Pocock's PR review skill and found no violations.
Guidance for Reviewers
🤖 Generated with Claude Code