mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 03:58:56 +00:00
53e6de7db9
# Phase 9 — approval-bypass closure (Decision 9, option a)
* Migration 000033_approval_kinds.up.sql: ALTER TABLE
issuance_approval_requests ADD COLUMN approval_kind +
payload JSONB; relax certificate_id + job_id to nullable;
CHECK (approval_kind IN ('cert_issuance','profile_edit'))
+ CHECK (per-kind nullability invariant) + index on
approval_kind. Idempotent throughout via DO blocks.
* domain.ApprovalKind enum (cert_issuance / profile_edit) +
IsValidApprovalKind. ApprovalRequest gains Kind +
Payload []byte for the pending profile diff.
* postgres.ApprovalRepository.Create + scanApprovalRow extended
to round-trip the new columns; certificate_id + job_id
switched to sql.NullString so profile_edit rows persist
cleanly. Default Kind=cert_issuance preserves back-compat
for every Phase-7-2026-05-03 caller.
* ApprovalService.RequestProfileEditApproval: new entry point
that creates a pending profile-edit row carrying the
serialized profile diff. Bypass mode (CERTCTL_APPROVAL_BYPASS)
short-circuits the same way it does for cert_issuance.
* ApprovalService.SetProfileEditApply hook: cmd/server/main.go
registers a closure that deserializes req.Payload + persists
via profileRepo.Update + emits a profile.edit_applied audit
row with category=auth. The hook avoids the Approval ↔
Profile import cycle.
* ProfileService.UpdateProfile: gates when (a) the live
profile carries RequiresApproval=true, OR (b) the proposed
edit would set it true. Returns ErrProfileEditPendingApproval
with the new approval ID; ProfileHandler maps to HTTP 202
Accepted + {pending_approval_id}. Both arms close the
flip-flop loophole because every transition through an
approval-tier profile fires the gate.
* TestProfileEdit_RequiresApprovalLoopholeClosed pins all 3
bypass attempts (flip-off / kept-on / flip-on) gated; nil-
approval-service preserves pre-Phase-9 direct-apply for
test fixtures.
* Approval service tests gain 4 profile_edit rows: pending row
shape; same-actor self-approve rejected with
ErrApproveBySameActor (load-bearing two-person integrity);
approve fails-closed when apply callback unwired;
apply callback invoked on approve.
* docs/reference/profiles.md (new) explains the gate +
edit response shape (202) + same-actor invariant + bypass
+ audit hooks.
# Phase 10 — RBAC management GUI
* useAuthMe hook (web/src/hooks/useAuthMe.ts): TanStack Query
fetches /api/v1/auth/me on app boot, caches for 60s, exposes
hasPerm(p) + hasAnyPerm + isAdmin predicates. Every Phase-10
page consumes this on mount + gates affordances against the
cached effective_permissions slice. Server-side enforcement
is the load-bearing gate; client-side hide/disable is UX.
* New routes:
- /auth/roles — list (auth.role.list); create-role modal
(auth.role.create) hidden when missing.
- /auth/roles/:id — detail + permissions; edit
(auth.role.edit), delete (auth.role.delete), add/remove
permission affordances each gated.
- /auth/keys — list of every actor with role grants; assign
+ revoke modals (auth.role.assign). actor-demo-anon
flagged system-managed; mutation buttons hidden for it.
- /auth/settings — stub showing /v1/auth/me identity +
bootstrap-endpoint availability via /v1/auth/bootstrap.
* AuditPage extended with category filter ('All categories'
+ the 3 enum values from migration 000032). Selection flows
to the API call params + the URL-driven query state.
* Layout: 3 new nav entries (Roles / API Keys / Auth Settings).
* api/client.ts: 12 new exported functions for the RBAC
surface (authMe, list/get/create/update/delete role,
list/add/remove role permissions, list keys, assign/revoke
key role, bootstrap-availability probe).
* data-testid attributes on every interactive element so a
future Playwright suite can assert behavior without brittle
CSS selectors.
* Empty state, error state, and unsaved-changes warnings on
every form per the prompt's implementation rules.
# Frontend tests
* RolesPage.test.tsx (6 tests): list render, empty state,
error state, hide-create-button-without-perm,
show-create-button-with-perm, submit-create-modal.
* KeysPage.test.tsx (3 tests): demo-anon flagged
system-managed (no buttons), permission-gated affordance
hide for auditor caller, assign-modal-POST contract.
* AuthSettingsPage.test.tsx (2 tests): identity surface,
bootstrap-OPEN-status surface.
* AuditPage.test.tsx (+1): category-filter select renders
with the 4 documented options.
15 frontend tests total in src/pages/auth/ + the audit
category-filter test; all pass via npx vitest run.
# Verifications
* go vet ./... clean.
* staticcheck across internal/auth + handler + router + cli +
service + repository + cmd + domain: clean.
* gofmt -l clean repo-wide.
* go test -short -count=1 green across internal/service,
internal/api/handler, internal/api/router, internal/auth,
internal/auth/bootstrap, internal/service/auth,
internal/domain/auth, cmd/server, cmd/cli, internal/cli.
* npx tsc --noEmit clean.
* npm run build green (vite build produces dist/index.html
+ 946KB JS bundle; chunk-size warning is pre-existing).
* npx vitest run src/pages/auth/ src/pages/AuditPage.test.tsx
green (15 tests, 4 files).
132 lines
7.0 KiB
TypeScript
132 lines
7.0 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 './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. */}
|
|
<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>
|
|
</Routes>
|
|
</BrowserRouter>
|
|
</AuthGate>
|
|
</AuthProvider>
|
|
</QueryClientProvider>
|
|
</ErrorBoundary>
|
|
</StrictMode>
|
|
);
|