mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-13 05:18:52 +00:00
64ad8e525c9c823fcc01fbd16e4982169cbe0aec
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
64ad8e525c |
feat(gui/oidc): Test Connection panel on create + edit forms (MED-5 GUI half)
Audit 2026-05-11 Fix 09 closure. MED-5's backend dry-run endpoint
(POST /api/v1/auth/oidc/test, gated auth.oidc.create) shipped on
dev/auth-bundle-2 (commit
|
||
|
|
cc8024932b |
feat(gui/oidc): expose AllowedEmailDomains on create + edit forms (A-3)
The CRIT-5 closure (2026-05-10) made `OIDCProvider.AllowedEmailDomains`
load-bearing on the OIDC login path: a token whose email domain isn't in
the configured allowlist gets ErrEmailDomainNotAllowed. But the GUI never
exposed the field — `web/src/pages/auth/OIDCProvidersPage.tsx`'s create
form had zero inputs for it, and `OIDCProviderDetailPage.tsx` neither
rendered nor edited the value.
For multi-tenant IdPs (Auth0, Azure AD common endpoint, Google Workspace)
this is the single most important provider knob — the difference between
"anyone in any tenant of this IdP can log in" and "only @acme.com can log
in." Operators driving certctl from the GUI had no way to know the field
exists, let alone set it. Same shape as CRIT-5's pre-closure state: the
control was claimed, persisted, accepted via API, but invisible at the
surface 90% of operators actually use.
Closure across both GUI pages:
web/src/pages/auth/OIDCProvidersPage.tsx
- Create modal gains a chip-style multi-input below fetch_userinfo.
- New exported `validateEmailDomain(s)` mirrors the backend validator
(CRIT-5 closure rules: no @ / no whitespace / no wildcards /
lowercase only / must be FQDN). Returns "" on accept, a
non-empty error string on reject. Server is still the source of
truth — server-returned 400s render via the existing error UI.
- Inline "addEmailDomain" handler: trim → lowercase → validate →
dedupe → push onto form.allowed_email_domains. Enter key in the
input adds the entry without requiring a click on Add.
- Each chip carries a × remove button + data-testid plumbing for
E2E coverage.
web/src/pages/auth/OIDCProviderDetailPage.tsx
- Read-only view's <dl> renders a new row "Allowed email domains"
with an explicit "any (no gate configured)" sentinel when the
list is empty. Operators can tell the difference between "not
configured" and "field exists but the GUI doesn't show it" — the
whole class of lying-field this fix exists to retire.
- Edit form mirrors the create-modal chip control + pre-populates
from provider.allowed_email_domains at startEdit time (defensive
clone so chip mutations don't reach through into the cached
TanStack Query data).
- Save round-trips the trimmed list as `allowed_email_domains` in
the PUT body alongside the other editable fields.
- "Clear all" affordance with a confirm() dialog that warns about
removing the tenant gate (cross-tenant logins permitted after
save) — for operators who want to test enforcement-off then turn
back on without retyping the full domain list.
- Imports `validateEmailDomain` from OIDCProvidersPage for parity.
web/src/api/client.ts
- No changes — `allowed_email_domains?: string[]` was already in
both OIDCProvider and OIDCProviderRequest types. The CRIT-5
backend closure had already shipped the type but no GUI consumer
ever used it.
Regression coverage (Vitest, all passing):
OIDCProvidersPage.test.tsx (7 new):
AllowedEmailDomains — Add persists a chip and is included in submit body
AllowedEmailDomains — rejects entries containing @
AllowedEmailDomains — rejects wildcard entries
AllowedEmailDomains — normalizes mixed-case input to lowercase
AllowedEmailDomains — Enter key adds the entry without clicking Add
AllowedEmailDomains — chip × button removes the entry
AllowedEmailDomains — duplicate entry is rejected
validateEmailDomain unit suite (7 new):
accepts a plain lowercase FQDN (with multi-label TLDs)
rejects entries containing @ (with leading-@ variant)
rejects entries with whitespace (with tab variant)
rejects wildcards (with both *.x and x.* variants)
rejects mixed-case
rejects bare hostnames (no dot)
rejects empty strings
OIDCProviderDetailPage.test.tsx (5 new):
AllowedEmailDomains — read-only view shows configured entries
AllowedEmailDomains — read-only view shows "any" sentinel when empty
AllowedEmailDomains — edit form pre-populates + PUT round-trips
AllowedEmailDomains — removing a chip and saving submits the trimmed list
AllowedEmailDomains — Add validates against backend rules
Verify gate green: `tsc --noEmit` clean across the web/ tree;
OIDCProvidersPage + OIDCProviderDetailPage suites pass all 29 tests
(19 + 10) — 13 of those are new A-3 cases, 16 were existing CRIT-5 /
Bundle 2 Phase 8 coverage. Three pre-existing test failures in
AuthSettingsPage.test.tsx + KeysPage.test.tsx confirmed unrelated
(reproduce on the base commit `191384c` without any of this fix's
changes applied; not in scope for this CRIT fix).
Spec at cowork/auth-bundles-fixes-2026-05-11/03-crit-allowed-email-domains-gui.md
Closure annotation appended to CRIT-5 row of cowork/auth-bundles-audit-2026-05-10.md;
Lying-fields cross-reference table row #1 marked closed across both
the backend (CRIT-5, 2026-05-10) and GUI (A-3, 2026-05-11) legs.
Operator advisory in CHANGELOG.md v2.1.0 release notes — operators
who provisioned OIDC providers through the GUI between v2.1.0 and
this fix should verify allowed_email_domains matches their tenant
policy (the field was configurable only via API / MCP / direct SQL
during that window).
|
||
|
|
9143003e95 |
auth-bundle-2 Phase 8: GUI auth surface (OIDC providers + group mappings + sessions + LoginPage IdP buttons + AuthState refactor + logout wiring)
Closes Phase 8 of cowork/auth-bundle-2-prompt.md. Every Bundle 2 endpoint
now has a permission-gated, data-testid-instrumented React surface.
Frontend changes
================
api/client.ts (Category H — AuthState refactor):
* fetchJSON now sends `credentials: 'include'` on every request so the
HttpOnly session cookie + the JS-readable CSRF cookie ride along with
Bearer-mode requests transparently. Mode is determined per call by
what cookies are present, NOT by a state-machine — the same client
works for Bearer-only deploys, session-only deploys, and the mixed
upgrade path described in cowork/auth-bundles-index.md Category H.
* readCSRFCookie() + isStateChangingMethod() helpers auto-attach
`X-CSRF-Token` to POST/PUT/PATCH/DELETE when the CSRF cookie exists.
Bearer-only callers ride through unchanged (no CSRF cookie → no
header → backend's CSRF middleware skips).
* AuthInfoResponse extended with optional `oidc_providers?:
AuthInfoOIDCProvider[]` matching the Phase 6 server extension.
* New API helpers (1:1 with Phase 5 / 7.5 endpoints):
- listOIDCProviders / createOIDCProvider / updateOIDCProvider /
deleteOIDCProvider / refreshOIDCProvider
- listGroupMappings / addGroupMapping / removeGroupMapping
- listSessions(actorID?, actorType?) / revokeSession / logout
- breakglassLogin / breakglassSetPassword / breakglassUnlock /
breakglassRemove
Permission gates fire server-side; the GUI predicates are UX only.
pages/auth/OIDCProvidersPage.tsx (NEW):
* Lists configured OIDC providers, gated on `auth.oidc.list`.
* Empty state + error state + loading state.
* Embedded Configure-Provider modal with form fields for name,
issuer_url, client_id, client_secret, redirect_uri,
groups_claim_path/format, fetch_userinfo, scopes. Modal hidden
unless caller has `auth.oidc.create`.
* Unsaved-changes confirmation on cancel.
pages/auth/OIDCProviderDetailPage.tsx (NEW):
* Provider config dl + edit/delete/refresh action buttons.
* Edit and refresh require `auth.oidc.edit`. Delete requires
`auth.oidc.delete`.
* Type-confirm-name delete dialog. Surfaces server's 409 Conflict
("ErrOIDCProviderInUse") inline so the operator knows to revoke
the provider's active sessions first.
* Refresh discovery cache button → POST .../refresh → server re-runs
RefreshKeys with the IdP-downgrade-attack defense from Phase 3.
* Group→role mappings link.
pages/auth/GroupMappingsPage.tsx (NEW):
* Per-provider group-claim → role-id mapping CRUD.
* Empty state explains the fail-closed semantics from Phase 3
(no mappings ⇒ no users authenticate via this provider).
* Inline add form (group_name input + role_id select populated from
`authListRoles`); add/remove gated on `auth.oidc.edit`.
pages/auth/SessionsPage.tsx (NEW):
* Default "My sessions" view available to anyone holding
`auth.session.list`.
* "All actors (admin)" toggle exposed only when caller holds
`auth.session.list.all`; renders an actor_id filter input that
threads ?actor_id= through the GET.
* Self-pill marker on the caller's own rows.
* Revoke button is shown when (a) the row is the caller's own session
(handler-side own-bypass) OR (b) caller holds `auth.session.revoke`.
* Confirms via window.confirm; surfaces revocation errors inline.
pages/LoginPage.tsx (MODIFIED):
* Fetches /v1/auth/info on mount; if `oidc_providers[]` is non-empty,
renders one "Sign in with X" button per provider linking to the
provider's `login_url` (the server-side handler in Phase 5 builds
this URL with state + nonce + PKCE verifier sealed in the pre-login
cookie; the GUI never touches those values).
* The API-key form remains as a fallback for Bearer-mode deploys and
the Phase 7.5 break-glass path.
* All interactive elements carry data-testid:
login-oidc-providers / login-oidc-button-{id} / login-api-key-form /
login-api-key-input / login-api-key-submit.
components/AuthProvider.tsx (MODIFIED):
* logout() now also fires POST /auth/logout via the api/client helper
before clearing local state. The endpoint is auth-exempt; the
catch-and-swallow keeps the local logout flow working even if the
cookie is already invalid (idempotent server-side as well).
components/Layout.tsx (MODIFIED):
* Two new nav entries under the Auth section: "OIDC Providers" + "Sessions".
main.tsx (MODIFIED):
* Four new routes:
- /auth/oidc/providers
- /auth/oidc/providers/:id
- /auth/oidc/providers/:id/mappings
- /auth/sessions
Vitest coverage
===============
Five new test files, 28 new test cases. Pattern matches Bundle 1
Phase 10's Vitest scaffold (vi.mock api/client, render with
QueryClient + MemoryRouter, authMe-driven permission shaping,
data-testid selectors).
* OIDCProvidersPage.test.tsx (5 tests): ErrorState w/o auth.oidc.list,
empty state, list + create button render, hide-create-button
without auth.oidc.create, submit-creates-via-API.
* OIDCProviderDetailPage.test.tsx (5 tests): ErrorState w/o list,
full-perms render, hide edit/refresh/delete with only list,
refresh button calls API, delete confirm-button stays disabled
until typed text matches provider name.
* GroupMappingsPage.test.tsx (5 tests): ErrorState w/o list, empty
fail-closed warning, mapping rows render, hide-form without
auth.oidc.edit, submit-add-form-calls-API.
* SessionsPage.test.tsx (6 tests): ErrorState w/o list, own sessions
+ self-pill, hide All-actors toggle without list.all, show
toggle with list.all, hide revoke on other-actor sessions without
auth.session.revoke, click-revoke calls API after window.confirm.
* LoginPage.test.tsx (extended +2 tests): renders OIDC buttons when
/auth/info reports providers; omits the OIDC block when none.
Verification
============
* `npx tsc --noEmit` — 0 errors.
* Vitest run across api/components/hooks/utils/auth/pages = 475 tests,
all green.
* `npm run build` — green (980 KB bundle, no surprises vs Phase 7).
* No backend (Go) changes in this commit; Phase 5-7.5 surfaces
consumed unchanged.
Not in this commit (deferred)
=============================
* "Test login flow" button on the provider detail page (prompt §Phase 8
optional row). Requires a server-side test=true flag on the OIDC
login handler — out of scope for the GUI commit.
* `web/src/__tests__/e2e/` Keycloak-via-testcontainers harness for the
15 comprehensive flow checks. Tracked under Phase 10 of
cowork/auth-bundle-2-prompt.md.
|