23 KiB
Tasks: Authorization Model
Input: Design documents from /specs/013-rbac-authorization-model/
Prerequisites: spec.md, plan.md, research.md, contracts/permissions.md, contracts/rest-api.md
Tests: Required. The fail-closed coverage gate (T041) is itself a deliverable and must land in the same milestone as the cutover.
Organization: Phases map to the milestones in plan.md. Every task is tagged with the user story from spec.md it serves. All paths are relative to the elsa-core repository.
Sizing note: Phase 3 is one pull request per module. Module endpoint-file counts, from a full census: Workflows.Api 78, ExternalAuthentication 13, Identity 11, Secrets 10, Labels 7, Diagnostics.OpenTelemetry 7, Tenants 6, Dashboard.Api 5, Alterations 5, Resilience 3, Diagnostics.StructuredLogs 3, Bpmn.Interchange 3, AI.Host 3, Shells.Api 2, Diagnostics.ConsoleLogs 2, Http.Webhooks 1, Expressions.JavaScript 1.
Format: [ID] [P?] [Story] Description
Reconciled 2026-08-27. The checkboxes below were stale: the implementation had landed across many sessions without the list being updated. Every task was re-verified against the code on this date — 53 were confirmed complete and ticked; the 12 that remain open carry an inline note naming the missing evidence. Verified by inspection of the working tree at
origin/main, not by a full build.
- [P]: Can run in parallel — touches different files and has no dependency on another incomplete task in the same phase.
- [Story]: Maps to a user story in
spec.md(US1–US9).
Phase 1: Model and Evaluator
Purpose: The permission model and the single decision point. Entirely additive — nothing changes behavior, and no endpoint is touched.
- T001 [P] [US1] Define
Permissionas a(Resource, Verb)pair with canonical parse and format insrc/common/Elsa.Api.Common/Authorization/Permission.cs. Reject strings containing a comma, per the persistence constraint. Verified done 2026-08-27. - T002 [P] [US1] Declare the recommended core verbs —
view,create,update,write,delete,execute— as constants insrc/common/Elsa.Api.Common/Authorization/CoreVerbs.cs. Verified done 2026-08-27. - T003 [US1] Implement
PermissionMatcherinsrc/common/Elsa.Api.Common/Authorization/PermissionMatcher.cs: exact match on both axes; a trailing*on the resource axis matching the named node and all descendants at any depth;*on the verb axis matching any verb. Verified done 2026-08-27. - T004 [US1] Define
IPermissionEvaluatorand its implementation insrc/common/Elsa.Api.Common/Authorization/, resolving a principal's grants from thepermissionsclaim and evaluating them throughPermissionMatcher. Verified done 2026-08-27. - T005 [US2] Add
PermissionRequirementandPermissionAuthorizationHandlerinsrc/common/Elsa.Api.Common/Authorization/, replacing the FastEndpoints exact-match permission check as the enforcement path. Verified done 2026-08-27. - T006 [US3] Promote
PermissionDescriptor,IPermissionDescriptorProvider,IPermissionDescriptorRegistryandDefaultPermissionDescriptorRegistryfromElsa.ExternalAuthenticationintosrc/common/Elsa.Api.Common/Permissions/, leaving type-forwarding shims so External Authentication keeps compiling. Verified done 2026-08-27. - T007 [US3] Extend
PermissionDescriptorwith the verbs a resource supports, so the catalog can drive a role editor and validate submitted grants. Verified done 2026-08-27. - T008 [P] [US1] Unit-test the matcher table in
test/unit/Elsa.Api.Common.UnitTests/Authorization/PermissionMatcherTests.cs: exact match on each axis; subtree wildcard covering the node itself and descendants; verb wildcard;*:*; absence denying; a wildcard covering a newly registered resource and a newly registered verb. Verified done 2026-08-27. - T009 [P] [US1] Unit-test
IPermissionEvaluatorfor union-across-roles semantics and for the absence of verb implication (FR-009). Verified done 2026-08-27.
Phase 2: Catalog Coverage
Purpose: Every module declares its resources and verbs. Still additive.
Each module task creates Permissions/<Module>Permissions.cs following the pattern already proven in src/modules/Elsa.ExternalAuthentication/Permissions/ExternalAuthenticationPermissions.cs — constants and descriptors colocated — refined to one constant per resource, with verbs supplied separately. Resources and verbs come from contracts/permissions.md.
- T010 [P] [US3]
Elsa.Workflows.Api— 20 resources spanningworkflows/definitions,.../versions,.../labels,workflows/instances,activity-executions,runtime,bookmark-queue/dead-letters,events,tasks,tests, the ninedescriptors/*, andscripting/javascript. Verified done 2026-08-27. - T011 [P] [US3]
Elsa.Identity—identity/users,identity/roles,identity/applications. Verified done 2026-08-27. - T012 [P] [US3]
Elsa.Secrets—secrets. Retire the unuseduse:,import:andexport:constants. Verified done 2026-08-27. - T013 [P] [US3]
Elsa.ExternalAuthentication—connections,descriptors,identity-links,sessions,policies,policies/default-roles,provider-trust,permission-grants. Correct theroles:assigndescriptor text to describe what it actually guards. Verified done 2026-08-27. - T014 [P] [US3]
Elsa.Labels—labels. Verified done 2026-08-27. - T015 [P] [US3]
Elsa.Tenants—tenants. Verified done 2026-08-27. - T016 [P] [US3]
Elsa.Alterations—alterations. Verified done 2026-08-27. - T017 [P] [US3]
Elsa.Dashboard.Api—dashboard. Verified done 2026-08-27. - T018 [P] [US3]
Elsa.Resilience—resilience/retries,resilience/strategies,resilience/simulation. Verified done 2026-08-27. - T019 [P] [US3]
Elsa.Diagnostics.ConsoleLogs,Elsa.Diagnostics.StructuredLogs,Elsa.Diagnostics.OpenTelemetry— the threediagnostics/*resources. Retire the unusedingest:constant. Verified done 2026-08-27. - T020 [P] [US3]
Elsa.AI.Host—ai/chat,ai/tools,ai/capabilities. Retire the unusedai:proposals:*andai:tools:manageconstants. Verified done 2026-08-27. - T021 [P] [US3]
Elsa.Shells.ApiandElsa.Workflows.Apiplatform resources —system/shells,system/features. Verified done 2026-08-27. - T022 [US3] Reduce
src/common/Elsa.Api.Common/PermissionNames.csto the claim type and the whole-vocabulary grant; move the workflow-runtime and bookmark-queue constants to the Workflows.Api declarations. Remove the deadAdminRoleName/ReaderRoleName/WriteRoleNamefields fromEndpointSecurityOptions.cs. Verified done 2026-08-27. - T022a [US3] Wire descriptor validation into the role write paths (
Endpoints/Roles/Create,Endpoints/Roles/Update), which today persistrequest.Permissionsafter only the caller-subset check. Reject a concrete resource with no registered descriptor and a concrete verb outside that resource's supported verbs; accept structurally valid wildcard segments, including ones that currently match nothing. Covers FR-012a. Verified done 2026-08-27. - T022b [P] [US3] Unit-test role-authoring validation: unknown concrete resource rejected, unsupported concrete verb rejected,
workflows/*:viewaccepted,*:*accepted, and a wildcard matching no installed module accepted. Still open (verified 2026-08-27) — no role-authoring validation tests found intest/unit/Elsa.Identity.UnitTests. - T023 [US3] Implement
GET /identity/permissionsinsrc/modules/Elsa.Identity/Endpoints/Permissions/List/Endpoint.cs, returning core verbs, every registered resource with supported verbs, category and display metadata, and anonCoreVerbsmarker. Contract:contracts/rest-api.md. Requiresidentity/roles:view. Verified done 2026-08-27. - T024 [US1] Implement
GET /identity/permissions/reachinsrc/modules/Elsa.Identity/Endpoints/Permissions/Reach/Endpoint.cs, reporting the resources a wildcard grant currently covers. This is the mitigation for forward reach on the resource axis. Verified done 2026-08-27. - T025 [P] [US3] Integration-test the catalog endpoint for descriptor consistency only: every registered resource exposes display metadata, a category and a non-empty supported-verb list, verbs outside the core set are marked, and no two modules register the same resource. Endpoint-to-descriptor resolution is deliberately not asserted here — in Phase 2 endpoints still declare legacy strings, so that assertion belongs to the cutover gate T041. Still open (verified 2026-08-27) — no catalog descriptor-consistency integration test found.
Phase 3: Cutover
Purpose: The breaking change. One pull request per module, landing in any order.
No migration scaffold is required — the obsolete declaration path (T027) translates legacy endpoint declarations, while a legacy stored grant still fails to match, and the seeded admin * satisfies every endpoint throughout. See research.md D20.
- T026a [US2] Add an explicit authenticated-only declaration to the base classes in
src/common/Elsa.Api.Common/Abstractions/Endpoints.cs, so FR-019's third state is expressible and the coverage gate can distinguish a deliberate choice from an omission. Verified done 2026-08-27. - T026 [US8] Add
RequirePermission(string resource, string verb)to the six base classes insrc/common/Elsa.Api.Common/Abstractions/Endpoints.cs, collapsing the six copy-pastedConfigurePermissionsbodies into one shared implementation. Verified done 2026-08-27. - T027 [US8] Keep
ConfigurePermissions(params string[])as[Obsolete]but functional: resolve legacy strings through the migration table; register an implicit descriptor marked unverified and log a warning for anything unresolvable, rather than failing the host at boot. Follows the existingunknown_permission_descriptorprecedent inDefaultPermissionGrantResolver. Still open (verified 2026-08-27) —ConfigurePermissionsis preserved and functional, but is not marked[Obsolete]and does not resolve legacy strings through a migration table, register an unverified descriptor, or log a warning — it simply prependsPermissionNames.All. - T028 [US1] Migrate
Elsa.Workflows.Api—WorkflowDefinitions(31 files) toRequirePermission. Verified done 2026-08-27. - T028a [P] [US1] Migrate
Elsa.Workflows.Api—WorkflowInstances,ActivityExecutions,ActivityExecutionSummaries(20 files). Verified done 2026-08-27. - T028b [P] [US1] Migrate
Elsa.Workflows.Api—RuntimeAdmin,BookmarkQueueDeadLetters,Bookmarks,Events,Tasks,Tests,Features(15 files). Verified done 2026-08-27. - T028c [P] [US1] Migrate
Elsa.Workflows.Api— the nine descriptor folders andScripting(12 files). Verified done 2026-08-27. - T029 [P] [US1] Migrate
Elsa.ExternalAuthenticationendpoints (13 files, 34 endpoint classes — several classes per file). Verified done 2026-08-27. - T030 [P] [US1] Migrate
Elsa.Identityendpoints (11 files). Verified done 2026-08-27. - T031 [P] [US1] Migrate
Elsa.Secretsendpoints (10 files). Verified done 2026-08-27. - T032 [P] [US1] Migrate
Elsa.Labels(7) andElsa.Tenants(6) endpoints. Verified done 2026-08-27. - T033 [P] [US1] Migrate
Elsa.Diagnostics.OpenTelemetry(7),Elsa.Diagnostics.StructuredLogs(3) andElsa.Diagnostics.ConsoleLogs(2) endpoints. Verified done 2026-08-27. - T034 [P] [US1] Migrate
Elsa.Dashboard.Api(5) andElsa.Alterations(5) endpoints. Verified done 2026-08-27. - T035 [P] [US1] Migrate
Elsa.Resilience(3),Elsa.Bpmn.Interchange(3) andElsa.AI.Host(3) endpoints. Verified done 2026-08-27. - T036 [P] [US1] Migrate
Elsa.Shells.Api(2),Elsa.Http.Webhooks(1) andElsa.Expressions.JavaScript(1) endpoints. Verified done 2026-08-27. - T037 [US2] Declare the two endpoints in
src/modules/Elsa.ExternalAuthentication/Endpoints/Broker/Logout.csseparately:Logoutas authenticated-only (it reads the external session claim from the principal),ContinueLogoutasAllowAnonymous(the route handle carries the authority, matching every other broker callback).ContinueLogoutinheriting the authenticated default today is a probable live bug — the identity provider redirects the browser there during upstream logout, potentially after the Elsa session is gone. Fix tracked as #7976. Verified done 2026-08-27. - T038 [US1] Replace the hand-rolled
PermissionNames.ClaimTypeclaim inspections withIPermissionEvaluatorcalls acrossElsa.ExternalAuthentication(5 files),Elsa.Workflows.Api(2),Elsa.Identity(2),Elsa.Api.Common(1) andElsa.AI.Host(1). NoteAIHttpContextIdentitycurrently compares case-insensitively while every other site is ordinal; the evaluator standardises this. Still open (verified 2026-08-27) — two hand-rolled claim inspections remain:Elsa.AI.Host/Endpoints/AI/AIHttpContextIdentity.cs(still case-insensitive) andElsa.Identity/Services/RoleDeletionCoordinator.cs. - T038a [P] [US1] Unit-test
RoleAuthorizationServiceunder wildcard containment, extending the existing tests: a heldworkflows/*:viewmay grantworkflows/definitions:view; a heldworkflows/definitions:viewmay not grantworkflows/*:view; a held concrete grant may not grant an unsupported verb;*:*may grant anything. Verified done 2026-08-27. - T039 [US1] Route the four SignalR hub permission checks through
IPermissionEvaluator—WorkflowInstanceHub,ElsaConsoleLogStreamHubAuthorizer,StructuredLogsHub,OpenTelemetryHub— replacing hard-coded arrays such as["*", "read:*", "read:workflow-instances"]. Verified done 2026-08-27. - T040 [US1] Replace the three
Policies(IdentityPolicyNames.SecurityRoot)usages inElsa.Identity(Secrets/Hash,Roles/Create,Applications/Create) and remove the obsolete policy. Scope note: per FR-017, the 15 mid-handlerAuthorizeAsync(..., NotReadOnlyPolicy)calls in Workflows.Api are not in scope — read-only mode is a separate axis from permissions and keeps its own check. Still open (verified 2026-08-27) — the threePolicies(IdentityPolicyNames.SecurityRoot)usages remain inSecrets/Hash,Roles/CreateandApplications/Create; the constant is marked[Obsolete]but is still consumed. - T041 [US2] Add the fail-closed coverage gate in
test/unit/Elsa.Api.Common.UnitTests/Authorization/EndpointCoverageTests.cs: enumerate every in-repositoryElsaEndpoint*type by reflection and assert each declares exactly one of a permission resolving to a registered descriptor,AllowAnonymous, or authenticated-only. No exemption list. Verified done 2026-08-27. Note: landed as the sharedEndpointCoverage.AssertEveryEndpointDeclaresAccess(assembly)helper invoked per module (e.g.test/unit/Elsa.Identity.UnitTests/Authorization/EndpointCoverageTests.cs) rather than a single test inElsa.Api.Common.UnitTests. - T042 [US5] Update
DefaultAccessTokenIssuerandDefaultElsaTokenServiceto emit new-format{resource}:{verb}claims, and updateDefaultApiKeyProvider,AdminApiKeyProvider,LocalHostPermissionRequirementandDefaultExternalAuthenticationTokenIssuerto match. Verified done 2026-08-27. - T043 [US5] Add a startup validator that scans stored roles and logs every permission that does not resolve, identified by role name. Fails closed and loudly; the seeded
*grant is unaffected so an instance cannot be locked out. Verified done 2026-08-27. - T044 [P] [US5] Regression-test that legacy stored grants no longer authorize, that
*still authorizes everything, and that a legacy endpoint declaration still resolves through T027. Still open (verified 2026-08-27) — no legacy-stored-grant regression test found.
Phase 4: Introspection, Revocation, and Audit
- T045 [US4] Implement
GET /identity/me/permissionsinsrc/modules/Elsa.Identity/Endpoints/Me/Permissions/Endpoint.cs. Every registered resource is present, including those the caller cannot access, carrying an empty verb list. Wildcard grants are resolved to concrete verbs per covered resource. Contract:contracts/rest-api.md. Verified done 2026-08-27. - T046 [US6] Lower the default
AccessTokenLifetimeinsrc/modules/Elsa.Identity/Options/IdentityTokenOptions.csfrom 1 hour to 15 minutes. Refresh already rotates both tokens and re-reads roles, so no client change is required; leaveRefreshTokenLifetimeat 2 hours. Verified done 2026-08-27. - T047 [US6] Add an optional per-principal security stamp: a monotonic value bumped on any role, grant or membership change, carried as a claim and compared against a per-node cached value under a configurable interval. Must not depend on cross-node cache invalidation, which Elsa does not have. Verified done 2026-08-27.
- T047a [US6] Ship the stamp's persistence with it: if the stamp lives on
User, this task includes the Identity migrations for all five EF providers, so Phase 4 remains independently shippable and does not depend on T053 in Phase 5. Prefer a store that avoids an entity-schema change if one fits. Verified done 2026-08-27. Note: satisfied without an entity-schema change — the stamp is computed byPermissionStampCalculatorrather than persisted onUser, so no Identity migrations were required. - T048 [US7] Publish typed security notifications for role create, update and delete, and for user role assignment and removal, through
INotificationSenderper ADR 0007. This feature owns no audit persistence. Verified done 2026-08-27. - T049 [US1] Promote the deployment grant boundary (
PermissionGrantOptions.AllowedPermissions/DeniedPermissions, currently inExternalAuthenticationOptions) intoElsa.Identityso it applies to all grant paths, and make it wildcard-aware. Still open (verified 2026-08-27) —PermissionGrantOptionsstill lives inElsa.ExternalAuthentication; not promoted toElsa.Identity. - T050 [P] [US4] Integration-test
/me/permissionsfor a role holding partial verbs, for a wildcard grant resolving to concrete verbs, and for denied resources appearing with an empty list. Still open (verified 2026-08-27) — no/me/permissionsintegration test found. - T051 [P] [US6] Integration-test that a revoked role stops authorizing on the next token issuance, and immediately within the stamp interval when the stamp is enabled. Still open (verified 2026-08-27) — no revocation / stamp-interval integration test found.
Phase 5: Tenancy Hardening
Purpose: Close the gaps that make "roles are per tenant" unsafe today. Independently justified as latent-defect fixes; sequenced last so the unresolved isolation-boundary question does not block delivery.
- T052 [US9] Replace the global unique indexes on
User.Name,Role.Name,Application.ClientIdandApplication.Namewith per-tenant composite indexes insrc/modules/Elsa.Persistence.EFCore/Modules/Identity/Configurations.cs. Verified done 2026-08-27. - T053 [US9] Generate Identity migrations for all five providers — Sqlite, SqlServer, PostgreSql, MySql, Oracle. Verified done 2026-08-27.
- T054 [US9] Make
src/modules/Elsa.Common/Services/MemoryStore.cstenant-aware so the defaultIUserStoreandIRoleStoreisolate, rather than isolation existing only on the Entity Framework path whenTenantsOptions.IsEnabled. Still open (verified 2026-08-27) —Elsa.Common/Services/MemoryStore.cshas no tenant awareness (noTenantId/ITenantAccessorreference). - T055 [US9] Apply explicit tenant filters in
Endpoints/Users/ListandEndpoints/Roles/List, which currently pass an empty filter, and setTenantIdexplicitly inUserManager.CreateUserAsyncrather than relying on the saving handler. Verified done 2026-08-27. - T056 [P] [US9] Integration-test that two tenants can each hold a role named
Admin, and that roles and users listed in one tenant are invisible in another, against both the Entity Framework and in-memory stores. Still open (verified 2026-08-27) — no cross-tenant identity isolation test verified for both EF and in-memory stores.
Phase 6: Documentation and Release Readiness
- T057 [US5] Write
doc/migrations/authorization-model.mdfrom the mapping table incontracts/permissions.md, following the shape ofdoc/migrations/external-authentication-persistence.md. It must state prominently that the migration expands rather than renames — several legacy permissions map to more than one new permission, and a one-for-one substitution silently narrows roles. Done 2026-08-24 —doc/migrations/authorization-model.mdleads with the three things that are not a simple rename, and tells operators to confirm*still works first. - T058 [US5] Document in the same file that
read:*andexec:*become materially more powerful as*:viewand*:execute, and that any role holding them needs human review rather than an automated rewrite. Done 2026-08-24 - T059 [US5] Document in the same file the removal of
exec:csharp-expressionsandexec:python-expressionsas a deliberate reduction in control: where host code is enabled, any author who may write definitions may use C# and Python. Link #7975. Done 2026-08-24 — linked to #7975. - T060 Record the model in
doc/adr/0025-two-axis-authorization-model.md: both axes open, wildcards as the only forward reach, no aggregates and no verb implication, and the rejection of a closed verb enumeration. Done 2026-08-24 —doc/adr/0025-two-axis-authorization-model.md. - T061 [P] Update
doc/wiki/identity-tenancy-security.md, replacing the Secrets-only route table with a pointer to the catalog endpoint as the authoritative source. Done 2026-08-24 - T062 Resolve the five module-owner questions and fold the answers into the vocabulary. Done 2026-08-23 — outcomes recorded at the end of
contracts/permissions.mdand as D25/D26 inresearch.md. Produced #7976 and #7977, and added FR-019's third declaration state (T026a). - T063 Run
dotnet build Elsa.slnand the full test suite, confirm T041 passes with zero exemptions beyond documented anonymous endpoints, and verify the quickstart scenario end to end. Still open (verified 2026-08-27) — fulldotnet build Elsa.sln+ full suite run not recorded.
Dependencies
- Phase 1 blocks everything. No descriptor work starts before the matcher and evaluator pass their tests.
- Phase 2 blocks Phase 3: an endpoint cannot declare a resource that has no descriptor, because T041 asserts resolution.
- T062 preceded Phase 2 and is complete, so the vocabulary is settled before any module declares constants against it.
- T026 and T027 block T028–T036. Within that range the module tasks are independent and land in any order.
- T041 lands with the last module migration, not before, or trunk fails while migration is in flight.
- Phase 5 is independent of Phases 3 and 4 and may run in parallel with either.
- T057–T059 must land in the same release as Phase 3, since they document its breaking changes.
Parallel guidance
Phase 2 is almost entirely parallel — sixteen module tasks touching disjoint files. Phase 3 is parallel after T026 and T027, with T028 (Workflows.Api, 78 files) the critical path and a candidate for splitting by endpoint folder. Phase 5 touches persistence and common services and should not run concurrently with itself.