Skip to content

Declarative role-based authorization: required roles enforced - #152

Merged
SamuelXing merged 2 commits into
mainfrom
feat/required-roles
Aug 4, 2026
Merged

Declarative role-based authorization: required roles enforced#152
SamuelXing merged 2 commits into
mainfrom
feat/required-roles

Conversation

@SamuelXing

Copy link
Copy Markdown
Owner

Closes #142.

What

durare already carried identity end to end — AuthContext set at start, persisted, inherited by children, stamped on spans — but nothing could refuse a caller. This closes the loop, matching the Python/TS enforced model (Go is metadata-only, so this is a recorded match-Python-over-Go call, like the admin GC endpoint):

  • require_roles(name, roles) on the engine and builder declares the roles a caller must hold. Declarations naming unregistered workflows are rejected at launch/build.
  • Enforced before the body on every execution path — the check lives in the single function all five paths (direct, queued, scheduled, child, recovery) flow through, exactly where Python places its wrapper check. The first required role the caller holds becomes the run's assumed role (ctx.assumed_role()), mirroring DBOSAssumeRole.
  • A denial is terminal: typed Error::NotAuthorized / ErrorCode::NotAuthorized, and the run is finalized ERROR through the ordinary error path — Python's finalize-or-loop-forever insight, ported: the persisted auth context can never satisfy the check on retry, so an unauthorized queued run must not redequeue endlessly. Portable-mode rows record the cross-SDK DBOSNotAuthorizedError envelope name so foreign readers classify it.
  • Trust boundary documented (security guide, new Authorization section): authenticating the caller is the application's job; enqueue is ungated (a Client has no registry) and the executor refuses at dequeue — which also covers rows enqueued around your API.

Recorded divergences (parity tracker): declaration is post-registration rather than a decorator (works with macro-registered workflows without a durare-macros release; macro sugar can follow), no class-level default_required_roles (no class concept), and TS's pluggable AuthChecker hook is deferred until asked for.

Tests

Match-and-assume, no-auth denial (terminal ERROR + body never ran), wrong-roles denial, queued denial finalizes instead of looping, undeclared workflows unrestricted, launch-time typo rejection. Full suite: 414 passed, 0 failed (--all-features, live Postgres); strict docs clean.

durare carried identity end to end (AuthContext, persisted and traced) but
could not refuse anyone. Now it can:

- DurableEngine::require_roles(name, roles) (and the builder equivalent)
  declares the roles a caller must hold to invoke a workflow. A
  declaration naming an unregistered workflow is rejected at launch/build
  — a typo would otherwise silently enforce nothing.

- The check runs in run_to_completion before the body, the single gate
  all five execution paths flow through (direct, queued, scheduled,
  child, recovery). The first required role the caller holds becomes the
  run's assumed role, visible to the body via ctx.assumed_role().

- A denial is Error::NotAuthorized (ErrorCode::NotAuthorized) and rides
  the ordinary returned-error path, finalizing the run ERROR. Terminal by
  construction: the persisted auth context can never satisfy the check on
  a retry, so leaving the row PENDING would redequeue an unauthorized
  queued run forever. Portable-mode rows record the denial under the
  cross-SDK DBOSNotAuthorizedError envelope name.

- Authenticating the caller stays the application's job (durare is a
  library); enqueueing is not gated — the executor refuses at dequeue,
  which also covers rows enqueued around the API. Documented in the
  security guide's new Authorization section.

Tests: match-and-assume (first matching required role), no-auth denial
(terminal ERROR, body never ran), wrong-roles denial, queued denial
finalizes instead of looping, undeclared workflows unrestricted, and the
launch-time typo rejection.
@SamuelXing
SamuelXing force-pushed the feat/required-roles branch from e44aa9e to a679641 Compare August 4, 2026 02:37
CHANGELOG only: the role-authorization Added entry alongside the
integration-recipes Documentation entries from #151.
@SamuelXing
SamuelXing merged commit de306c6 into main Aug 4, 2026
5 checks passed
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.

[Parity] Declarative role-based authorization (required-roles enforcement)

1 participant