Skip to content

fix: group-creation authorization, a silent save failure, and an ambiguous testid - #1514

Open
joshunrau wants to merge 3 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:fixes
Open

fix: group-creation authorization, a silent save failure, and an ambiguous testid#1514
joshunrau wants to merge 3 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:fixes

Conversation

@joshunrau

Copy link
Copy Markdown
Collaborator

Three independent bug fixes, one commit each.

fix(api): restrict group creation to administratorscloses #1468

POST /v1/groups was declared @RouteAccess({ action: 'create', subject: 'Group' }), which every
GROUP_MANAGER passes: they hold manage Group conditioned on their own groups, and JwtAuthGuard
evaluates 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.create takes no ability, and a create has no existing row to scope against.

Declared { action: 'manage', subject: 'all' } instead — option (1) from the issue, matching
PATCH /v1/setup and who the admin UI intends to expose it to.

  • Unit: groups.controller.spec.ts now reads the route's own @RouteAccess metadata and evaluates it
    against an ability built for each base permission level — the same computation the guard performs.
    It fails against the old declaration (can('create', 'Group') really is true for a group manager).
  • E2E: POST /v1/groups is back in the PRIVILEGED_REQUESTS table in authorization.spec.ts, which
    the issue had deliberately excluded.
  • .agents/docs/architecture/auth-and-permissions.md gains the route in its inventory of non-ordinary
    access declarations.

fix(web): say why a save from the user manage sheet failedcloses #1472

The 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 requiresGroup in src/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. validationSummary deduplicates them, since several blank
required fields otherwise repeat one sentence per field.

  • Unit: validationSummary in src/utils/__tests__/validation.test.ts.
  • E2E: admin-management.spec.ts seeds a groupless user over the API, edits only the email, and
    asserts the reason is visible without scrolling.

fix(web): give the subject record table a selector of its owncloses #1475

The issue reports two elements carrying data-testid="subject-table". In fact only one ever reached
the DOM: libui's DataTable does not forward data-testid, so the id on the table was inert and
getByTestId('subject-table') silently resolved to the Table tab link instead. Renaming the link
alone would have left the id matching nothing.

So the link is now subject-table-tab, and subject-table sits on a wrapper around the table, where
it actually renders — which is what the issue asks for.

  • E2E: the datahub row-action test now asserts subject-table resolves to exactly one element and
    contains the table, and that subject-table-tab is the tab link.

Verification

pnpm lint clean. pnpm test and pnpm test:e2e pass except for failures that predate this branch
point, all in the custom-identifier combobox merged in #1508: 3 unit tests in
apps/web/src/__tests__/start-session-form.test.tsx and 6 e2e tests (5 in start-session.spec.ts,
1 in datahub.spec.ts). Each was confirmed failing identically at 827f9fa93 with this branch's
changes reverted.

🤖 Generated with Claude Code

joshunrau and others added 3 commits August 25, 2026 15:27
`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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant