Conversation
The React shell gated its own Settings and Admin Apps pages on `useAuth().isAdmin`, but no per-app control did, so a non-admin was offered create / execute / stop / retry / delete buttons that answer 403 now that the server enforces them. The gap was structural rather than an oversight per control: `useAuth` lived in `@sep/shell`, and the workspace dependency direction is shell -> apps -> framework -> api, so neither the app packages nor the framework could reach it. Exactly one app was role-aware, and only via a shell-side wrapper threading a boolean down as a prop. Move the context down the graph instead of threading the boolean across it. `@sep/api` has no `@sep` dependencies, already carries React as a peer and hosts the query hooks, and every consuming package already depends on it, so it hosts the context and `useAuth` while `@sep/shell` keeps owning the provider and all session/token state. A missing provider now resolves to a signed-out, non-admin session instead of throwing, so the many test and Storybook renders that mount these components bare keep working; a once-per-bundle dev warning keeps that from failing silently. Gated controls read a derived `canMutate`, never `isAdmin`. The server already resolves a minimum role per route rather than one administrator flag, so widening the UI to match is an edit to `deriveCanMutate` and to no call site. `isAdmin` survives only where the question really is "is this an administrator": the shell's two page guards and their `enabled: isAdmin` query suppression. Gating the framework's shared components covers every schema-driven app at once; the bespoke apps' local mutations are swept at their rendering call sites, so a hidden control issues no request and the hooks stay usable unchanged on the admin path. Selection affordances are hidden alongside the action they feed, so no bulk toolbar is left stranded. The create and edit pages keep their back chrome behind the guard state, since nothing links a read-only session there and anyone who arrives did so by URL. `SchemaListView` now drops an `actions` column with no delete handler rather than rendering a header over empty cells. Reads are untouched: the server gate keys on HTTP method, so every GET still succeeds for a non-admin, and no query suppression was added. This is what the UI advertises, never a security control -- the backend gate remains the only boundary. Also collapses the snippets prop-threading onto the hook and deletes the registry wrapper that fed it. The e2e harness mocked `isAdmin: false` while driving write controls, which the flag did not previously affect; those stubs now say admin, matching what the specs exercise. Addressed review feedback: dropped the dead `actions` column in the shared list view, kept back navigation on the page guards, moved the read-only wording into one component so it cannot drift, and added direct tests for the capability derivation and the missing-provider fallback.
nachodd
requested review from
a team,
peter-o-addo and
yyyyyyyan
as code owners
August 21, 2026 01:11
Contributor
There was a problem hiding this comment.
Pull request overview
Centralizes frontend authorization capability state and hides mutation controls from non-admin users across shared framework components and bespoke apps.
Changes:
- Moves
AuthContextanduseAuth()into@sep/api, derivingcanMutate. - Gates create, execute, edit, delete, stop, scheduling, and integration controls.
- Adds broad unit/E2E coverage and read-only messaging.
Reviewed changes
Copilot reviewed 77 out of 77 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
frontend/packages/shell/src/contexts/auth.tsx |
Uses shared API auth context. |
frontend/packages/shell/src/contexts/auth.test.tsx |
Tests capability derivation. |
frontend/packages/shell/src/appRegistry.tsx |
Removes snippets auth wrapper. |
frontend/packages/framework/src/index.ts |
Exports read-only notice. |
frontend/packages/framework/src/components/TaskHistoryTable/TaskHistoryTable.tsx |
Gates task stopping. |
frontend/packages/framework/src/components/TaskHistoryTable/TaskHistoryTable.test.tsx |
Tests stop visibility. |
frontend/packages/framework/src/components/SnippetExecutionAccordion/SnippetExecutionAccordion.tsx |
Gates snippet execution. |
frontend/packages/framework/src/components/SnippetExecutionAccordion/SnippetExecutionAccordion.test.tsx |
Tests read-only execution behavior. |
frontend/packages/framework/src/components/SchemaListView/SchemaListView.tsx |
Removes empty action columns. |
frontend/packages/framework/src/components/SchemaListView/SchemaListView.test.tsx |
Tests action-column removal. |
frontend/packages/framework/src/components/SchemaDrivenApp/SchemaDrivenApp.tsx |
Guards entity editing. |
frontend/packages/framework/src/components/SchemaDrivenApp/SchemaDrivenApp.test.tsx |
Tests entity edit access. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppTaskEditPage.tsx |
Guards task editing. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppTaskEditPage.test.tsx |
Tests task edit access. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppListPage.tsx |
Gates creation and deletion. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppListPage.test.tsx |
Tests list write controls. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppDetailPage.tsx |
Gates detail-page actions. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppDetailPage.test.tsx |
Tests detail action visibility. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppCreatePage.tsx |
Guards creation pages. |
frontend/packages/framework/src/components/SchemaDrivenApp/AppCreatePage.test.tsx |
Tests create-page access. |
frontend/packages/framework/src/components/ScheduledTasksPanel/ScheduledTasksPanel.tsx |
Gates schedule mutations. |
frontend/packages/framework/src/components/ScheduledTasksPanel/ScheduledTasksPanel.test.tsx |
Tests schedule controls. |
frontend/packages/framework/src/components/ScheduledTasksPanel/ScheduledTaskRow.tsx |
Adds read-only schedule rows. |
frontend/packages/framework/src/components/ReadOnlyNotice/ReadOnlyNotice.tsx |
Adds shared permission notice. |
frontend/packages/framework/src/components/ReadOnlyNotice/index.ts |
Exports notice component. |
frontend/packages/framework/.storybook/preview.tsx |
Supplies admin story context. |
frontend/packages/e2e/tests/tasks.spec.ts |
Updates task test session. |
frontend/packages/e2e/tests/snippets-list-filters.spec.ts |
Updates snippets test session. |
frontend/packages/e2e/tests/snippets-download.spec.ts |
Updates download test session. |
frontend/packages/e2e/tests/sidebar-navigation.spec.ts |
Updates navigation test session. |
frontend/packages/e2e/tests/shell.spec.ts |
Updates shell test session. |
frontend/packages/e2e/tests/schema-form-empty-state.spec.ts |
Updates form test session. |
frontend/packages/e2e/tests/report.spec.ts |
Updates report test session. |
frontend/packages/e2e/tests/mysql-backups.spec.ts |
Updates backup test session. |
frontend/packages/e2e/tests/inventory.spec.ts |
Updates inventory test session. |
frontend/packages/e2e/tests/inventory-sync.spec.ts |
Updates sync test session. |
frontend/packages/e2e/tests/inventory-schedules.spec.ts |
Updates schedule test session. |
frontend/packages/e2e/tests/backup_pg.spec.ts |
Updates PostgreSQL backup session. |
frontend/packages/e2e/tests/backup_mongo.spec.ts |
Updates MongoDB backup session. |
frontend/packages/e2e/tests/atw.spec.ts |
Updates ATW test session. |
frontend/packages/e2e/tests/archives.spec.ts |
Updates archives test session. |
frontend/packages/e2e/tests/app-disabled.spec.ts |
Updates disabled-app test session. |
frontend/packages/e2e/tests/alters.spec.ts |
Updates alters test session. |
frontend/packages/e2e/tests/alerts.spec.ts |
Updates alerts test session. |
frontend/packages/e2e/tests/alert-troubleshooting.spec.ts |
Updates troubleshooting session. |
frontend/packages/e2e/tests/_template.spec.ts |
Updates E2E template session. |
frontend/packages/apps/topology/src/TopologyView.tsx |
Gates topology collection. |
frontend/packages/apps/topology/src/TopologyView.test.tsx |
Tests topology access. |
frontend/packages/apps/snippets/src/SnippetsListPage.tsx |
Uses shared mutation capability. |
frontend/packages/apps/snippets/src/SnippetsListPage.test.tsx |
Tests snippets write controls. |
frontend/packages/apps/snippets/src/SnippetsApp.tsx |
Removes auth prop threading. |
frontend/packages/apps/report/tests/ReportResultPage.test.tsx |
Tests report actions. |
frontend/packages/apps/report/src/ReportResultPage.tsx |
Gates report jobs. |
frontend/packages/apps/inventory/src/SyncControl.tsx |
Gates inventory synchronization. |
frontend/packages/apps/inventory/src/SyncControl.test.tsx |
Tests sync visibility. |
frontend/packages/apps/inventory/src/InventorySchedulePage.tsx |
Gates inventory schedules. |
frontend/packages/apps/inventory/src/InventorySchedulePage.test.tsx |
Tests schedule access. |
frontend/packages/apps/inventory/src/InventoryAppNavigation.tsx |
Gates nested deletion. |
frontend/packages/apps/inventory/src/ConnectivityControl.tsx |
Gates connectivity probes. |
frontend/packages/apps/inventory/src/ConnectivityControl.test.tsx |
Tests probe visibility. |
frontend/packages/apps/dipper/src/DipperApp.tsx |
Gates diagnostic execution. |
frontend/packages/apps/dipper/src/DipperApp.test.tsx |
Tests Dipper access. |
frontend/packages/apps/atw/tests/ResultsPane.test.tsx |
Tests send controls. |
frontend/packages/apps/atw/tests/IncidentWorkspacePage.test.tsx |
Tests lifecycle controls. |
frontend/packages/apps/atw/tests/IncidentListPage.test.tsx |
Tests incident mutations. |
frontend/packages/apps/atw/tests/CollectPane.test.tsx |
Adds auth test setup. |
frontend/packages/apps/atw/tests/CollectPane.search.test.tsx |
Tests batch execution access. |
frontend/packages/apps/atw/src/ResultsPane.tsx |
Gates sending and selection. |
frontend/packages/apps/atw/src/IncidentWorkspacePage.tsx |
Gates incident lifecycle actions. |
frontend/packages/apps/atw/src/IncidentListPage.tsx |
Gates incident mutations. |
frontend/packages/apps/atw/src/CollectPane.tsx |
Gates batch execution. |
frontend/packages/apps/alerts/tests/AlertsListPage.test.tsx |
Tests alert write controls. |
frontend/packages/apps/alerts/src/AlertsListPage.tsx |
Gates alert mutations. |
frontend/packages/api/tests/auth-context.test.ts |
Tests shared auth fallback. |
frontend/packages/api/src/index.ts |
Exports auth context API. |
frontend/packages/api/src/auth-context.ts |
Defines shared auth capability state. |
changelog.d/SEP-1844.changed.md |
Documents read-only UI behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ery flag Address review feedback. The snippet accordion withheld its execute form from a read-only session by disabling the schema query, but a disabled query still serves a cached entry. The schema is held with `staleTime: Infinity` under a key carrying no identity, and the shell never clears the query cache, so an admin's fetch would render the form for a non-admin opening the same snippet later in the same tab. The render now gates on `canMutate` as well; the query flag stays as the request optimization. Covered by a test that populates the cache as an admin and re-renders read-only, which fails without the render gate. Also restores two e2e session fixtures to non-admin. The snippet-download spec exercises a GET that stays permitted, and the sidebar-navigation specs only navigate and assert readable sentinels, so leaving both non-admin keeps end-to-end coverage of the read-only experience this change creates rather than broadening privileges the specs never use.
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.
What
A non-admin signed in to SEP saw create / execute / stop / retry / delete controls that answer 403 now that the server enforces per-route roles. This hides them, so the UI offers only what the session can actually do.
The gap was structural, not an oversight per control.
useAuthlived in@sep/shell, and the workspace dependency direction isshell -> apps -> framework -> api— neither the app packages nor the framework could reach it. Exactly one app (snippets) was role-aware, and only because a shell-side wrapper read the flag and threaded it down as a prop.How
The context moved down the graph rather than the boolean being threaded across it.
@sep/apihas zero@sepdependencies, already declares React as a peer and hosts the query hooks, and every consuming package already depends on it — so no new workspace package and nopackage.jsonchanges. It now ownsAuthContext+useAuth()(frontend/packages/api/src/auth-context.ts);@sep/shellkeeps the provider and all session/token state, and re-exportsuseAuthso shell-local imports are unchanged.A missing provider resolves to a signed-out, non-admin session instead of throwing, so the many test and Storybook renders that mount these components bare keep working. That trades a loud failure for a silent one, so a once-per-bundle dev-mode warning fires when it happens.
Gated controls read
canMutate, neverisAdmin. The derivation lives in one function,deriveCanMutate.isAdminsurvives only where the question really is "is this an administrator": the shell's Settings and Admin Apps guards and theirenabled: isAdminquery suppression. (TheAtwAppregistry wrapper still passesisAdminas a prop — that one gates a link to the Settings page, an admin-identity concern, and was left alone.)Gating the framework's shared components covers every schema-driven app at once. The bespoke apps are swept at their rendering call sites, not on the hooks, so a hidden control issues no request and the hooks stay usable unchanged on the admin path.
AppCreatePage,AppTaskEditPage,SchemaDrivenApp's entity edit (whole-page guard);AppListPagecreate + row delete;AppDetailPageexecute / edit / delete, and the whole action bar when nothing is left in it;ScheduledTasksPanel+ScheduledTaskRow;TaskHistoryTablestop;SnippetExecutionAccordionexecute form;SchemaListViewactions columnTaskHistoryTableReads are untouched. The server gate keys on HTTP method, so every GET still succeeds for a non-admin and no new query suppression was needed. One query is now avoided: the snippet accordion no longer fetches a form schema it will not render.
snippets collapses onto the hook and the bespoke
SnippetsAppwrapper inappRegistry.tsxis deleted.Hiding a control is not enforcement. The backend gate stays the only boundary.
Acceptance criteria
useAuth()andisAdminimportable by@sep/frameworkand everyapps/*package with no dependency on@sep/shell; same module exportscanMutate.UNAUTHENTICATED_SESSIONinstead of throwing.&& canMutateon existing markup, so the admin branch is byte-identical.canMutate; a repo-wide check shows noisAdminleft in a mutation decision.Notes for the reviewer
SEP-1850 has already merged (
214e45f52), so the server resolves a minimum role per route andUser.roleis already on the wire. AC9 assumes it has not. I keptderiveCanMutateasisAdminper the ticket: most unsafe routes still requireadmin, so keying on a lesser role here would put back the controls that answer 403. The one real consequence is that aneditor— who the server would accept on Alert Templates' push and restore — is offered no button for them. Widening this is now a change to that one function plus a per-control minimum role, which is exactly the seam this PR builds. Called out in the derivation comment and the changelog fragment.Two judgement calls beyond the ticket, both needed to keep the suite honest:
isAdmin: falsewhile clicking create / execute / delete. The flag did not previously affect app pages; now it does. Flipped to admin, which is what those specs were written to exercise.Out of scope, per the ticket: reporting a refusal when a 403 still happens (SEP-1845), granular per-action permissions, redesigning the sparse read-only pages that remain, and server-driven per-app capability flags.
One tradeoff recorded in the module doc: the session and its derived capability share one context, so a silent-refresh token rotation re-renders capability consumers too. That is a handful of controls every few minutes; splitting the capability into its own context is the fix if it ever costs more than it saves.
Test plan
pnpm -r type-check— clean across all 17 packages.pnpm -r test— 1426 passing. New both-halves coverage for every gated surface: framework (AppCreatePage,AppTaskEditPage,AppListPage,AppDetailPagetask + entity,SchemaDrivenApp,ScheduledTasksPanel,TaskHistoryTable,SnippetExecutionAccordion,SchemaListView), apps (atwlist / workspace / results / collect,alerts,report,inventorysync / connectivity / schedule,dipper,topology,snippets), plus the capability derivation and the missing-provider fallback.pnpm test:e2e— 132 passing.oxlint0 errors,oxfmt --checkclean.Reviewed by two independent reviewers before opening. One returned no defects; the other found a real one and three design points, all addressed in this branch: the shared
SchemaListViewleft a header-onlyactionscolumn for non-admins (now dropped when no delete handler is wired, with a regression test); the page guards had no way back (back chrome kept); the read-only wording was forked between the guard and the snippet accordion (one component, newinlinevariant); and the capability derivation had no test in its own package (added).