Skip to content

ui: tell users who are not cluster operators that the console is not for them - #1246

Merged
ac-pplx merged 4 commits into
mainfrom
ac/admin-ui-operator-gate
Nov 2, 2026
Merged

ac-pplx merged 4 commits into
mainfrom
ac/admin-ui-operator-gate

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Users who are not cluster operators now get one clear notice instead of a console full of pages that cannot load.

Every console page reads /admin/api/v1. Its middleware answers only organization admins of the cluster's managing organization, and returns 403 Caller is not a managing organization admin to everyone else. The menu nevertheless offered:

  • Overview and Sandboxes to every signed-in user;
  • Organizations and Users to any organization admin.

So a tenant org's admin, or any member, landed on pages that showed 403 toasts and empty tables, and Overview reported "0 organizations, 0 sandboxes".

Such users now see a notice in place of the page. It names the account and organization they are signed in with, says what the console requires, and offers a sign-out button. In external sign-in without a logout URL, signing out can only return to the same notice, so neither the notice nor the sidebar has a Log out button there. Both say to sign out of the identity provider instead. No admin requests are made for them.

If the cluster record fails to load, whether at sign-in or when a stored session is restored on a page load (in password and in external sign-in), an organization admin sees "Could not load cluster information" with a Retry instead of the notice, and stays signed in. Only a failed /me read ends a restored session. A failed Retry shows its error, and the next Retry tries again. For operators nothing changes. The menu items no longer carry role fields, which antd's Menu was passing through to the DOM as an unknown managingOrgAdmin attribute (a React warning on every page).

Overview also reports a failed request as an error instead of showing 0, and drops answers that arrive after the page is left.

Stack

Independent of the other open console PRs. It edits ui/src/app/dashboard/page.tsx, as #1244 does; whichever merges second needs a small rebase.

Validation

  • npm ci, npm test (84 pass, including src/lib/console-access.test.mjs and the provider-only sign-out check), npx tsc --noEmit and npm run build in ui/, on this head. The browser runs below were recorded on an earlier head of this branch; the sidebar change has unit-test coverage only.
  • A Playwright loop against a local Kind stack with synthetic data. It signs in as a managing-org admin, as a tenant org's admin and as a member, and runs every page and flow on main (production build) and on this branch (the Kind space-ui image).
    • On main, the tenant admin and the member trigger GET /admin/api/v1/orgs -> 403 and GET /admin/api/v1/sandboxes -> 403 on each page.
    • On this branch they make no admin requests and see the notice.
    • The operator's run is unchanged: no new console errors, page errors or failed requests compared with main.

Visual confirmation

A tenant org's admin opening Overview, Users and Sandboxes:

Before (main) After
before after

before: tenant admin
after: tenant admin

Videos: before, after.

Severity and risk

Severity: medium. Nothing worked for these users before. The change replaces broken pages with an explanation.

Risk: low. The gate uses the same check the backend applies: isManagingOrgAdmin, meaning role organization_admin and the user's org is the cluster's managing_org_id. If the admin API ever opens pages to tenant admins, the gate must change with it. An operator who hands the managing role to another organization (Organizations, "Make manager") sees the notice at once: the cluster record the console holds now names another managing organization, and the admin API no longer admits them either. Roll back by reverting the commits.

AI assistance: Claude Code wrote the code and this description, and ran the validation; a maintainer reviews it.

@github-actions github-actions Bot added the triage:pending Waiting for a maintainer to triage label Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thanks for opening this pull request! A SPACE maintainer will take a look.

CONTRIBUTING.md explains how we review outside contributions.

@ac-pplx
ac-pplx marked this pull request as ready for review October 31, 2026 18:43
@ac-pplx
ac-pplx added this pull request to stack #1276 November 1, 2026 04:54
zibi-pplx
zibi-pplx previously approved these changes Nov 1, 2026
@zibi-pplx
zibi-pplx dismissed their stale review November 1, 2026 22:29

Withdrawing this approval for now. Any feedback above still stands.

…for them

Every console page reads the admin API, which answers only organization
admins of the cluster's managing organization. The menu nevertheless
offered Overview and Sandboxes to every signed-in user and Organizations
and Users to any organization admin, so a tenant admin or a member signed
in to pages that failed with 403 toasts and empty tables, and Overview
showed "0 organizations, 0 sandboxes".

Such users now see one notice naming the account and organization they
are signed in with and what the console requires, with a sign-out
button, and no pages that cannot work. The menu items no longer carry
role fields, which also stops antd's Menu from passing them to the DOM.
Overview reports a failed request instead of a zero.
The operators-only gate decided from the signed-in user and the cluster
record, and treated a missing cluster record as "not an operator". The
password sign-in tolerates a failed GET /auth/v1/cluster and leaves it
null, so a managing-org admin whose cluster request failed once right
after signing in was told the console was not for them, with every page
hidden.

consoleAccess() now has three answers: operator, not an operator, and
undecided for an organization admin whose cluster record is missing. The
undecided case shows "Could not load cluster information" with a Retry
that refetches the cluster through the auth context. Members are not
operators whatever the cluster record says.
Restoring a stored session read /auth/v1/cluster with /me and discarded
the token when either failed, so a reload during a cluster-read outage
signed the operator out instead of reaching the Retry state that sign-in
already reaches. The cluster read now falls back to "unknown" there too,
and in external sign-in; only a failed /me discards the session.

A failed Retry shows its error. In external sign-in without a logout
URL, the operators-only notice drops its Log out button, which could
only return to the same notice, and says to sign out of the provider.
The overview drops answers that land after the page is left.
@ac-pplx
ac-pplx force-pushed the ac/admin-ui-operator-gate branch from f45c57f to 521216d Compare November 2, 2026 06:56
@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review — 1 finding

Low

  1. ui/src/app/dashboard/layout.tsx:141 — in external sign-in without a logout URL, the sidebar's "Log out" still produces the loop the notice is built to avoid.
    The notice drops its button when providerSignOutOnly is set (line 180), because signing out there can only return to /login. The sidebar button opens the same confirmation and calls the same logout(). That leaves the signed-in state alone and runs window.location.assign('/login') (auth.tsx:166). After the reload, the provider session answers /me, and login/page.tsx:41 sends the user straight back to /dashboard and the same notice. You can see the button at the bottom left of the "after" screenshot. Hiding the sidebar button, or swapping it for the same "sign out of " text, when providerSignOutOnly is set would make the two agree.

What I checked:

  • The gate matches the backend. require_admin_auth sits in front of every /admin/api/v1 route (routes/admin/mod.rs:293). It answers 403 unless authz::is_managing_org_admin holds, which is role organization_admin and user.org_id equal to the cluster's managing org: the same predicate as consoleAccess. Every console page loads its data through adminApi: Overview, orgs, orgs/[id], users, users/[id], api-keys, credential-rules, sandboxes, templates and nodes. sandboxes/[id] loads through getConsoleSandbox, which is also an admin read. The public-API calls (create, rollback, snapshots, template list) are on pages that can't render without that admin read. So no page that worked for a tenant admin or a member loses anything.
  • /auth/v1/cluster sits behind require_auth only (session_auth.rs:120), so non-operators can read it. Turning a failed read into null covers the transient case and doesn't hide an authorization answer. The password login() path already did .catch(() => null), so all three entry points now behave alike.
  • Every hook in DashboardLayout runs before the early return null, so the new useState calls don't change hook order between renders.

Verified against 521216d6fc415086b85ef30a3b073d24a0970ead. Reproduced locally: npm ci, npm test (83 pass, including console-access.test.mjs) and npm run build in ui/ of an isolated checkout. Pull CI on this head didn't build or test ui/. I read the auth and login flow but didn't run it in a browser.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. 1 non-blocking finding in the review comment above.

… out

External sign-in without a logout URL can only return to /login, which the
provider session signs straight back in. The cluster-operator notice already
drops its Log out button for that case, but the sidebar still offered one.
Show the same "sign out of <provider>" text there instead, via a tested helper
shared with the notice.
@ac-pplx

ac-pplx commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. fc74519 replaces the sidebar Log out with the same "Sign out of " text when only the provider can end the session, using one tested helper, so the notice and the sidebar agree.

@ac-pplx
ac-pplx added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 6f9b9e6 Nov 2, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:pending Waiting for a maintainer to triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants