Skip to content

feat(plugins): let a plugin register its own authz-schema actions - #19

Merged
zumbrunn merged 2 commits into
mainfrom
feat/plugin-extensible-authz-schema
Sep 2, 2026
Merged

feat(plugins): let a plugin register its own authz-schema actions#19
zumbrunn merged 2 commits into
mainfrom
feat/plugin-extensible-authz-schema

Conversation

@zumbrunn

@zumbrunn zumbrunn commented Sep 2, 2026

Copy link
Copy Markdown
Member

What

DunePlugin.authzActions lets a plugin declare a new admin-permission action (e.g. "billing.manage") and gate a route behind it the same correct way every built-in admin route is (authz.check()), instead of reusing an existing, semantically mismatched permission or hand-rolling a check outside the authz system entirely.

Closes the "Plugin-extensible authz schema" backlog item.

Design

  • Relation-only: a plugin action can require existing relations (member/admin/editor/author/owner), not define a new relation type — covers the actual motivating case without opening the harder question of arbitrary plugin-defined relation semantics.
  • src/auth/authz-schema.ts: the module-level duneAuthzSchema constant is now buildDuneAuthzSchema()'s zero-argument output (built-ins only) — unaffected for any existing caller reading it directly (tests, headless usage). bootstrap() calls the same builder with plugin-contributed actions merged in, once per site.
  • Collision handling: bootstrap.ts collects every plugin's authzActions after setup() has run, before the authz system is created. A name colliding with a built-in action or another plugin's is dropped with a logged warning, not silently merged — first declaration wins in registration order.
  • Multisite-safe by construction: the merged schema lives on BootstrapResult.authzSchema, not a process-wide singleton, so one site's plugin action can't leak into another's schema.
  • roleHasPermission() (the one synchronous permission-check path, ResponseTransformContext.auth.hasPermission()) gains an optional third parameter — a site's actual actionToRelations map — so it resolves a plugin-contributed action correctly instead of only ever seeing the built-ins.

Companion change

Needs a matching @dune/plugin-admin change (widening AdminPermission from a closed union to accept any string) for a plugin author to actually pass a custom action through withGuards()/requirePermission() without a type-level workaround — separate PR: duneorg/plugin-admin#5.

Testing

  • New tests/auth/authz_plugin_actions_test.ts — real bootstrap() + real authz.check() end to end, including both collision cases (built-in and cross-plugin).
  • Unit coverage added to tests/auth/authz_schema_test.ts for buildDuneAuthzSchema()/roleHasPermission()'s new parameter.
  • Full suite: deno test -A tests/ — 1815 passed, 0 failed.
  • deno task check (lint + typecheck + dynamic-import-lint) clean.

Note on versioning

This is folded into the existing, still-unpublished ## [0.34.4] CHANGELOG entry rather than a new version bump — deno.json stays at 0.34.4 since that version hasn't published to JSR yet.

🤖 Generated with Claude Code

zumbrunn and others added 2 commits September 2, 2026 10:04
DunePlugin.authzActions lets a plugin declare a new admin-permission
action (e.g. "billing.manage") and gate a route behind it the same
correct way every built-in admin route is (authz.check()), instead of
reusing an existing, semantically-mismatched permission or hand-rolling
a check outside the authz system entirely.

Relation-only — a plugin can require existing relations (member/admin/
editor/author/owner), not define a new relation type, which keeps the
scope to the actual motivating case without opening the harder question
of arbitrary plugin-defined relation semantics.

src/auth/authz-schema.ts's module-level duneAuthzSchema constant becomes
buildDuneAuthzSchema()'s zero-argument output (built-ins only, DUNE_BASE_
AUTHZ_ACTIONS exported for collision-checking) — bootstrap() now calls
this once per site, after every plugin's setup() has run and their
authzActions have been collected and de-duplicated (first declaration
wins on a name collision, built-in or cross-plugin, dropped with a
logged warning rather than silently merged/overwritten). BootstrapResult
gains authzSchema (the site's actual merged schema) alongside authz,
which is already built against it via createDuneAuthSystem()'s new
optional schema config field. roleHasPermission() gains an optional
third parameter (a site's actual actionToRelations map) so the one
synchronous permission-check path (ResponseTransformContext.auth
.hasPermission(), threaded through response-transforms.ts's new
actionToRelations option) resolves a plugin-contributed action correctly
instead of only ever seeing the built-ins.

Multisite-safe by construction: the schema lives on BootstrapResult, not
a process-wide singleton, so one site's plugin action can't leak into
another's schema.

Companion @dune/plugin-admin change needed for a plugin author to
actually pass a custom action through withGuards()/requirePermission()
without a type-level workaround (AdminPermission is currently a closed
union) — separate PR.

3 new tests (real bootstrap() + real authz.check() end to end, including
both collision cases) plus unit coverage for buildDuneAuthzSchema()/
roleHasPermission()'s new parameter.

Folded into the still-unpublished 0.34.4 CHANGELOG entry, not a new
version bump — 0.34.4 has not published to JSR yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zumbrunn
zumbrunn merged commit 497b527 into main Sep 2, 2026
5 checks passed
@zumbrunn
zumbrunn deleted the feat/plugin-extensible-authz-schema branch September 2, 2026 08:47
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