Declarative role-based authorization: required roles enforced - #152
Merged
Conversation
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
force-pushed
the
feat/required-roles
branch
from
August 4, 2026 02:37
e44aa9e to
a679641
Compare
CHANGELOG only: the role-authorization Added entry alongside the integration-recipes Documentation entries from #151.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #142.
What
durare already carried identity end to end —
AuthContextset 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.ctx.assumed_role()), mirroringDBOSAssumeRole.Error::NotAuthorized/ErrorCode::NotAuthorized, and the run is finalizedERRORthrough 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-SDKDBOSNotAuthorizedErrorenvelope name so foreign readers classify it.Clienthas 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-macrosrelease; macro sugar can follow), no class-leveldefault_required_roles(no class concept), and TS's pluggableAuthCheckerhook 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.