Skip to content

feat(plugins): B2.0 public_pages capability — read-only token-bound portals#53

Merged
DavidsonGomes merged 2 commits into
developfrom
host/b2-public-pages-readonly
Apr 25, 2026
Merged

feat(plugins): B2.0 public_pages capability — read-only token-bound portals#53
DavidsonGomes merged 2 commits into
developfrom
host/b2-public-pages-readonly

Conversation

@DavidsonGomes
Copy link
Copy Markdown
Member

Summary

  • Add Capability.public_pages + Capability.safe_uninstall enum values to plugin_schema.py
  • New PluginPublicPage + PluginPublicPageTokenSource Pydantic models with validators (bundle under ui/public/, no revoked_when SQL fragment in v1)
  • Extend ReadonlyQuery with public_via + bind_token_param fields
  • New file routes/plugin_public_pages.py with 3 endpoints (/p/<slug>/<prefix>/<token>, /data, /public-assets/<path>)
  • Token validated parametrically on every request (table/column identifiers whitelisted at install by schema)
  • Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle management
  • All Vault §B2 mitigations applied: rate limit (60/min/IP via PR feat(security): rate-limit public share endpoint + security headers #52's rate_limit.limiter), security headers, CSP on bundles

Note: this PR depends on #52 (rate-limit) which provides rate_limit.py. Branch is on top of host/rate-limit.

Scope: B2.0 only (deferred to B2.1)

  • PIN flow, writable_data public_via, auto_set_columns → deferred
  • HMAC cookie → deferred

Test plan

  • 113 backend + dashboard tests pass (excluding pre-existing failure in test_cache_hit_does_not_call_compute_again)
  • Capability.public_pages and Capability.safe_uninstall in enum
  • PluginPublicPage with bundle: ui/pages/x.js → validation error (must be ui/public/)
  • PluginPublicPage with token_source.table: other_slug_patients → validation error (must start with plugin slug)
  • GET /p/nutri/portal/{valid_token} → serves bundle (no auth required)
  • GET /p/nutri/portal/{invalid_token} → 404
  • 61st request in 1 min to portal → 429

🤖 Generated with Claude Code

DavidsonGomes and others added 2 commits April 25, 2026 15:06
Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate
limiting. Add flask-limiter (in-memory, single-process MVP) with:
- 60 req/min/IP on view_share (Vault §2.S1)
- Global default 600 req/min on all other routes (non-blocking baseline)
- Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options
  headers on every public share response (Vault §2.S2)

The Limiter singleton lives in rate_limit.py to break the circular-import
chain between app.py (which imports route blueprints) and the blueprints
that need @limiter.limit() decorators.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ortals

Add the public_pages plugin capability (B2.0 scope, read-only):

plugin_schema.py:
- Add Capability.public_pages and Capability.safe_uninstall enum values
- Add PluginPublicPageTokenSource + PluginPublicPage Pydantic models
  (bundle must be under ui/public/, revoked_when disallowed in v1 to prevent SQL injection)
- Extend ReadonlyQuery with public_via + bind_token_param fields
- Add 4 PluginManifest model validators: capability required, table slug-prefix,
  unique ids/route_prefixes, readonly_data references valid page

routes/plugin_public_pages.py (new):
- GET /p/<slug>/<route_prefix>/<token>       — serve portal bundle (60 req/min/IP)
- GET /p/<slug>/<route_prefix>/<token>/data  — serve token-bound readonly query (120/min)
- GET /p/<slug>/public-assets/<path>         — serve ui/public/ static assets
- Token validation via parametric SQL (identifiers validated at install by schema)
- Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle
- Vault §B2.S2: Referrer-Policy, Cache-Control no-store, HSTS, X-Content-Type-Options on all responses
- CSP: default-src 'self' on portal bundles
- Rate limiting via rate_limit.limiter (imported from PR #52)

app.py:
- Import and register plugin_public_pages_bp
- /p/... paths already bypass auth_middleware (non-/api/ paths are passthrough)

Co-Authored-By: Claude Sonnet 4.6 <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 54ea568 into develop Apr 25, 2026
4 checks passed
@DavidsonGomes DavidsonGomes deleted the host/b2-public-pages-readonly branch April 25, 2026 18:34
jbmendonca pushed a commit to jbmendonca/evo-nexus that referenced this pull request May 15, 2026
…ortals (evolution-foundation#53)

* feat(security): rate-limit public share endpoint + security headers

Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate
limiting. Add flask-limiter (in-memory, single-process MVP) with:
- 60 req/min/IP on view_share (Vault §2.S1)
- Global default 600 req/min on all other routes (non-blocking baseline)
- Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options
  headers on every public share response (Vault §2.S2)

The Limiter singleton lives in rate_limit.py to break the circular-import
chain between app.py (which imports route blueprints) and the blueprints
that need @limiter.limit() decorators.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(plugins): B2.0 public_pages capability — read-only token-bound portals

Add the public_pages plugin capability (B2.0 scope, read-only):

plugin_schema.py:
- Add Capability.public_pages and Capability.safe_uninstall enum values
- Add PluginPublicPageTokenSource + PluginPublicPage Pydantic models
  (bundle must be under ui/public/, revoked_when disallowed in v1 to prevent SQL injection)
- Extend ReadonlyQuery with public_via + bind_token_param fields
- Add 4 PluginManifest model validators: capability required, table slug-prefix,
  unique ids/route_prefixes, readonly_data references valid page

routes/plugin_public_pages.py (new):
- GET /p/<slug>/<route_prefix>/<token>       — serve portal bundle (60 req/min/IP)
- GET /p/<slug>/<route_prefix>/<token>/data  — serve token-bound readonly query (120/min)
- GET /p/<slug>/public-assets/<path>         — serve ui/public/ static assets
- Token validation via parametric SQL (identifiers validated at install by schema)
- Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle
- Vault §B2.S2: Referrer-Policy, Cache-Control no-store, HSTS, X-Content-Type-Options on all responses
- CSP: default-src 'self' on portal bundles
- Rate limiting via rate_limit.limiter (imported from PR evolution-foundation#52)

app.py:
- Import and register plugin_public_pages_bp
- /p/... paths already bypass auth_middleware (non-/api/ paths are passthrough)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <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