Files
certctl/web/src/main.tsx
T
shankar0123 f1d97710e1 feat(gui+auth): break-glass admin GUI surface (CRIT-4 closure)
Closes CRIT-4 of the 2026-05-10 audit. Bundle 2 Phase 7.5 shipped the
break-glass backend (Argon2id + lockout + 4 endpoints) but no GUI
surface. Operators recovering during an SSO outage had to hand-craft
curl commands — operationally hostile and the opposite of what
docs/operator/security.md advertised. This commit closes the gap.

Three GUI surfaces:

1. LoginPage.tsx — inline "Use break-glass account (SSO outage
   recovery)" toggle below the API-key form. Clicking reveals an
   amber-bordered inline form (actor-id + password, autocomplete=off).
   Calls breakglassLogin(actor_id, password); on success navigates
   to "/" where AuthProvider re-validates via the session-cookie path.
   Intentionally low-visibility (text-amber-600 small text) — this is
   the deliberate-bypass path, not the everyday-login path.

2. web/src/pages/auth/BreakglassPage.tsx — admin page at /auth/breakglass
   (permission-gated by auth.breakglass.admin). Three sections:
     - Sticky security banner ("every action audited; use only during
       incidents").
     - Set/rotate-password form (≥12-char + confirm-match).
     - Credentialed-actor table with rotate / unlock (disabled when
       not locked) / remove per row. Remove requires type-the-actor-id
       confirmation.

3. Layout.tsx nav — "Break-glass" entry under the auth section. Visible
   to all callers; the page itself permission-gates (server-side 403 is
   the load-bearing defense). Cosmetic hide-when-no-perm is deferred
   to fix 14's LOW bundle.

Backend support (new endpoint required to enumerate credentialed actors):

- internal/repository/breakglass.go — BreakglassCredentialRepository
  gains List(ctx, tenantID) method.
- internal/repository/postgres/breakglass.go — postgres impl; reuses
  the existing breakglassColumns / scanBreakglass helpers.
- internal/auth/breakglass/service.go — Service.List(ctx) method;
  returns ErrDisabled when CERTCTL_BREAKGLASS_ENABLED=false (handler
  maps to 404 for surface invisibility).
- internal/api/handler/auth_breakglass.go — ListCredentials handler;
  password_hash field NEVER serialized to the wire (response shape
  is intentionally limited to actor_id + timestamps + failure_count +
  locked_until).
- internal/api/router/router.go — registers GET
  /api/v1/auth/breakglass/credentials gated by auth.breakglass.admin.
- internal/api/router/openapi_parity_test.go — SpecParityExceptions
  entry for the new endpoint (full OpenAPI row rides along with the
  next OpenAPI sweep).

GUI api/client.ts gains breakglassListCredentials() + the
BreakglassCredentialRow type matching the wire shape.

Six Vitest cases in BreakglassPage.test.tsx pin the contract:
permission gate (forbidden state when caller lacks the perm; admin
surface when they have it), set-password mismatch rejection, set-
password below-threshold-length rejection, unlock-disabled-when-not-
locked, remove-modal type-confirm.

Verification gate green:
- gofmt -l clean on all touched files
- go vet clean
- go test -short -count=1 on internal/api/router (TestRouter_OpenAPIParity
  + TestRouterRBACGateCoverage + TestRouter_AuthExemptAllowlist),
  internal/api/handler (all BCL tests + ListCredentials),
  internal/auth/breakglass (Service.List + stubRepo.List),
  internal/repository/postgres, internal/domain/auth (auditor pin)
  — all pass.

CRIT-1 + CRIT-2 + CRIT-3 from the same audit are already closed on
this branch (commits 68ca42f, ca1e135, 00eace8). CRIT-5 (AllowedEmail-
Domains lying field) remains the last Critical blocker for v2.1.0.
Spec: cowork/auth-bundles-fixes-2026-05-10/04-crit-4-breakglass-gui.md.

Refs: cowork/auth-bundles-audit-2026-05-10.md CRIT-4
2026-05-10 20:24:52 +00:00

147 lines
8.1 KiB
TypeScript

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ErrorBoundary from './components/ErrorBoundary';
import AuthProvider from './components/AuthProvider';
import AuthGate from './components/AuthGate';
import Layout from './components/Layout';
import DashboardPage from './pages/DashboardPage';
import CertificatesPage from './pages/CertificatesPage';
import CertificateDetailPage from './pages/CertificateDetailPage';
import AgentsPage from './pages/AgentsPage';
import AgentDetailPage from './pages/AgentDetailPage';
import JobsPage from './pages/JobsPage';
import NotificationsPage from './pages/NotificationsPage';
import PoliciesPage from './pages/PoliciesPage';
import RenewalPoliciesPage from './pages/RenewalPoliciesPage';
import IssuersPage from './pages/IssuersPage';
import TargetsPage from './pages/TargetsPage';
import ProfilesPage from './pages/ProfilesPage';
import OwnersPage from './pages/OwnersPage';
import TeamsPage from './pages/TeamsPage';
import AgentGroupsPage from './pages/AgentGroupsPage';
import AuditPage from './pages/AuditPage';
import ShortLivedPage from './pages/ShortLivedPage';
import AgentFleetPage from './pages/AgentFleetPage';
import DiscoveryPage from './pages/DiscoveryPage';
import NetworkScanPage from './pages/NetworkScanPage';
import HealthMonitorPage from './pages/HealthMonitorPage';
import DigestPage from './pages/DigestPage';
import ObservabilityPage from './pages/ObservabilityPage';
import JobDetailPage from './pages/JobDetailPage';
import IssuerDetailPage from './pages/IssuerDetailPage';
import IssuerHierarchyPage from './pages/IssuerHierarchyPage';
import TargetDetailPage from './pages/TargetDetailPage';
import SCEPAdminPage from './pages/SCEPAdminPage';
import ESTAdminPage from './pages/ESTAdminPage';
// Bundle 1 Phase 10 — RBAC management pages.
import RolesPage from './pages/auth/RolesPage';
import RoleDetailPage from './pages/auth/RoleDetailPage';
import KeysPage from './pages/auth/KeysPage';
import AuthSettingsPage from './pages/auth/AuthSettingsPage';
import ApprovalsPage from './pages/auth/ApprovalsPage';
// Bundle 2 Phase 8 — OIDC + session management pages.
import OIDCProvidersPage from './pages/auth/OIDCProvidersPage';
import OIDCProviderDetailPage from './pages/auth/OIDCProviderDetailPage';
import GroupMappingsPage from './pages/auth/GroupMappingsPage';
import SessionsPage from './pages/auth/SessionsPage';
import BreakglassPage from './pages/auth/BreakglassPage';
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 10_000,
retry: 1,
refetchOnWindowFocus: true,
},
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<AuthGate>
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route index element={<DashboardPage />} />
<Route path="certificates" element={<CertificatesPage />} />
<Route path="certificates/:id" element={<CertificateDetailPage />} />
<Route path="agents" element={<AgentsPage />} />
<Route path="agents/:id" element={<AgentDetailPage />} />
<Route path="fleet" element={<AgentFleetPage />} />
<Route path="jobs" element={<JobsPage />} />
<Route path="jobs/:id" element={<JobDetailPage />} />
<Route path="notifications" element={<NotificationsPage />} />
<Route path="policies" element={<PoliciesPage />} />
<Route path="renewal-policies" element={<RenewalPoliciesPage />} />
<Route path="profiles" element={<ProfilesPage />} />
<Route path="issuers" element={<IssuersPage />} />
<Route path="issuers/:id" element={<IssuerDetailPage />} />
{/* Rank 8 — operator-managed multi-level CA hierarchy.
Admin-gated at the API; the page renders the
backend's 403 as ErrorState for non-admin
callers. See docs/intermediate-ca-hierarchy.md. */}
<Route path="issuers/:id/hierarchy" element={<IssuerHierarchyPage />} />
<Route path="targets" element={<TargetsPage />} />
<Route path="targets/:id" element={<TargetDetailPage />} />
<Route path="owners" element={<OwnersPage />} />
<Route path="teams" element={<TeamsPage />} />
<Route path="agent-groups" element={<AgentGroupsPage />} />
<Route path="audit" element={<AuditPage />} />
<Route path="short-lived" element={<ShortLivedPage />} />
<Route path="discovery" element={<DiscoveryPage />} />
<Route path="network-scans" element={<NetworkScanPage />} />
<Route path="health-monitor" element={<HealthMonitorPage />} />
<Route path="digest" element={<DigestPage />} />
<Route path="observability" element={<ObservabilityPage />} />
{/* SCEP RFC 8894 + Intune master bundle Phase 9.4 (initial)
+ Phase 9 follow-up (rebrand): per-profile SCEP
Administration page with Profiles / Intune Monitoring /
Recent Activity tabs. Route is unconditional; the page
itself renders an "Admin access required" banner for
non-admin callers and skips the underlying API calls so
the server never sees a 403-prone request. */}
<Route path="scep" element={<SCEPAdminPage />} />
{/* Backward-compat alias for external bookmarks the Phase 9
release advertised. Lands on the Intune Monitoring tab. */}
<Route path="scep/intune" element={<SCEPAdminPage />} />
{/* EST RFC 7030 hardening master bundle Phase 8: per-profile
EST Administration page with Profiles / Recent Activity /
Trust Bundle tabs. Same admin-gate pattern as SCEP — the
route is unconditional; the page renders an "Admin access
required" banner for non-admin callers and skips the
underlying API calls so the server never sees a 403. */}
<Route path="est" element={<ESTAdminPage />} />
{/* Bundle 1 Phase 10 — RBAC management surface.
Every page reads /api/v1/auth/me on mount via the
useAuthMe hook and gates affordances against the
cached effective_permissions slice. Server-side
enforcement is the load-bearing layer; client-side
hide/disable is UX. */}
{/* Bundle 2 Phase 8 — OIDC + session management surface. */}
<Route path="auth/oidc/providers" element={<OIDCProvidersPage />} />
<Route path="auth/oidc/providers/:id" element={<OIDCProviderDetailPage />} />
<Route path="auth/oidc/providers/:id/mappings" element={<GroupMappingsPage />} />
<Route path="auth/sessions" element={<SessionsPage />} />
<Route path="auth/roles" element={<RolesPage />} />
<Route path="auth/roles/:id" element={<RoleDetailPage />} />
<Route path="auth/keys" element={<KeysPage />} />
<Route path="auth/settings" element={<AuthSettingsPage />} />
<Route path="auth/approvals" element={<ApprovalsPage />} />
{/* Audit 2026-05-10 CRIT-4 closure — break-glass admin surface. */}
<Route path="auth/breakglass" element={<BreakglassPage />} />
</Route>
</Routes>
</BrowserRouter>
</AuthGate>
</AuthProvider>
</QueryClientProvider>
</ErrorBoundary>
</StrictMode>
);