Guard infrastructure import plan approvals

- add candidate import approval and preview gate for node onboarding
- document metrics, availability, and proxy role behavior
- lock config auth reads and preserve storage metrics fallback
This commit is contained in:
rcourtman
2026-07-02 21:59:26 +01:00
parent aa1913d0bb
commit 97b9369333
26 changed files with 1013 additions and 54 deletions
@@ -111,6 +111,13 @@ shared `/api/resources` payload carries a backend-authored Proxmox workload
target linked to that node agent. Suppressing an inside-guest install cue for
that case means governed actions have a node-agent path; it must not be
presented as proof that a guest-local agent is installed.
Candidate-sourced PVE/PBS/PMG onboarding remains lifecycle-owned even when the
candidate starts in Discovery or a platform probe: the settings connection flow
may preview the monitored-system impact and credential path, but setup
commands, downloaded scripts, manual token command copies, and manual saves
must stay blocked until the operator explicitly approves the candidate import
plan. Type-first manual adds without a candidate must stay on the normal
credential-entry path and must not synthesize a discovery import plan.
Agent-facing operations-loop status wiring in `internal/api/router.go` and
`internal/api/agent_resource_context.go` is lifecycle-adjacent only because it
shares agent route infrastructure. Other handlers in `internal/api/` such as
@@ -3173,6 +3180,16 @@ That same node setup modal owner must also stay on the direct
`NodeModalStatusFooter.tsx`, `nodeModalModel.ts`, and `useNodeModalState.ts`,
rather than relying only on broad lane ownership or downstream command tests
to catch lifecycle drift in the settings surface.
Candidate-sourced PVE/PBS/PMG node setup is part of that same node setup
owner. When a connection originates from Discovery or the API platform probe,
`InfrastructureWorkspace.tsx` must pass the candidate into
`NodeCredentialSlot.tsx`, `infrastructureImportPlanModel.ts` must derive the
endpoint, identity, credential path, collection scope, monitored-system preview
request, and reset signature, and the setup surface must require explicit
operator approval before any assisted setup command, downloaded setup script,
manual token command copy, or manual save can proceed. Direct type-first adds
with no candidate remain normal credential entry and must not fabricate an
import plan.
That same Proxmox lifecycle transport now explicitly includes the shared
`frontend-modern/src/api/nodes.ts` client boundary itself: changes to setup
command or install-command request transport must carry both lifecycle proof
@@ -151,6 +151,23 @@ configured admin role may pass admin gates. Installations that intentionally
make every proxy-authenticated user an admin must do that by leaving the role
header unset and protecting Pulse at the proxy/IdP layer.
Local credential auth reads are also part of this shared boundary. Handlers in
`internal/api/auth.go` and `internal/api/router.go` that compare `AuthUser` and
`AuthPass` must snapshot those mutable config fields while holding
`config.Mu.RLock()` before running constant-time username or password-hash
checks. CSRF bootstrap checks may snapshot local auth, API-token, and proxy
secret presence under that same lock, then evaluate SSO provider state outside
the config lock.
Candidate import plans for PVE/PBS/PMG onboarding are API-contract consumers,
not local UI estimates. Probe and Discovery candidates must build their
preview through `frontend-modern/src/api/monitoredSystemLedger.ts` and
`POST /api/license/monitored-system-ledger/preview`, preserving source,
resource type, name, hostname, host URL, and resource id in the request. The
frontend may render the preview and approval state, but it must not persist a
candidate or call node setup/save APIs until the operator has approved the
current endpoint, credential path, and collection scope.
Assistant chat `workflow_state` events are a shared AI/API payload boundary.
The backend source of truth is `internal/ai/chat.WorkflowStateData`, generated
to `frontend-modern/src/api/generated/aiChatEvents.ts` by
@@ -193,6 +193,15 @@ deep-link flow. The self-hosted `Plans` page is not a deep-link-only flow:
`system-billing` stays visible whenever commercial presentation is allowed,
while demo or commercial-suppressed sessions may still hide it.
Candidate import-plan presentation inside the Infrastructure settings dialog is
a shared primitive composition boundary. `NodeCandidateImportPlan.tsx` may use
shared `Button`, checkbox styling, lucide icons, and
`MonitoredSystemImpactPreview`, while `InfrastructureWorkspace.tsx` owns the
route-backed dialog state that feeds probe or Discovery candidates into the
credential slot. That surface must keep the approval card readable inside the
existing dialog body and must not introduce a second nested modal, detached
wizard shell, or page-local preview renderer for monitored-system impact.
Frontend localization is a shared primitive boundary. Locale support must flow
through typed message catalogs with an English fallback and explicit seed
locale coverage rather than page-local string switches.
@@ -371,6 +371,14 @@ truth for live infrastructure data.
## Current State
The monitoring-owned storage metrics runtime must preserve store-backed storage
chart continuity during resolver warm-up. `syncUnifiedStorageMetrics` must
prefer the resolver's canonical storage metrics target when it exists, but must
fall back to the storage resource id instead of dropping the resource when the
resolver has not yet produced a storage target. The fallback is a metrics
continuity path for canonical storage resources, not a second storage identity
or recovery-source model.
That same reloadable multi-tenant monitor boundary also owns instance-wide
notification settings fan-out. `ForEachMonitor` visits every live tenant
monitor so callers can propagate the webhook security allowlist and public
@@ -204,6 +204,12 @@ regression protection.
fetches, Discovery probes, or page-local external-link logic on the table
hot path.
4. Keep shared auth gating in `internal/api/router.go` cheap and local: pre-auth quick-setup and recovery routing may short-circuit on loopback/session/token checks, but they must not trigger chart, metrics, or broad persistence fan-out on the protected request hot path.
Reading mutable auth configuration for CSRF bootstrap and login checks must
stay a short in-memory snapshot under `config.Mu.RLock()`: local
username/password presence, API-token presence, and proxy-auth secret
presence may be copied while locked, while SSO provider presence is checked
after the lock is released. This preserves the protected hot path and avoids
holding the config lock across provider/state work.
The same rule applies to public setup-script lifecycle routes: `/api/auto-register`
and `/api/auto-unregister` may bypass the global auth wall so their handlers can
validate request-body setup tokens, but the router-level public-path check must
@@ -234,6 +234,12 @@ existing resource-policy redaction still governs any model-bound context.
Pulse Mobile token scopes, bypass the server-minted credential requirement,
or expose relay secrets beyond the existing public onboarding diagnostics.
4. Change security/auth/token transport behavior through the shared `frontend-modern/src/api/security.ts`, `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, `frontend-modern/src/components/Settings/useAPITokenManagerState.ts`, `internal/api/security.go`, `internal/api/security_tokens.go`, and `internal/api/system_settings.go` boundary.
Local username/password verification in `internal/api/auth.go` and
`internal/api/router.go` must snapshot `AuthUser` and `AuthPass` under
`config.Mu.RLock()` before comparison. Security/privacy may consume that
shared auth result, but it must not read mutable credential fields outside
the shared config lock or hold the lock across password hashing, session
mutation, SSO provider checks, or response writing.
Release metadata surfaced through `/api/version` remains outside token,
auth, and privacy state. Adding or changing `agentUpdateTargetVersion`
must stay limited to non-secret deployable release identity and must not
@@ -239,6 +239,11 @@ Storage/recovery auth-adjacent changes may consume SSO-authenticated sessions,
but they must not reinterpret SAML or multi-provider SSO availability as a
storage/recovery entitlement; that provider-route and license truth belongs to
the shared API/security boundary.
Storage/recovery may consume authenticated chart and report routes through the
shared API boundary, but it must not own router-level auth configuration reads.
Those reads stay API/security-owned and must snapshot mutable auth credentials
under `config.Mu.RLock()` before storage-adjacent routes rely on the resulting
session or API-token decision.
Storage/recovery may also consume org-scoped session identity from the shared
API boundary, but durable user IDs remain the authorization principal. Contact
email may support display or legacy lookup only; storage and recovery surfaces