feat(admin): Operators page + readable org names (console clarity)#963
Merged
Conversation
Adds a dedicated admin-only "Operators" page to the control-plane admin console for managing who can sign in and at what role (admin/viewer), over the existing but previously UI-less /api/v1/operators API. Until now the first admin had to be minted by hand via the break-glass token. Disambiguates the two "users" concepts that were easy to confuse: - "Org Users" (renamed from "Users") — per-org *database* login accounts customers use to connect to their warehouse (PG wire / Flight SQL). - "Operators" (new) — PostHog staff who administer this console. Both pages now carry explicit descriptions and a reciprocal cross-link so "users" is never ambiguous. Backend: narrow the operators handler to an operatorStore interface so the last-admin lockout guard is unit-testable (operators_api_test.go), mirroring the existing apiStore pattern. No behavior change. Frontend: new Operators.tsx (list / add / change-role / revoke, admin-gated, self-lockout warnings); route + nav entry; and gate useOperators on isAdmin (403-tolerant) like useDucklingDrift. Vitest coverage in Operators.test.tsx. e2e: admin_operators harness assertion — grant / list / role-change / validation / revoke round-trip on a throwaway probe (the SPA can't be driven in-Job; the last-admin guard is unit-tested). Docs: CLAUDE.md admin section updated for the new page + the distinction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test Impact PlanDeterministic summary of how this PR changes tests, CI runners, and coverage-risk signals. Summary
Signals
Coverage risk: neutral or increased No coverage-reduction warnings detected. |
The impersonation org selector, the "you are impersonating" banner, and the write-confirm dialog all showed only org UUIDs, which are impossible to tell apart at a glance. Show the human-readable label (database name → hostname alias → UUID) as the primary identifier, with the UUID as muted secondary context, and sort the picker by label. Extract the readable-name resolution (previously inlined in Overview) into a shared `orgLabel()` helper in lib/format, unit-tested in format.test.ts and reused by Overview so there's one source of truth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the Fable review of the operators page (and a real e2e failure): - e2e admin_operators tripped the last-admin guard: the probe was promoted to admin then deleted, but this shared stack has no other admin operators, so the probe became the SOLE admin and DELETE correctly 409'd — and the row was then unremovable by design, wedging future runs. Keep the probe viewer-only end-to-end (viewers are always deletable) and use a fresh probe email. The admin / role-change / last-admin-guard paths stay covered by operators_api_test.go. - Operators revoke dialog wording: revoke does NOT block sign-in (a fresh @posthog.com SSO login re-provisions the user as a read-only viewer), so its real effect is dropping an admin back to viewer. Say that accurately instead of "they can no longer sign in". - operators_api_test.go: assert viewer GET /operators -> 403, completing the RBAC coverage across all three routes. - CLAUDE.md: sweep the stale tests/e2e-mw-dev/harness.sh path to the current tests/mw-dev/e2e/harness.sh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
Adds a dedicated admin-only Operators page to the control-plane admin console for managing who can sign in to the console and at what role (admin / viewer), over the existing
/api/v1/operatorsAPI — which had a full backend + API client + React-Query hooks but no page. Until now the first admin had to be minted by hand (break-glass internal-secret → curl the operators API); there was no in-console way to see or manage operators.Why the two "users" concepts were confusing
The console had a page called Users that manages org database login accounts (what customers use to connect to their warehouse). "Operators" (console access) is a completely different thing, and both were reasonably called "users." This PR makes the distinction unmissable:
/users/operatorsBoth pages now have explicit descriptions, an info note, and a reciprocal cross-link, so it's always clear which "users" you're looking at.
Changes
Frontend
pages/Operators.tsx: list operators (email, role, granted-by, updated), add operator, change role, revoke — admin-gated, with the signed-in user tagged you and self-lockout warnings when demoting/revoking your own admin./operatorsroute + nav entry (admin-only,ShieldCheckicon).useOperatorsnow gates onisAdmin+ tolerates 403/404 (mirrorsuseDucklingDrift), so a viewer sees an "admin only" notice instead of an error.Users.tsx→ titled Org Users, sharpened description, cross-link to Operators.Backend (no behavior change)
operatorStoreinterface (satisfied by*configstore.ConfigStore), mirroring the existingapiStorepattern, so the handler — including the last-admin lockout guard — is unit-testable.Tests
operators_api_test.go(new): list / upsert / validation (empty, wrong domain, bad role) / delete / viewer-403, and the last-admin guard (409 on demoting or deleting the final admin — the lockout-prevention path the harness can't safely exercise on a shared stack).Operators.test.tsx(new): admin sees the list + affordances + "you" tag + Org-Users cross-link; viewer sees the admin-only notice (no list, no add).admin_operatorsharness assertion: grant → list → role-change → validation → revoke round-trip on a throwaway@posthog.comprobe (self-cleaning, isolated). The operators API previously had zero e2e coverage.Docs
Verification
npm run typecheck✅ ·npm run test✅ (69/69) ·npm run build✅ ·npm run lint(no new warnings) ✅go build -tags kubernetes ./...✅ ·go vet -tags kubernetes ./controlplane/admin/✅go test -tags kubernetes ./controlplane/admin/— operators tests ✅ (only pre-existing*_postgres_test.gofail locally: they needdocker-compose, which isn't on this box)bash -n tests/mw-dev/e2e/harness.sh✅The full
admin_operatorse2e assertion runs against the real mw-dev cluster in the per-PRe2e-mw-devworkflow.Screenshot
UI change — reviewer can pull the branch and
just ui-devto view; the Operators page follows the exact layout/idiom of the existing Users page (DataTable + header + dialogs).🤖 Generated with Claude Code