Skip to content

feat(acl): gate run_stored_query to admin — close the last read-gap#173

Merged
jrosskopf merged 1 commit into
mainfrom
feat/acl-stored-query
Jun 15, 2026
Merged

feat(acl): gate run_stored_query to admin — close the last read-gap#173
jrosskopf merged 1 commit into
mainfrom
feat/acl-stored-query

Conversation

@jrosskopf

Copy link
Copy Markdown
Contributor

Closes the final point-3 read-gap. run_stored_query executes pre-declared arbitrary SQL over the whole corpus (pages/blocks/links) and projects arbitrary columns (aggregates, joins) — there is no reliable per-row page_id/owner to filter on, so the per-instance read ACL (which resolve/neighbours/expand/list/search use) can't apply row-by-row.

The sound gate is at the capability level: stored queries are an operator/analytics affordance, so admin-only — exactly like the admin_* inspection tools (require_admin). A non-admin member is now refused (admin role required); the unauthenticated dev/on-host path (role None, no verifier) is unaffected.

No consumer regresses: Carl's only caller is the operator dashboard read-proxy, which forwards under the operator's minted escurel:admin token. No member-facing Carl path uses run_stored_query.

Tests (red/green, real HTTP MCP)

  • stored_query_acl.rs: non_admin_member_cannot_run_stored_query (refused, no rows leaked) + admin_runs_stored_query (count query returns the row).
  • Existing mcp.rs::run_stored_query_routes_through_http (AuthMode::Disabled) stays green — the dev/on-host path is unchanged.
  • clippy clean; docs/spec/protocol.md documents the access rule.

With this, all three read tools that previously leaked across owners — resolve, neighbours (#172), and now run_stored_query — are closed. Remaining ACL items are non-read: assign_event admin-gate (deferred pending a caller audit so it can't break the matching write path) and capture_event (left open by design — members capture their own erasure/opt-in events).

🤖 Generated with Claude Code

run_stored_query executes pre-declared arbitrary SQL over the whole corpus
(pages/blocks/links) and projects arbitrary columns (aggregates, joins), so
there is no reliable per-row owner against which to apply the per-instance read
ACL. The sound gate is at the capability level: operator/analytics only, exactly
like the admin_* inspection tools (require_admin). A non-admin member is now
refused; the unauthenticated dev/on-host path (role None) is unaffected.

Carl's only caller is the operator dashboard read-proxy, which forwards under
the operator's minted escurel:admin token — so no member path regresses.

Red/green, real HTTP MCP: stored_query_acl.rs (non-admin refused / admin runs).
Existing mcp.rs run_stored_query route test (AuthMode::Disabled) stays green.
Documents the access rule in docs/spec/protocol.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jrosskopf jrosskopf merged commit 9cbb34b into main Jun 15, 2026
1 check passed
@jrosskopf jrosskopf deleted the feat/acl-stored-query branch June 15, 2026 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant