Skip to content

Enforce ActiveAdmin authorization on list_resources and query - #6

Merged
lloydwatkin merged 1 commit into
mainfrom
enforce-authorization-on-reads
Aug 19, 2026
Merged

Enforce ActiveAdmin authorization on list_resources and query#6
lloydwatkin merged 1 commit into
mainfrom
enforce-authorization-on-reads

Conversation

@lloydwatkin

@lloydwatkin lloydwatkin commented Aug 19, 2026

Copy link
Copy Markdown
Member

Problem

🤖 The read tools bypassed authorization entirely. RequestHandler#tool_query and #tool_list_resources never consulted the resource namespace's ActiveAdmin authorization adapter — only update (via RecordUpdater) did. So any authenticated MCP user could list every registered resource and Ransack-query every row, regardless of the abilities their admin role grants them (CanCanCan, Pundit, etc.). @current_user was accepted by the constructor and then ignored on the read path.

Two aggravating details, both addressed here:

  • ResourceRegistry.sensitive_attributes (encrypted_password, password_digest, reset_password_token, api_key, secret) was applied only to the list_resources schema listing — not to query's records.as_json, so those columns leaked through query.
  • limit is capped at 100 per call, but q is arbitrary Ransack, so the whole table is reachable by paging through filters.

Reported downstream as OLIOEX/api#14286 (from OLIOEX/api#14273).

Fix

  • Extract adapter construction into a shared ActiveadminMcp::Authorization wrapper around ActiveAdmin's per-namespace authorization adapter. RecordUpdater now uses it too (single source of truth).
  • query now:
    • refuses resources the current user may not read (authorized?(:read, model)), and
    • scopes results through the adapter's scope_collection, so the MCP user only sees records they could see in the UI, and
    • strips the same sensitive attributes list_resources already omits.
  • list_resources now hides resources the current user may not read.

Driven entirely by config.namespace.authorization_adapter, so this is generic across any ActiveAdmin project. With ActiveAdmin's default adapter every check passes and collections are returned unchanged — applications without an authorization adapter are unaffected. Projects wiring CanCanCan via config.cancan_ability_class = "..." get their ability enforced on reads, scoping, and writes alike.

Tests

  • New spec/activeadmin_mcp/authorization_spec.rb for the wrapper (build-from-class and build-from-string, delegation, default :read action).
  • request_handler_spec: query is refused when unauthorized, is scoped through the adapter before limiting, and strips sensitive attributes; list_resources returns only authorized resources.
  • resource_registry_spec: coverage for the new public .resources.
  • Existing record_updater_spec unchanged and green (refactor preserves behaviour).

Full suite green (71 examples, 0 failures).

Docs

README "How it works" + tools table and the in-code MCP tool descriptions updated to state reads are authorized/scoped; CHANGELOG.md gains a Security entry under [Unreleased].


Released as 0.0.3; consumed downstream in OLIOEX/api#14289.

🤖 Generated with Claude Code

The read tools ignored the resource namespace's authorization adapter, so
any authenticated MCP user could list and Ransack-query every registered
resource regardless of their admin abilities — only `update` was gated.

Route `list_resources` and `query` through the same adapter as the admin
UI (extracted into a shared `Authorization` wrapper that `RecordUpdater`
now also uses): unreadable resources are hidden and refused, and query
results are scoped via `scope_collection`. `query` also strips the same
sensitive attributes that `list_resources` already omits.

With ActiveAdmin's default adapter every check passes, so applications
without an authorization adapter are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lloydwatkin
lloydwatkin merged commit 5b178df into main Aug 19, 2026
1 check passed
@lloydwatkin
lloydwatkin deleted the enforce-authorization-on-reads branch August 19, 2026 12:28
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