fix: group-creation authorization, a silent save failure, and an ambiguous testid - #1514
Open
joshunrau wants to merge 3 commits into
Open
fix: group-creation authorization, a silent save failure, and an ambiguous testid#1514joshunrau wants to merge 3 commits into
joshunrau wants to merge 3 commits into
Conversation
`POST /v1/groups` was declared `@RouteAccess({ action: 'create', subject: 'Group' })`, which
every `GROUP_MANAGER` passes. A group manager holds `manage Group` conditioned on their own
groups, and `JwtAuthGuard` evaluates the declaration against the subject *type*, for which CASL
ignores a rule's conditions. Sibling routes survive this because their service re-checks against
real rows through `accessibleQuery`; `GroupsService.create` takes no ability, and a create has no
existing row to scope against, so nothing narrowed it.
Declare it `{ action: 'manage', subject: 'all' }` instead, matching `PATCH /v1/setup` and who the
admin UI intends to expose it to.
Closes DouglasNeuroInformatics#1468
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3jRKqX2Dh4pnfetVsHFXb
Submitting the admin "Manage" sheet with an invalid field fired no request, no toast and no top-level error: the only signal was an inline message next to the rejected field, which sits below the fold of a scrolling sheet the admin submits from the top of. A groupless non-admin user -- which the API still accepts -- made this reachable on a change as ordinary as editing an email, and the submit read as a no-op. Report a failed submit in the sheet header, which does not scroll, listing the distinct reasons the data was rejected. The create and update schemas already agree through the shared `requiresGroup`, so what was left of DouglasNeuroInformatics#1472 was the invisible failure. Closes DouglasNeuroInformatics#1472 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3jRKqX2Dh4pnfetVsHFXb
The Table tab link and the record table both declared `data-testid="subject-table"`. Only the link ever carried it: libui's `DataTable` does not forward `data-testid` to the DOM, so a suite selecting the record table by that id silently got the tab link instead. Name the link `subject-table-tab` and put `subject-table` on a wrapper around the table, where it actually renders. Closes DouglasNeuroInformatics#1475 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3jRKqX2Dh4pnfetVsHFXb
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.
Three independent bug fixes, one commit each.
fix(api): restrict group creation to administrators— closes #1468POST /v1/groupswas declared@RouteAccess({ action: 'create', subject: 'Group' }), which everyGROUP_MANAGERpasses: they holdmanage Groupconditioned on their own groups, andJwtAuthGuardevaluates the declaration against the subject type, where CASL ignores a rule's conditions. Sibling
routes survive that because their service re-checks against real rows through
accessibleQuery;GroupsService.createtakes no ability, and a create has no existing row to scope against.Declared
{ action: 'manage', subject: 'all' }instead — option (1) from the issue, matchingPATCH /v1/setupand who the admin UI intends to expose it to.groups.controller.spec.tsnow reads the route's own@RouteAccessmetadata and evaluates itagainst an ability built for each base permission level — the same computation the guard performs.
It fails against the old declaration (
can('create', 'Group')really istruefor a group manager).POST /v1/groupsis back in thePRIVILEGED_REQUESTStable inauthorization.spec.ts, whichthe issue had deliberately excluded.
.agents/docs/architecture/auth-and-permissions.mdgains the route in its inventory of non-ordinaryaccess declarations.
fix(web): say why a save from the user manage sheet failed— closes #1472The half of this issue about the two schemas disagreeing was already closed: both the create form and
the manage sheet require a group through the shared
requiresGroupinsrc/utils/validation.ts.What was left is the reported symptom — submitting the sheet fired no request, no toast and no
top-level error. Groups is the last section of a scrolling sheet, so the only signal sat below the
fold and the submit read as a no-op. This is reachable on a change as ordinary as editing an email,
for any groupless non-admin user (which the API still accepts).
A failed submit now reports itself in the sheet header, which does not scroll, listing the
distinct reasons the data was rejected.
validationSummarydeduplicates them, since several blankrequired fields otherwise repeat one sentence per field.
validationSummaryinsrc/utils/__tests__/validation.test.ts.admin-management.spec.tsseeds a groupless user over the API, edits only the email, andasserts the reason is visible without scrolling.
fix(web): give the subject record table a selector of its own— closes #1475The issue reports two elements carrying
data-testid="subject-table". In fact only one ever reachedthe DOM: libui's
DataTabledoes not forwarddata-testid, so the id on the table was inert andgetByTestId('subject-table')silently resolved to the Table tab link instead. Renaming the linkalone would have left the id matching nothing.
So the link is now
subject-table-tab, andsubject-tablesits on a wrapper around the table, whereit actually renders — which is what the issue asks for.
subject-tableresolves to exactly one element andcontains the table, and that
subject-table-tabis the tab link.Verification
pnpm lintclean.pnpm testandpnpm test:e2epass except for failures that predate this branchpoint, all in the custom-identifier combobox merged in #1508: 3 unit tests in
apps/web/src/__tests__/start-session-form.test.tsxand 6 e2e tests (5 instart-session.spec.ts,1 in
datahub.spec.ts). Each was confirmed failing identically at827f9fa93with this branch'schanges reverted.
🤖 Generated with Claude Code