Skip to content

feat(plugins): writable_data requires_role + readonly_data current_user auto-injection#55

Merged
DavidsonGomes merged 1 commit into
developfrom
feat/plugin-rbac-and-readonly-scoping
Apr 25, 2026
Merged

feat(plugins): writable_data requires_role + readonly_data current_user auto-injection#55
DavidsonGomes merged 1 commit into
developfrom
feat/plugin-rbac-and-readonly-scoping

Conversation

@DavidsonGomes
Copy link
Copy Markdown
Member

Summary

Two related Wave 2.1.x extensions to the plugin contract closing the last two gaps blocking endpoint-level RBAC for plugin authors. Discovered during evonexus-plugin-nutri Step 3 RBAC decision (split patients_admin vs patients_clinical was needed because the host had no role gate per endpoint).

  • Gap 1PluginWritableResource.requires_role: Optional[List[str]] field. Handler returns 403 when current_user.role is not in the list. 'admin' always passes (super-user override).
  • Gap 5readonly_data auto-injects :current_user_id and :current_user_role as bind params on every query. Plugins reference them in SQL for server-enforced scoping. Both names are reserved — client requests carrying them in query string get 400 (no identity spoofing).

Why

Before this PR, plugin authors had two unsafe options for role-based access:

  1. Enforce in the UI only — anyone with curl bypasses
  2. Split a single resource into N endpoints by role (workaround used in evonexus-plugin-nutri patients_admin vs patients_clinical) — duplicates schema and still doesn't gate the endpoints themselves

For readonly_data, the only scoping option was to declare a nutritionist_id param and hope the UI supplies it correctly — curl ? gets the unfiltered set.

This PR closes both with ~25 lines of host code.

Compat

  • Existing plugins (PM Essentials) work unchanged. requires_role defaults to None (any auth user passes). Auto-injected bind params are silently ignored if SQL doesn't reference them.
  • Reserved-param guard is the only behavioural change visible to existing clients — but no plugin in the marketplace uses current_user_id or current_user_role as a query-string param today.

Test plan

  • 13 new pytest cases in tests/backend/test_plugins_rbac_and_scoping.py
    • Pydantic accepts/rejects requires_role correctly (kebab-case validator)
    • 403 path: clinical resource + recepcao role
    • 200/201 path: clinical resource + nutricionista role
    • admin override: clinical resource + admin role
    • Open resource (no requires_role): any role passes
    • readonly auto-inject: scoped result per-user
    • readonly spoofing: client ?current_user_id=2 returns 400
    • Backwards compat: query without :current_user_id ref still returns all rows
  • Pre-existing tests/backend/ suite: 91 passed (2 unrelated cache-test failures pre-existing — _compute_preview signature drift in test_plugins_preview_endpoint.py)
  • Plugin nutri full regression after consuming the new field on 6 writable resources: 14/14 install ACs + 89 pytest + 9/9 yaml validator

🤖 Generated with Claude Code

…er auto-injection

Two related Wave 2.1.x extensions to the plugin contract that close the last
two gaps blocking endpoint-level RBAC for plugin authors (gap inventory in
evonexus-plugin-nutri Step 3 RBAC decision).

Changes
- PluginWritableResource.requires_role: Optional[List[str]] — when set, the
  POST/PUT/DELETE handler returns 403 if current_user.role is not in the list.
  'admin' role always passes (super-user override). Backwards compatible:
  resources without the field accept any authenticated user (legacy default).
  Validator enforces kebab-case role names (^[a-z][a-z0-9-]*$).

- routes.plugins.writable_data: enforces requires_role at the endpoint, with
  a 403 message naming the required roles and the actor's current role.

- routes.plugins.readonly_data: auto-injects :current_user_id and
  :current_user_role as bind params on every readonly query. Plugins can
  reference them directly in SQL for server-enforced scoping without an
  app-layer wrapper. The two parameter names are reserved — client requests
  carrying them in the query string get 400 (no identity spoofing).

Tests
- tests/backend/test_plugins_rbac_and_scoping.py — 13 cases covering Pydantic
  acceptance/rejection, 403/200 paths for writable, scoping/spoofing for readonly,
  backwards compat for resources without requires_role and queries without
  :current_user_id refs.

Compat
- Existing plugins (PM Essentials) continue to work unchanged — the new field
  defaults to None and the auto-injected bind params are silently ignored if
  the SQL doesn't reference them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @DavidsonGomes, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@DavidsonGomes DavidsonGomes merged commit 502c9ac into develop Apr 25, 2026
4 checks passed
@DavidsonGomes DavidsonGomes deleted the feat/plugin-rbac-and-readonly-scoping branch April 25, 2026 19:51
jbmendonca pushed a commit to jbmendonca/evo-nexus that referenced this pull request May 15, 2026
…er auto-injection (evolution-foundation#55)

Two related Wave 2.1.x extensions to the plugin contract that close the last
two gaps blocking endpoint-level RBAC for plugin authors (gap inventory in
evonexus-plugin-nutri Step 3 RBAC decision).

Changes
- PluginWritableResource.requires_role: Optional[List[str]] — when set, the
  POST/PUT/DELETE handler returns 403 if current_user.role is not in the list.
  'admin' role always passes (super-user override). Backwards compatible:
  resources without the field accept any authenticated user (legacy default).
  Validator enforces kebab-case role names (^[a-z][a-z0-9-]*$).

- routes.plugins.writable_data: enforces requires_role at the endpoint, with
  a 403 message naming the required roles and the actor's current role.

- routes.plugins.readonly_data: auto-injects :current_user_id and
  :current_user_role as bind params on every readonly query. Plugins can
  reference them directly in SQL for server-enforced scoping without an
  app-layer wrapper. The two parameter names are reserved — client requests
  carrying them in the query string get 400 (no identity spoofing).

Tests
- tests/backend/test_plugins_rbac_and_scoping.py — 13 cases covering Pydantic
  acceptance/rejection, 403/200 paths for writable, scoping/spoofing for readonly,
  backwards compat for resources without requires_role and queries without
  :current_user_id refs.

Compat
- Existing plugins (PM Essentials) continue to work unchanged — the new field
  defaults to None and the auto-injected bind params are silently ignored if
  the SQL doesn't reference them.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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