feat(webui): console authorization, RBAC, redaction, and audit model (Closes #633) #811
Merged
sysadmin
merged 2 commits from 2026-07-22 22:16:39 -05:00
feat/issue-633-console-authz-audit-model into master
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#811
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #633
Phase 1 of the Web Console epic #631. Its predecessor, the architecture ADR #632, landed as PR #796; this is the auth/audit foundation that the epic's phase ordering (AC3) requires before any dashboard child ships.
Problem
The read-only MVP (#426–#436) ships with no authentication. Protection comes from network placement alone, which #435 documents plainly. That is adequate while every route is a GET, and inadequate the moment Phase 2 wires a gated write.
#633 asks for the model those writes must pass through. This lands the authority before any write exists, so a write cannot later be added without something to check it against.
What this delivers
webui/console_authz.pywebui/console_redaction.pywebui/console_audit.pyGET /api/console/security-modeldocs/webui-authz-audit.mdAuthorization (
console_authz). Three identity sources (nonedefault,local-dev,access-proxy), a four-role matrix (viewer, operator, controller, admin), the privileged-action list, and a fail-closedauthorize(). Every console action carries atask_keyintotask_capability_map— the same source of truthgitea_resolve_task_capabilityand the MCP tool gates use — so the console cannot invent an authority the MCP layer does not already define. The role is always server-side configuration; a client cannot assert one. Deny reasons are closed and enumerated, there is no implicit allow branch, and even an allow reportsexecution_enabled: falsewhileACTIVE_PHASEis 1.Redaction (
console_redaction). One pass over API payloads, rendered HTML, logs, and audit records. It reusesgitea_audit.redactas the shared authority rather than forking it, then adds console patterns for keychain references, credential assignments, PEM private-key blocks, and JWTs. It never raises: an unredactable value degrades to the placeholder rather than being emitted raw.Audit (
console_audit).gitea_auditrecords MCP mutations; it has no console actor, identity source, correlation id, or retention class, and an authorization denial is not a mutation, so it would never appear there at all. The console record is additive, joined to the MCP record oncorrelation.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 (90 / 365 / 730 days); an unknown action is retained as privileged rather than standard, because for a safety control the conservative direction is to keep the record longer.Wiring (
app.py). The existing preview and attempt routes gain anauthorizationblock and emit a decision record. The terminal outcome is unchanged —webui/gated_actions.pystill fails closed for every action — so this layer cannot loosen anything. Phase 2 enforces on this same decision instead of adding a parallel check.Reviewer note: one deliberate gap, stated plainly
probe_auth_required()readsWEBUI_REQUIRE_PROBE_AUTHand no route consults it. Setting that variable changes nothing at runtime today.I found this while reviewing the module against the issue's security requirement ("fail closed on missing auth for non-public health probes if configured") and chose to document it rather than wire it: enforcing it would change the MVP
/healthcontract, which is outside this issue's accepted scope. The documentation now says it enforces nothing today, andtest_probe_auth_is_declared_but_not_yet_enforcedpins that status with a failure message pointing at the doc section to update. Wiring it in Phase 2 is therefore a deliberate change rather than a silent one.The alternative — leaving the doc implying enforcement — would leave an operator who sets the variable believing a probe is protected worse off than one who knows it is not.
Provenance of the work
The worktree for this issue held roughly 1,900 lines of uncommitted, unpublished work from an earlier session whose lease had expired and whose owning process was dead. The control plane reported the issue unclaimed (
gitea_assess_work_issue_duplicate:block: false,claim_status: not_claimed, no linked PR, no conflicting branch), and the allocator offered it as unallocated work.I allocated it normally, adopted that WIP rather than discarding it, and reviewed it as a reviewer would. Two changes are mine:
docs/webui-authz-audit.mdembedded literal assignment-shaped examples, so the document tripped the very detectors it documents and its ownscan_for_secretstest failed. Fixed in the document by naming the keys bare — the detectors were not weakened.Acceptance criteria
docs/webui-authz-audit.md,rbac_matrix()redaction_policy(),TestRedactionaudit_policy(),REQUIRED_FIELDS, 90/365/730security-modelendpointRequired tests named by the issue — redaction units (token, keychain, password), default-deny for unauthenticated write stubs, and audit record creation for a simulated privileged preview — are all present.
Verification
Run with
venv/bin/python -m pytestfrom the allocated worktree.tests/test_webui_console_authz_audit.py— 75 passed, 93 subtests.-k "webui or gated or audit or redact") — 403 passed, 237 subtests.Baseline: a clean detached worktree pinned to master
9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8reproduces the same eleven failing node identifiers — six intest_commit_payloads.py, two intest_issue_702_review_findings_f1_f6.py, and one each intest_mcp_server.py::TestPreflightVerification,test_post_merge_moot_lease.py, andtest_reconciler_supersession_close.py. None touch console, redaction, or audit code. This branch adds no new failures.I also verified the acceptance criteria directly rather than only through the inherited tests: every action resolves to a real MCP permission, anonymous is denied on all nine actions, unknown action and unknown role deny by their own reason codes, an admin allow still reports
execution_enabled: false, requesting execution returnsphase_not_active, sample credentials redact, and a built record carries every required field with no detector hit.Scope
Seven files, 2069 insertions, 1 deletion. No production secrets. No write is enabled by this change, and
ACTIVE_PHASEstays 1.Provenance
feat/issue-633-console-authz-audit-model/Users/jasonwalker/Development/Gitea-Tools/branches/feat-issue-633-console-authz-audit-model479e434f921e556fb7a64cf52194db1a69ff84549eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #811
issue: #633
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 97038-2788fbc2067c
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-811-479e434
phase: claimed
candidate_head:
479e434f92target_branch: master
target_branch_sha: none
last_activity: 2026-07-23T01:24:29Z
expires_at: 2026-07-23T01:34:29Z
blocker: none
Review — PR #811 at
479e434f92Independent reviewer validation against Issue #633 (authoritative). Reviewer: sysadmin / prgs-reviewer; author: jcwalker3 — independence confirmed.
Acceptance criteria → evidence
docs/webui-authz-audit.mdpluswebui/console_authz.py::rbac_matrix(): four ordered roles (viewer/operator/controller/admin), nine actions each bound to a realtask_capability_maptask key; merge/delete flagged dual-control + break-glass. Verified eachtask_keyresolves to a real MCP permission intask_capability_map.py.webui/console_redaction.pyreusesgitea_audit.redact(no fork) and adds console patterns (keychain refs/commands, credential assignments, PEM blocks, JWTs, bearer). Fail-closed (never raises; unredactable → placeholder).TestRedactioncovers token, keychain, password patterns as the issue requires.webui/console_audit.py: all required fields asserted by tests; retention 90/365/730 days; unknown action conservatively retained as privileged; redact-at-build plus re-scan-and-drop at write; append-only JSONL, off by default.authorizationdecision block and emit audit events;GET /api/console/security-modelpublishes the model; response-shape change is additive only.Security review
Default-deny verified: anonymous denied on all nine actions; unknown action/role deny with closed reason codes; even an allow reports
execution_enabled: falsewhileACTIVE_PHASE=1; execution requests denyphase_not_active.webui/gated_actions.pystill fails closed (_MVP_ACTIONS_ENABLED = False), so this PR enables no write. No secrets, tokens, or credentials in the diff; security-model endpoint scan returns no detector hits.The one declared gap —
WEBUI_REQUIRE_PROBE_AUTHis declared but not yet enforced by any route — is documented plainly indocs/webui-authz-audit.mdand pinned bytest_probe_auth_is_declared_but_not_yet_enforced, which fails with a pointer to the doc if it is ever wired. The MVP has only the public/healthprobe, so there is no non-public probe left unprotected today. Accepted as a deliberate, documented Phase 2 handoff, not a silent hole.Independent validation (reviewer worktree, exact head
479e434)tests/test_webui_console_authz_audit.py: 75 passed, 93 subtests.-k "webui or gated or audit or redact": 404 passed, 237 subtests.53c2c92782fdb4b3ac4af922e6814f345141f82f(6× test_commit_payloads, 2× test_issue_702, 1 each test_mcp_server/TestPreflightVerification, test_post_merge_moot_lease, test_reconciler_supersession_close). Pre-existing, unrelated to console/authz/audit code; this PR introduces no new failures.Scope
7 files, +2069/−1: three new webui modules, one additive app.py wiring block, two docs, one new test module. No unrelated or speculative changes.
All Issue #633 acceptance criteria and required tests are satisfied. No blocking findings.
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #811
issue: #633
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38446-9c698fc1363c
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr811-66a89a46
phase: claimed
candidate_head:
66a89a46bbtarget_branch: master
target_branch_sha:
53c2c92782last_activity: 2026-07-23T03:02:16Z
expires_at: 2026-07-23T03:12:16Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #811
issue: #633
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38446-9c698fc1363c
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr811-66a89a46
phase: validated
candidate_head:
66a89a46bbtarget_branch: master
target_branch_sha:
53c2c92782last_activity: 2026-07-23T03:07:15Z
expires_at: 2026-07-23T03:17:15Z
blocker: none
Canonical PR State
STATE: approved
WHO_IS_NEXT: merger
NEXT_ACTION: Merge PR #811 at the approved head only after merger preflight and sync assessment recommend merge_now.
NEXT_PROMPT:
WHAT_HAPPENED: Fresh independent reviewer approval at synchronized head
66a89a46(supersedes stale Review #509 at479e434).WHY: All Issue #633 acceptance criteria and required tests pass on the complete resulting change after master sync; no blocking findings.
ISSUE: #633
HEAD_SHA:
66a89a46bbREVIEW_STATUS: approved / approval_at_current_head
MERGE_READY: true
BLOCKERS: none
VALIDATION: Focused tests/test_webui_console_authz_audit.py 75 passed 93 subtests; broader -k webui|gated|audit|redact 426 passed; full suite 4472 passed 11 failed identical to master baseline 53c2c927; worktree clean; sync base
53c2c927is parent of merge commit66a89a46.NATIVE_REVIEW_PROOF: transport=native_mcp; entrypoint=mcp_server; token_fingerprint=5dff9a62607f4833
LAST_UPDATED_BY: sysadmin / prgs-reviewer / 2026-07-23
Review body — PR #811 at
66a89a46bbFresh independent re-review after branch synchronization. Do not reuse Review #509 (old head
479e434). Reviewer: sysadmin / prgs-reviewer; author: jcwalker3 — independence confirmed.Synchronization proof
66a89a46bb479e434(feature) +53c2c92782(master)Acceptance criteria (Issue #633)
Security
Default-deny for anonymous; execution_enabled false while ACTIVE_PHASE=1; gated_actions still fail closed. Declared probe-auth non-enforcement documented and pinned. No production secrets.
Validation (branches/review-pr811-66a89a46)
53c2c927All gates pass. APPROVE.
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #811
issue: #633
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38446-9c698fc1363c
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr811-66a89a46
phase: released
candidate_head:
66a89a46bbtarget_branch: master
target_branch_sha:
53c2c92782last_activity: 2026-07-23T03:09:26Z
expires_at: 2026-07-23T03:19:26Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #811
issue: #633
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 71175-6a8151d431c5
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/merge-pr811-66a89a46
phase: claimed
candidate_head:
66a89a46bbtarget_branch: master
target_branch_sha:
53c2c92782last_activity: 2026-07-23T03:16:27Z
expires_at: 2026-07-23T03:26:27Z
blocker: none
Stale #332 review-decision lock cleanup (#594)
Status: APPLIED
sysadminprgs-merger2026-07-23T03:16:40.596718+00:00approveon PR feat(webui): console authorization, RBAC, redaction, and audit model (Closes #633) (#811)closed(merged=True)689c60fc7c386dd25926f9161d93cfc25524c5d32prgs-reviewerManual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.