Phase 1 of the MCP Control Plane Web Console (#631) defines the model that
future gated writes must pass through, and enables none of them.
The read-only MVP (#426-#436) ships with no authentication; protection comes
from network placement alone (#435). That is adequate while every route is a
GET and inadequate the moment Phase 2 wires a write. This lands the authority
first, so no write can later be added without something to check it against.
webui/console_authz.py
Identity sources (none / local-dev / access-proxy), a four-role matrix
(viewer, operator, controller, admin), the privileged-action list, and a
fail-closed authorize(). Every action maps to a task_key in
task_capability_map, so the console cannot invent an authority the MCP layer
does not already define. Roles are always server-side configuration, never a
client assertion. Deny reasons are closed and enumerated; there is no
implicit allow branch, and even an allow reports execution_enabled=false
while ACTIVE_PHASE is 1.
webui/console_redaction.py
One redaction pass for API payloads, rendered HTML, logs, and audit records.
Reuses gitea_audit.redact as the shared authority rather than forking it,
then adds console patterns for keychain references, credential assignments,
PEM private-key blocks, and JWTs. Never raises: an unredactable value
degrades to the placeholder rather than being emitted raw.
webui/console_audit.py
Console-side audit records, which gitea_audit cannot supply: it records MCP
mutations and carries no console actor, identity source, correlation id, or
retention class, and an authorization denial is not a mutation at all. The
two are additive and join on correlation.request_id. Records are redacted at
build time, re-scanned at write time, and dropped rather than persisted if
they still trip a detector. Retention is per-record; an unknown action is
retained as privileged rather than standard.
webui/app.py
Attaches an authorization block to the existing preview and attempt routes
and records the decision. The terminal outcome is unchanged - gated_actions
still fails closed for every action - so this cannot loosen anything. Adds
GET /api/console/security-model publishing the three policies as JSON.
Probe authentication is deliberately declarative in this slice:
probe_auth_required() reports operator intent and no route consults it. The
documentation says so plainly and a regression test pins the not-enforced
status, so wiring it in Phase 2 is a deliberate change rather than a silent
one. An operator who sets the variable believing it protects a probe would be
worse off than one who knows it does not.
Tests: tests/test_webui_console_authz_audit.py - 75 passed, 93 subtests,
covering each acceptance criterion and each test the issue requires
(redaction units, default-deny for unauthenticated write stubs, audit record
creation for a simulated privileged preview).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>