Repository navigation
feat(auth)!: structured authorization model, phases 1-6 - #7980
Conversation
Greptile SummaryThis update moves API authorization to structured resource-and-verb permissions, adds permission discovery and declaration coverage support, and scopes permission-stamp validation to each tenant. No blocking failure remains. Confidence Score: 5/5No blocking failure remains. No accepted blocking findings remain after review of the current implementation.
What T-Rex did
Reviews (7): Last reviewed commit: "ci(bounty): point the footer step at the..." | Re-trigger Greptile |
cd47dce to
125782e
Compare
CI on #7980 was red. Running the full suite locally rather than the subset I had been checking surfaced 24 failures across four projects, in three distinct classes. Enforcement no longer depends on a DI registration. RequirePermission attached a PermissionRequirement evaluated by a registered handler, so a host that had not called AddElsaAuthorization got 403 on every endpoint with nothing to indicate why. Several test hosts wire FastEndpoints directly and did exactly that. The requirement is now evaluated inline against a shared stateless evaluator, with a host-registered IPermissionEvaluator still taking precedence. Registration remains worthwhile for the catalog and the validator; authorization can no longer silently fail closed because of a missing one. Registration also moved from AddFastEndpointsFromModule to AddFastEndpointsAssembly. Registering an endpoint assembly is what should guarantee its permissions work, and a host may never call the former. Finishes T039. The four SignalR hubs still matched hard-coded legacy permission strings, which no longer exist, so every hub denied access. They now route through the evaluator like every other enforcement path. Test fixtures granting legacy strings were updated to the new vocabulary. Two categories were deliberately left alone: naming tests asserting the legacy constants still hold their old values, which is true and worth keeping, and the workflow script authorization tests, which asserted a MissingPermission outcome that D21 removed -- those now assert the host switch is the only control. One test previously pinned that the hub honors a FastEndpoints-configured permissions claim type. It now asserts the opposite, and says why: Elsa is the only authority that expands roles into permission claims (ADR 0009), and this model no longer uses the FastEndpoints permission mechanism, so its separately configurable claim type is not consulted. That property is also unreadable outside reflection. Whole solution builds with 0 errors and every test project passes. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@greptileai review |
There was a problem hiding this comment.
Pull request overview
Implements the new structured RBAC authorization model (spec 013 / #7978 / #7974) across Elsa Core: open {resource}:{verb} permissions with hierarchical resources, wildcard matching, centralized evaluation, module-contributed catalogs, fail-closed endpoint coverage, and supporting Identity hardening (tenancy + revocation bound + audit notifications).
Changes:
- Replaces legacy permission-string usage with
RequirePermission(resource, verb)and a centralizedIPermissionEvaluator, including SignalR hub authorization. - Introduces module-contributed permission descriptors and Identity endpoints for catalog (
/identity/permissions), reach (/identity/permissions/reach), and effective grants (/identity/me/permissions), plus grant validation on role writes. - Hardens Identity tenancy and revocation behavior (per-tenant uniqueness, in-memory tenant isolation, 15-minute access tokens, optional permission-stamp infrastructure), and adds endpoint coverage tests.
Reviewed changes
Copilot reviewed 254 out of 259 changed files in this pull request and generated 4 comments.
Show a summary per file
File table
| File | Description |
|---|---|
| test/unit/Elsa.Workflows.Api.UnitTests/RealTime/Hubs/WorkflowInstanceHubTests.cs | Updates hub auth tests to new permission vocabulary/claim source. |
| test/unit/Elsa.Workflows.Api.UnitTests/OutputConverters/OutputConverterEndpointTests.cs | Asserts endpoint permission via registry instead of FastEndpoints AllowedPermissions. |
| test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/RuntimeAdmin/RuntimeAdminAuthorizationTests.cs | Updates runtime-admin auth expectations to resource+verb model. |
| test/unit/Elsa.Workflows.Api.UnitTests/Authorization/EndpointCoverageTests.cs | Adds coverage gate ensuring every endpoint declares access. |
| test/unit/Elsa.Identity.UnitTests/Services/RoleManagerTests.cs | Updates in-memory role store tests for tenant-aware stores. |
| test/unit/Elsa.Identity.UnitTests/Services/RoleDeletionCoordinatorTests.cs | Updates coordinator tests for tenant-aware stores + evaluator-based role auth. |
| test/unit/Elsa.Identity.UnitTests/Services/RoleAuthorizationServiceTests.cs | Updates role auth tests for evaluator-based containment checks. |
| test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs | Updates in-memory user store tests for tenant-aware stores. |
| test/unit/Elsa.Identity.UnitTests/HostedServices/AdminUserInitializerTests.cs | Updates role store setup for tenant-aware stores. |
| test/unit/Elsa.Identity.UnitTests/Authorization/EndpointCoverageTests.cs | Adds coverage gate ensuring every identity endpoint declares access. |
| test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/InMemoryExternalIdentityProvisionerTests.cs | Updates in-memory user store setup for tenant-aware stores. |
| test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationUserDeletionDependencyContributorTests.cs | Updates in-memory user store setup for tenant-aware stores. |
| test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ExternalAuthenticationRoleDeletionDependencyContributorTests.cs | Updates role auth service construction for new evaluator-based model. |
| test/unit/Elsa.Api.Common.UnitTests/Elsa.Api.Common.UnitTests.csproj | Adds Api.Common unit test project configuration for coverage gating. |
| test/unit/Elsa.Api.Common.UnitTests/Authorization/PermissionTests.cs | Adds parsing/wildcard classification tests for new Permission model. |
| test/integration/Elsa.ExternalAuthentication.IntegrationTests/Broker/BrokerSecurityTests.cs | Updates in-memory user store setup for tenant-aware stores. |
| test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/OpenTelemetryHubTests.cs | Updates hub auth tests to new permission vocabulary. |
| test/integration/Elsa.Diagnostics.OpenTelemetry.IntegrationTests/OpenTelemetryAuthorizationTests.cs | Updates endpoint auth assertions to use permission registry. |
| src/modules/Elsa.Workflows.Api/Security/WorkflowDefinitionScriptAuthorizationService.cs | Removes per-author script permissions; host switch becomes the sole gate. |
| src/modules/Elsa.Workflows.Api/RealTime/Hubs/WorkflowInstanceHub.cs | Switches hub permission checks to PermissionEvaluator + resource/verb permissions. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Variables/Post/Endpoint.cs | Updates workflow instance variable write endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Variables/List/Endpoint.cs | Updates workflow instance variable read endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs | Updates workflow instance list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/List/Endpoint.cs | Updates journal list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/GetLastEntry/Endpoint.cs | Updates journal entry endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/FilteredList/Endpoint.cs | Updates journal filtered list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs | Updates workflow instance import endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Endpoint.cs | Updates workflow instance get endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Export/Endpoint.cs | Updates workflow instance export endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/ExecutionState/Endpoint.cs | Updates execution-state endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Delete/Endpoint.cs | Updates delete endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Cancel/Endpoint.cs | Updates cancel endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Endpoint.cs | Updates bulk delete endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Endpoint.cs | Updates bulk cancel endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs | Updates revert version endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/List.cs | Updates list versions endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs | Updates delete version endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs | Updates update-references endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs | Updates retract endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Reload/Endpoint.cs | Updates reload endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Refresh/Endpoint.cs | Updates refresh endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs | Updates publish endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs | Updates write endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs | Updates list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/IsNameUnique/Endpoint.cs | Updates validation endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs | Updates import-files endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs | Updates import endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Graph/Segments/Endpoint.cs | Updates subgraph segments endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Graph/Endpoint.cs | Updates subgraph endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetManyById/Endpoint.cs | Updates get-many endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetById/Endpoint.cs | Updates get-by-id endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetByDefinitionId/Endpoint.cs | Updates get-by-definitionId endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs | Updates export endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/PostEndpoint.cs | Updates execute (POST) endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/GetEndpoint.cs | Updates execute (GET) endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs | Updates dispatch endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs | Updates delete-version-by-id endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs | Updates delete endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Count/Endpoint.cs | Updates count endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Consumers/Endpoint.cs | Updates consumers endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs | Updates bulk retract endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs | Updates bulk publish endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs | Updates bulk dispatch endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs | Updates bulk delete versions endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs | Updates bulk delete endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/VariableTypes/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/Tests/Activities/Endpoint.cs | Updates test activity endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/Tasks/Complete/Endpoint.cs | Updates tasks endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/StorageDrivers/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs | Removes privileged-expression permission filtering; uses host descriptor IsBrowsable only. |
| src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Status/Endpoint.cs | Tightens status endpoint to runtime:view only. |
| src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Resume/Endpoint.cs | Updates mutating endpoint to runtime:control verb. |
| src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/Pause/Endpoint.cs | Updates mutating endpoint to runtime:control verb. |
| src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin/ForceDrain/Endpoint.cs | Updates mutating endpoint to runtime:control verb. |
| src/modules/Elsa.Workflows.Api/Endpoints/OutputConverters/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/LogPersistenceStrategies/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/Features/List/Endpoint.cs | Updates installed-features endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/Features/Get/Endpoint.cs | Updates installed-feature endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/Events/TriggerAuthenticated/Endpoint.cs | Updates event trigger endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs | Updates descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/BookmarkQueueDeadLetters/Replay/Endpoint.cs | Updates dead-letter replay endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/BookmarkQueueDeadLetters/List/Endpoint.cs | Updates dead-letter list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/BookmarkQueueDeadLetters/Get/Endpoint.cs | Updates dead-letter get endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/BookmarkQueueDeadLetters/Delete/Endpoint.cs | Updates dead-letter delete endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutionSummaries/ListSummaries/Endpoint.cs | Updates activity-execution summaries endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/Report/Endpoint.cs | Updates activity-execution report endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/List/Endpoint.cs | Updates activity-execution list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Endpoint.cs | Updates call-stack endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/Get/Endpoint.cs | Updates get endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/Count/Endpoint.cs | Updates count endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Endpoint.cs | Updates activity descriptors list endpoint to RequirePermission. |
| src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/Get/Endpoint.cs | Updates activity descriptor get endpoint to RequirePermission. |
| src/modules/Elsa.Tenants/Permissions/TenantPermissions.cs | Adds tenants resource constants + descriptor provider. |
| src/modules/Elsa.Tenants/Endpoints/Tenants/Update/Endpoint.cs | Updates tenant update endpoint to RequirePermission. |
| src/modules/Elsa.Tenants/Endpoints/Tenants/Refresh/Endpoint.cs | Updates tenant refresh endpoint to RequirePermission. |
| src/modules/Elsa.Tenants/Endpoints/Tenants/List/Endpoint.cs | Updates tenant list endpoint to RequirePermission. |
| src/modules/Elsa.Tenants/Endpoints/Tenants/Get/Endpoint.cs | Updates tenant get endpoint to RequirePermission. |
| src/modules/Elsa.Tenants/Endpoints/Tenants/Delete/Endpoint.cs | Updates tenant delete endpoint to RequirePermission. |
| src/modules/Elsa.Tenants/Endpoints/Tenants/Add/Endpoint.cs | Updates tenant create endpoint to RequirePermission. |
| src/modules/Elsa.Shells.Api/Permissions/ShellPermissions.cs | Adds shells resource constants + descriptor provider. |
| src/modules/Elsa.Shells.Api/Endpoints/Shells/ReloadAll/Endpoint.cs | Updates shell reload-all endpoint to RequirePermission. |
| src/modules/Elsa.Shells.Api/Endpoints/Shells/Reload/Endpoint.cs | Updates shell reload endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Permissions/SecretsResourcePermissions.cs | Adds secrets resource constants + descriptor provider. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs | Updates secrets update endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Test/Endpoint.cs | Updates secrets test endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Rotate/Endpoint.cs | Updates secrets rotate endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Revoke/Endpoint.cs | Updates secrets revoke endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Picker/Endpoint.cs | Updates secrets picker endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/List/Endpoint.cs | Updates secrets list endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Get/Endpoint.cs | Updates secrets get endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Descriptors/Endpoint.cs | Updates secrets descriptors endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Delete/Endpoint.cs | Updates secrets delete endpoint to RequirePermission. |
| src/modules/Elsa.Secrets/Endpoints/Secrets/Create/Endpoint.cs | Updates secrets create endpoint to RequirePermission. |
| src/modules/Elsa.Resilience/Permissions/ResiliencePermissions.cs | Adds resilience resources + descriptor provider. |
| src/modules/Elsa.Resilience/Endpoints/SimulateResponse/Endpoint.cs | Updates resilience simulate endpoint to RequirePermission. |
| src/modules/Elsa.Resilience/Endpoints/Retries/List/Endpoint.cs | Updates retries endpoint to RequirePermission. |
| src/modules/Elsa.Resilience/Endpoints/ResilienceStrategies/List/Endpoint.cs | Updates strategies endpoint to RequirePermission. |
| src/modules/Elsa.Persistence.EFCore/Modules/Identity/Configurations.cs | Changes uniqueness to per-tenant composite indexes. |
| src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs | Updates SQL Server Identity snapshot for per-tenant uniqueness. |
| src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs | Updates SQLite Identity snapshot for per-tenant uniqueness. |
| src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs | Updates PostgreSQL Identity snapshot for per-tenant uniqueness. |
| src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs | Updates Oracle Identity snapshot for per-tenant uniqueness. |
| src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs | Updates MySQL Identity snapshot for per-tenant uniqueness. |
| src/modules/Elsa.Labels/Permissions/LabelPermissions.cs | Adds labels resources + descriptor provider. |
| src/modules/Elsa.Labels/Endpoints/WorkflowDefinitionLabels/Update/Endpoint.cs | Updates workflow labels update endpoint to RequirePermission. |
| src/modules/Elsa.Labels/Endpoints/WorkflowDefinitionLabels/List/Endpoint.cs | Updates workflow labels list endpoint to RequirePermission. |
| src/modules/Elsa.Labels/Endpoints/Labels/Update/Update.cs | Updates label update endpoint to RequirePermission. |
| src/modules/Elsa.Labels/Endpoints/Labels/Post/Endpoint.cs | Updates label create endpoint to RequirePermission. |
| src/modules/Elsa.Labels/Endpoints/Labels/List/Endpoint.cs | Updates label list endpoint to RequirePermission. |
| src/modules/Elsa.Labels/Endpoints/Labels/Get/Endpoint.cs | Updates label get endpoint to RequirePermission. |
| src/modules/Elsa.Labels/Endpoints/Labels/Delete/Endpoint.cs | Updates label delete endpoint to RequirePermission. |
| src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs | Registers tenant accessor fallback + permission-stamp + validator services for shell host. |
| src/modules/Elsa.Identity/Services/UserManager.cs | Ensures tenant id is set on in-memory user creation path. |
| src/modules/Elsa.Identity/Services/RoleSecurityNotifier.cs | Adds mediator-based audit notifications for role/assignment changes. |
| src/modules/Elsa.Identity/Services/RoleAuthorizationService.cs | Uses evaluator-based containment checks for anti-escalation. |
| src/modules/Elsa.Identity/Services/PermissionStampValidator.cs | Adds optional stamp validation (cache + store lookup) for tighter revocation. |
| src/modules/Elsa.Identity/Services/PermissionStampCalculator.cs | Adds stamp computation derived from roles+permissions. |
| src/modules/Elsa.Identity/Services/MemoryUserStore.cs | Applies ambient-tenant filtering for in-memory user store. |
| src/modules/Elsa.Identity/Services/MemoryRoleStore.cs | Applies ambient-tenant filtering for in-memory role store. |
| src/modules/Elsa.Identity/Services/DefaultAccessTokenIssuer.cs | Issues permission-stamp claim when calculator is available. |
| src/modules/Elsa.Identity/Permissions/IdentityPermissions.cs | Adds identity resources + descriptor provider. |
| src/modules/Elsa.Identity/Options/PermissionStampOptions.cs | Adds config for enabling and caching permission-stamp validation. |
| src/modules/Elsa.Identity/Options/IdentityTokenOptions.cs | Reduces access-token lifetime to 15 minutes. |
| src/modules/Elsa.Identity/Notifications/RoleSecurityNotifications.cs | Adds typed, redacted notifications for audit subscribers. |
| src/modules/Elsa.Identity/Models/RoleFilter.cs | Adds tenant-aware role filtering semantics. |
| src/modules/Elsa.Identity/HostedServices/StoredPermissionValidator.cs | Logs stored role permissions that no longer resolve post-migration. |
| src/modules/Elsa.Identity/Features/IdentityFeature.cs | Registers tenant accessor fallback + stamp infra + stored-permission validator for module host. |
| src/modules/Elsa.Identity/Endpoints/Users/Update/Endpoint.cs | Updates user update endpoint to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Users/List/Endpoint.cs | Updates user list endpoint to RequirePermission + tenant filter. |
| src/modules/Elsa.Identity/Endpoints/Users/Delete/Endpoint.cs | Updates user delete endpoint to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs | Updates user create endpoint to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Roles/Update/Endpoint.cs | Adds grant validation + role-change notification; updates to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Roles/List/Endpoint.cs | Updates role list endpoint to RequirePermission + tenant filter. |
| src/modules/Elsa.Identity/Endpoints/Roles/Delete/RemediateAndDelete.cs | Updates role remediation+delete endpoint to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Roles/Delete/GetDeletionImpact.cs | Updates role deletion-impact endpoint to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Roles/Delete/Endpoint.cs | Updates role delete endpoint to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs | Adds grant validation + role-change notification; updates to RequirePermission. |
| src/modules/Elsa.Identity/Endpoints/Permissions/Reach/Endpoint.cs | Adds wildcard reach-report endpoint. |
| src/modules/Elsa.Identity/Endpoints/Permissions/List/Models.cs | Adds permission-catalog response models. |
| src/modules/Elsa.Identity/Endpoints/Permissions/List/Endpoint.cs | Adds permission-catalog endpoint with Roles:view requirement. |
| src/modules/Elsa.Identity/Endpoints/Me/Permissions/Models.cs | Adds effective-grants response models. |
| src/modules/Elsa.Identity/Endpoints/Me/Permissions/Endpoint.cs | Adds authenticated-only endpoint returning effective grants. |
| src/modules/Elsa.Identity/Endpoints/Applications/Create/Endpoint.cs | Updates application create endpoint to RequirePermission. |
| src/modules/Elsa.ExternalAuthentication/Endpoints/Sessions/ExternalAuthenticationSessionEndpoints.cs | Updates session endpoints to RequirePermission. |
| src/modules/Elsa.ExternalAuthentication/Endpoints/Previews/PreviewSignInEndpoints.cs | Updates preview endpoints to RequirePermission. |
| src/modules/Elsa.ExternalAuthentication/Endpoints/IdentityLinks/IdentityLinkEndpoints.cs | Updates identity-link endpoints to RequirePermission. |
| src/modules/Elsa.ExternalAuthentication/Endpoints/Descriptors/DescriptorCatalogEndpoints.cs | Updates descriptor endpoints to RequirePermission. |
| src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/TestConnection.cs | Updates connection test endpoint to RequirePermission. |
| src/modules/Elsa.ExternalAuthentication/Endpoints/Broker/Logout.cs | Makes logout endpoint explicitly authenticated-only (no permission). |
| src/modules/Elsa.Expressions.JavaScript/Permissions/JavaScriptPermissions.cs | Adds JS scripting resource + descriptor provider. |
| src/modules/Elsa.Expressions.JavaScript/Endpoints/TypeDefinitions/Endpoint.cs | Updates type-definitions endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.StructuredLogs/Permissions/StructuredLogsResourcePermissions.cs | Adds structured logs resource + descriptor provider. |
| src/modules/Elsa.Diagnostics.StructuredLogs/Endpoints/StructuredLogs/Storage/Endpoint.cs | Updates structured logs endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.StructuredLogs/Endpoints/StructuredLogs/Sources/Endpoint.cs | Updates structured logs endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.StructuredLogs/Endpoints/StructuredLogs/Recent/Endpoint.cs | Updates structured logs endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/RealTime/OpenTelemetryHub.cs | Updates hub auth to evaluator-based permissions. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Permissions/OpenTelemetryResourcePermissions.cs | Adds OpenTelemetry resource + descriptor provider. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Traces/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Trace/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Storage/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Resources/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Metrics/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/Logs/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.OpenTelemetry/Endpoints/OpenTelemetry/CollectorConfiguration/Endpoint.cs | Updates OpenTelemetry endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.ConsoleLogs/Services/ElsaConsoleLogStreamHubAuthorizer.cs | Updates console-log hub auth to evaluator-based permissions. |
| src/modules/Elsa.Diagnostics.ConsoleLogs/Permissions/ConsoleLogsResourcePermissions.cs | Adds console logs resource + descriptor provider. |
| src/modules/Elsa.Diagnostics.ConsoleLogs/Endpoints/ConsoleLogs/Sources/Endpoint.cs | Updates console logs endpoint to RequirePermission. |
| src/modules/Elsa.Diagnostics.ConsoleLogs/Endpoints/ConsoleLogs/Recent/Endpoint.cs | Updates console logs endpoint to RequirePermission. |
| src/modules/Elsa.Dashboard.Api/Permissions/DashboardResourcePermissions.cs | Adds dashboard resource + descriptor provider. |
| src/modules/Elsa.Dashboard.Api/Endpoints/Dashboard/WorkflowTrends/Endpoint.cs | Updates dashboard endpoint to RequirePermission. |
| src/modules/Elsa.Dashboard.Api/Endpoints/Dashboard/WorkflowHotspots/Endpoint.cs | Updates dashboard endpoint to RequirePermission. |
| src/modules/Elsa.Dashboard.Api/Endpoints/Dashboard/RecentActivity/Endpoint.cs | Updates dashboard endpoint to RequirePermission. |
| src/modules/Elsa.Dashboard.Api/Endpoints/Dashboard/Overview/Endpoint.cs | Updates dashboard endpoint to RequirePermission. |
| src/modules/Elsa.Dashboard.Api/Endpoints/Dashboard/NeedsAttention/Endpoint.cs | Updates dashboard endpoint to RequirePermission. |
| src/modules/Elsa.Bpmn.Interchange/Permissions/BpmnPermissions.cs | Defines workflow definitions resource constant for BPMN module (no descriptor). |
| src/modules/Elsa.Bpmn.Interchange/Endpoints/Bpmn/Import/Endpoint.cs | Updates BPMN import endpoint to RequirePermission. |
| src/modules/Elsa.Bpmn.Interchange/Endpoints/Bpmn/Export/Endpoint.cs | Updates BPMN export endpoint to RequirePermission. |
| src/modules/Elsa.Bpmn.Interchange/Endpoints/Bpmn/Analyze/Endpoint.cs | Updates BPMN analyze endpoint to RequirePermission. |
| src/modules/Elsa.Alterations/Permissions/AlterationPermissions.cs | Adds alterations resource + descriptor provider. |
| src/modules/Elsa.Alterations/Endpoints/Workflows/Retry/Endpoint.cs | Updates alterations endpoint to RequirePermission. |
| src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs | Updates alterations endpoint to RequirePermission. |
| src/modules/Elsa.Alterations/Endpoints/Alterations/Run/Endpoint.cs | Updates alterations endpoint to RequirePermission. |
| src/modules/Elsa.Alterations/Endpoints/Alterations/Get/Endpoint.cs | Updates alterations endpoint to RequirePermission. |
| src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs | Updates alterations endpoint to RequirePermission. |
| src/modules/Elsa.AI.Host/Permissions/AIResourcePermissions.cs | Adds AI resources + descriptor provider. |
| src/modules/Elsa.AI.Host/Endpoints/AI/Tools/Endpoint.cs | Updates AI tools endpoint to RequirePermission. |
| src/modules/Elsa.AI.Host/Endpoints/AI/Chat/Endpoint.cs | Updates AI chat endpoint to RequirePermission. |
| src/modules/Elsa.AI.Host/Endpoints/AI/Capabilities/Endpoint.cs | Updates AI capabilities endpoint to RequirePermission. |
| src/common/Elsa.Testing.Shared/Multitenancy/TestTenantAccessor.cs | Adds shared tenant accessor fixture for unit/integration tests. |
| src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj | Adds dependency needed for multitenancy test utilities. |
| src/common/Elsa.Api.Common/ShellFeatures/ElsaFastEndpointsFeature.cs | Ensures authorization model + descriptor discovery are registered for shell host. |
| src/common/Elsa.Api.Common/Permissions/PermissionGrantValidator.cs | Adds grant-validation service against descriptor registry. |
| src/common/Elsa.Api.Common/Permissions/PermissionDescriptor.cs | Adds module-contributed descriptor record model. |
| src/common/Elsa.Api.Common/Permissions/IPermissionDescriptorRegistry.cs | Adds registry contract (list/find/reach). |
| src/common/Elsa.Api.Common/Permissions/IPermissionDescriptorProvider.cs | Adds provider contract for module descriptor contributions. |
| src/common/Elsa.Api.Common/Permissions/DefaultPermissionDescriptorRegistry.cs | Adds default registry implementation (composed from providers). |
| src/common/Elsa.Api.Common/Extensions/ModuleExtensions.cs | Ensures module endpoint assemblies register authorization + descriptors. |
| src/common/Elsa.Api.Common/Authorization/PermissionRequirement.cs | Adds requirement type for ASP.NET authorization policies. |
| src/common/Elsa.Api.Common/Authorization/PermissionMatcher.cs | Adds wildcard/hierarchy matching logic. |
| src/common/Elsa.Api.Common/Authorization/PermissionEvaluator.cs | Adds stateless evaluator reading Elsa permission claims. |
| src/common/Elsa.Api.Common/Authorization/PermissionAuthorizationHandler.cs | Adds authorization handler bridging requirement â evaluator. |
| src/common/Elsa.Api.Common/Authorization/IPermissionEvaluator.cs | Adds evaluator contract. |
| src/common/Elsa.Api.Common/Authorization/EndpointPermissionRegistry.cs | Adds registry for introspecting endpoint-declared permissions. |
| src/common/Elsa.Api.Common/Authorization/CoreVerbs.cs | Adds recommended core verb convention set. |
| doc/wiki/identity-tenancy-security.md | Updates documentation to reflect new permission model and endpoints. |
Files not reviewed (5)
- src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Identity/20260823231449_PerTenantIdentityUniqueness.Designer.cs: Generated file
- src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Identity/20260823231459_PerTenantIdentityUniqueness.Designer.cs: Generated file
- src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Identity/20260823231437_PerTenantIdentityUniqueness.Designer.cs: Generated file
- src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Identity/20260823231421_PerTenantIdentityUniqueness.Designer.cs: Generated file
- src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Identity/20260823231406_PerTenantIdentityUniqueness.Designer.cs: Generated file
Suppressed comments (1)
src/modules/Elsa.Identity/Services/PermissionStampValidator.cs:43
- The permission-stamp cache key is only scoped by user name. Because user names are now unique per tenant (not globally), this can mix stamps between tenants. Also,
FindAsyncshould includeTenantIdto avoid resolving the wrong user when multiple tenants have the same name.
ðĄ Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Additive only. Nothing changes behavior: no endpoint declares against this
yet, and no existing enforcement path routes through it.
A permission is {resource}:{verb}, both axes open and string-keyed. A
trailing wildcard on the resource axis matches the named node and every
descendant at any depth, so workflows/definitions/* covers
workflows/definitions itself; * on the verb axis matches any verb.
Wildcards are the only construct with forward reach.
A bare * parses to *:* at parse time rather than being special-cased in
the evaluator, so superuser stays an ordinary grant and a stored or seeded
* keeps authorizing across the vocabulary migration without a lock-out
window.
Adds:
- Permission, with parsing that rejects a value containing a comma, since
the persistence converter joins collections with one
- CoreVerbs, the recommended set modules should reuse; a convention rather
than a closed vocabulary
- PermissionMatcher, one matching rule shape on both axes
- IPermissionEvaluator, the single place permission decisions are made,
skipping malformed claims so one bad stored grant cannot deny a principal
- PermissionRequirement and PermissionAuthorizationHandler
- The descriptor catalog in core: PermissionDescriptor now carries the
verbs a resource supports and marks non-core ones, and the registry can
report what a wildcard covers today
External Authentication keeps its own descriptor types for now; it moves to
the core catalog with the other modules in Phase 2, which keeps this change
purely additive.
55 unit tests cover the matcher table, wildcard forward reach, the
counterpart that concrete grants stay frozen, absence-is-denial, and the
seeded * case.
Refs #7974
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
âĶe 2) Still additive. Existing endpoints keep their legacy declarations; nothing changes behavior for them. Every module exposing protected endpoints now declares its resources and the verbs each accepts, following the pattern already proven in External Authentication -- constants and descriptors colocated -- refined to one constant per resource, with the verb supplied separately. 47 resources across 15 modules, matching the settled vocabulary. Descriptors are discovered from the same assemblies as a module's endpoints, in AddFastEndpointsFromModule. Registering them per module would let the catalog and the endpoints drift, which is the failure this model exists to remove; tying them to one registration makes the catalog necessarily describe the endpoints that exist. Adds: - GET /identity/permissions, the catalog a role editor renders from, so no client hard-codes permission strings - GET /identity/permissions/reach, reporting what a wildcard covers today. This is the mitigation for forward reach on the resource axis: a wildcard is useful precisely because it covers things that do not exist yet, so an author needs to see what it reaches now - GET /identity/me/permissions, resolving wildcards to concrete verbs so a client needs no matching logic, and listing denied resources with an empty verb list so "denied" is distinguishable from "unknown" - IPermissionGrantValidator, wired into Roles/Create and Roles/Update, which previously persisted request.Permissions after only the caller-subset check. Concrete segments validate against the catalog; wildcards validate structurally and are accepted even when they match nothing today, since installing a module later is what gives such a grant meaning - RequirePermission(resource, verb) and RequireAuthenticatedOnly() on the endpoint base classes, with the six copy-pasted ConfigurePermissions bodies collapsed into one implementation New endpoints require new-format grants, so during the transition they authorize only for holders of *, which parses to *:*. Phase 3 migrates the rest and closes that gap. 70 unit tests, including the wildcard-accepting validator cases. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BREAKING: legacy permission strings no longer authorize. A permanent alias layer would keep two vocabularies valid forever, so the break is deliberate and reported rather than absorbed. `*` survives unchanged -- it parses to `*:*` -- so an administrator cannot be locked out while roles are re-authored. All 168 declaration call sites across 151 files now use RequirePermission(resource, verb) with the constants their module declares, so a typo is a compile error rather than an unreachable endpoint. Enforcement consolidated onto IPermissionEvaluator: - RoleAuthorizationService evaluates containment through the evaluator rather than by set membership. This matters: a caller holding workflows/*:view can now delegate workflows/definitions:view, which set membership got wrong and which would otherwise force administrators to hold every concrete grant they wish to delegate. - The two Broker/Logout.cs endpoints declare explicitly. Logout is authenticated-only; ContinueLogout is anonymous, matching every other broker callback -- the route handle carries the authority and a top-level browser navigation sends no Authorization header. Removes the C#/Python expression permissions (#7975). They conflated an incoherent execution-side gate -- a workflow runs under the server's authority, not the caller's, so the check never constrained what a script could do -- with a meaningful authoring-side one. The host switch (AllowHostCodeExecution) becomes the single control. This is a deliberate reduction in control: where host code is enabled, any author who may write definitions may use C# and Python. Adds the fail-closed gate. Omitting a declaration previously inherited the FastEndpoints default with no Elsa-level fallback, so an endpoint could ship ungated unnoticed. EndpointCoverage asserts every endpoint declares exactly one of RequirePermission, RequireAuthenticatedOnly or AllowAnonymous, with no exemption list. Its canary assertion earned its keep immediately by catching that the gate was scanning an assembly containing no endpoints. EndpointPermissionRegistry records what each endpoint declares. The requirement is attached as an inline policy and is not readable back from the definition, so this keeps the declaration introspectable -- and lets tests assert a specific requirement rather than merely that one exists. Two behavior notes worth calling out: - The runtime status endpoint previously accepted either the read or the manage permission. It now requires workflows/runtime:view alone, which is least privilege; a role holding only control must also be granted view to read status. - BPMN interchange repeats the workflow-definitions path locally rather than taking a dependency on Elsa.Workflows.Api for one constant. It contributes no descriptor: the resource is owned and described by Workflows.Api, and the registry keeps one entry per resource. Also adds a startup validator that logs every stored role permission that no longer resolves, identified by role, so an upgrade is loud. 188 unit tests pass across Api.Common, Workflows.Api and Identity. Refs #7974, #7975, #7976 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Default access-token lifetime drops from 1 hour to 15 minutes. This is the revocation bound: permission claims are issued at sign-in and refresh re-reads the user's roles, so removing a role takes effect at most one access-token lifetime later. Refresh already rotates both tokens, so no client change is required and the refresh lifetime is unchanged. Adds an optional permission stamp for deployments needing a tighter bound. The stamp is derived from the user's roles and their permissions rather than stored as a counter on the user. That avoids changing the Identity schema, which would have required migrations across all five EF providers and made this milestone depend on the tenancy work. It also means every node computes the same value from the same store with no cross-node cache invalidation, which matters because Elsa has none. The stamp is issued unconditionally and only validated when enabled, so turning it on does not invalidate tokens already in flight; an absent stamp is not treated as a mismatch for the same reason. It changes when a role is added to or removed from the user and when a held role's permissions change, but not when an unrelated role changes. Role create and update now publish typed security notifications per ADR 0007, carrying the resulting grants so a reviewer can reconstruct what a role conferred at a point in time without replaying every prior event. This module owns no audit store: a future audit module subscribes and sets its own retention. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the gaps that made "roles are configurable per tenant" untrue however the rest of the stack behaved. Uniqueness becomes per tenant. User.Name, Role.Name, Application.Name and Application.ClientId carried globally unique indexes, so two tenants could not both hold a role named Admin. Migrations for all five EF providers drop the global indexes and create composite ones on (TenantId, Name). The in-memory user and role stores now scope to the ambient tenant. Isolation previously existed only on the Entity Framework path, and only when multitenancy was enabled, so a deployment running the default stores had none at all. The tenant-agnostic sentinel is honored, matching the EF query filter, so a shared platform role stays visible from every tenant. RoleFilter gains TenantId, matching UserFilter, and the role and user list endpoints pass it explicitly rather than relying on an ambient filter that only exists on one persistence path. UserManager.CreateUserAsync sets TenantId explicitly instead of relying on the EF saving handler, which does not run in memory and left users unassigned there. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
âĶodel Adds docs/migrations/authorization-model.md, following the shape of the external-authentication persistence guide. It leads with the three things that are not a simple rename, because each silently produces a wrong result if treated as one: - The migration expands where new sub-resources are finer-grained than what they replace, so a one-for-one substitution narrows roles. - read:* and exec:* become materially more powerful. They are literal claim values today, authorizing twelve of roughly forty read endpoints; their replacements work as the names always implied. Any role holding them needs review by hand, not an automated rewrite. - The C#/Python expression permissions are removed rather than translated, which is a deliberate reduction in control where host code is enabled. It also states plainly that `*` keeps working, and says to do that first, since it is what stops an instance locking itself out mid-migration. ADR 0012 records the model and, more usefully, why a closed verb enumeration was drafted and rejected: it was justified on implication, but aggregates were already excluded and no verb implies another, so the bitwise check was expressing set containment all along. The security wiki's API Authorization section replaces its Secrets-only route table with the catalog endpoint as the authoritative source, and states why read-only mode is a separate axis rather than a permission. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The whole solution builds with zero errors and every affected suite passes: 70 Api.Common, 23 Workflows.Api, 95 Identity, 154 External Authentication unit, 133 External Authentication integration. Most breakage was test call sites constructing the tenant-aware stores and the evaluator-backed RoleAuthorizationService directly. Adds TestTenantAccessor to Elsa.Testing.Shared rather than giving the production constructors an optional accessor, which would have let a missing registration silently disable isolation. Several External Authentication tests created fixtures in tenant-a while running under the default tenant, so the newly isolating store correctly stopped finding them. They are now scoped to the tenant their own fixtures use; JustInTimeProvisioningTests, which genuinely spans two tenants, is scoped per case. One production fix came out of it: IdentityFeature now ensures an ITenantAccessor with TryAdd. The identity stores are tenant-scoped, so a host that never enables multitenancy would otherwise fail to construct them -- which is what the DI registration tests were reporting. TryAdd leaves MultitenancyFeature's own registration untouched. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running Elsa.Server.Web, not by the test suites: the app failed at startup with "Unable to resolve service for type RoleSecurityNotifier while attempting to activate Roles.Update". RoleSecurityNotifier, the permission stamp services, the memory cache and the stored-permission validator were registered only in the CShells shell feature. Elsa.Server.Web uses the classic UseIdentity() path, whose IdentityFeature registered none of them, so every host on that path crashed while mapping endpoints. Unit tests did not catch it because they construct services directly rather than through either feature. Verified end to end against the running server: - The seeded admin role stores "*". It parsed to *:* and resolved to concrete verbs across all 27 registered resources, which is the bare-wildcard parse rule working on real data rather than in a test. - GET /identity/permissions returns the catalog for the modules this app installs -- 27 resources, 0 unverified, categories Dashboard, Identity, Resilience and Workflows -- rather than all 47, which is correct: the catalog describes what is installed. - GET /identity/permissions/reach?resource=workflows/* reports 19 covered resources. - A role holding only dashboard:view gets 200 on /dashboard/overview and 403 on /identity/roles, /identity/users, /workflow-definitions and /identity/permissions, while /identity/me/permissions returns 200 because it declares RequireAuthenticatedOnly -- confirming FR-019's third declaration state behaves as designed. - That same principal's /me/permissions lists all 27 resources with 26 carrying an empty verb list, so "denied" stays distinguishable from "unknown to this server". - The startup validator logged no unresolvable permissions, as expected for a seed holding only "*". Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running Elsa.ModularServer.Web. The shell host started cleanly and authorized correctly, but GET /identity/permissions returned zero resources and /identity/me/permissions returned no grants. Descriptor discovery was wired into AddFastEndpointsFromModule, which only the classic module path calls. CShells discovers endpoints from features implementing its own marker interface, so on a shell host no provider was ever registered. Authorization still worked, because the evaluator reads claims and needs no descriptors -- which is exactly why nothing failed loudly. What silently broke was everything built on the catalog: role authoring would have rejected every concrete grant as an unknown resource, introspection returned nothing for clients to render, and the stored-permission validator would have reported every concrete stored permission as unresolvable. ElsaFastEndpointsFeature now contributes descriptors from the loaded Elsa assemblies, bounded to those and run once per shell. Verified on the modular host, which installs far more modules than Elsa.Server.Web: - 47 resources registered, 0 unverified, across all 12 categories, with all 17 module-specific verbs present. That is the entire published vocabulary confirmed against a running server rather than a document. - Reach reports workflows/* covering 20, external-authentication/* covering 8, and * covering 47. - Creating a role with dashboard:view and workflows/*:view succeeds, confirming a wildcard grant survives authoring validation. - Creating one with invented/resource:view and secrets:publish is rejected with 400. Also makes those rejections actionable. The permission was reported without the reason, so an operator learned which entry was wrong but not why; both parts are now in the message, including the supported verbs for the resource. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI on #7980 was red. Running the full suite locally rather than the subset I had been checking surfaced 24 failures across four projects, in three distinct classes. Enforcement no longer depends on a DI registration. RequirePermission attached a PermissionRequirement evaluated by a registered handler, so a host that had not called AddElsaAuthorization got 403 on every endpoint with nothing to indicate why. Several test hosts wire FastEndpoints directly and did exactly that. The requirement is now evaluated inline against a shared stateless evaluator, with a host-registered IPermissionEvaluator still taking precedence. Registration remains worthwhile for the catalog and the validator; authorization can no longer silently fail closed because of a missing one. Registration also moved from AddFastEndpointsFromModule to AddFastEndpointsAssembly. Registering an endpoint assembly is what should guarantee its permissions work, and a host may never call the former. Finishes T039. The four SignalR hubs still matched hard-coded legacy permission strings, which no longer exist, so every hub denied access. They now route through the evaluator like every other enforcement path. Test fixtures granting legacy strings were updated to the new vocabulary. Two categories were deliberately left alone: naming tests asserting the legacy constants still hold their old values, which is true and worth keeping, and the workflow script authorization tests, which asserted a MissingPermission outcome that D21 removed -- those now assert the host switch is the only control. One test previously pinned that the hub honors a FastEndpoints-configured permissions claim type. It now asserts the opposite, and says why: Elsa is the only authority that expands roles into permission claims (ADR 0009), and this model no longer uses the FastEndpoints permission mechanism, so its separately configurable claim type is not consulted. That property is also unreadable outside reflection. Whole solution builds with 0 errors and every test project passes. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125782e to
8c39f15
Compare
Greptile found and reproduced a cross-tenant authorization bug, and it was mine: Phase 5 made user names unique per tenant rather than globally, but PermissionStampValidator kept caching by user name alone. Tenant A's lookup could therefore populate the cache with its own stamp and satisfy a revoked token belonging to a same-named user in tenant B, without ever resolving tenant B's user. Both the cache key and the user lookup are now tenant-scoped. Added PermissionStampValidatorTests, including the cross-tenant case; verified it fails without the fix and passes with it. Also from review: - Removed the legacy permission constants left unused in the three hubs after they moved to the evaluator, so no stale vocabulary lingers. - Narrowed two generic catch clauses. The IL scanner now catches only the exceptions an unresolvable metadata token actually throws, and the startup validator rethrows cancellation while still refusing to stop the host for anything else -- an unreachable or half-migrated store is exactly when an operator most needs the host up. Whole solution builds with 0 errors and every test project passes. Refs #7974 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@greptile review |
âĶion link Aligns the log message path to the correct documentation directory, changing `docs` to `doc` to avoid confusion and incorrect linking during log output.
The repository had two documentation roots. Merge docs/ into doc/ and remove the empty docs/ tree. The two adr/ folders both numbered from 0001, so the identity and authorization series is renumbered to continue the core series rather than collide with it: docs/adr/0001-0012 -> doc/adr/0014-0025 Every reference is updated to match: the Status cross-links between the renumbered ADRs, the ADR and path links in specs/012-external-authentication and specs/013-rbac-authorization-model, and doc/wiki/identity-tenancy-security.md. doc/adr/toc.md gains entries 14-25. doc/adr/graph.dot is regenerated out to 25; it had been stale since ADR 10 and now also carries the partial supersession edges declared by the ADRs themselves. docs/codebase/ and docs/migrations/ move across unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
âĶasses Streamlined syntax for method definitions by using expression-bodied members and simplified object instantiations across the Authorization module. This includes adjustments in `PermissionEvaluator`, `LocalHostRequirement`, and `WebApplicationExtensions` for better readability and maintainability.
The bounty workflow read docs/bounty-footer.md, the path the file had when the workflow was added in b421b00. The file later moved to doc/bounty/bounty-footer.md and the workflow was never updated, so the read step has been resolving nothing and the appended comment was empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This repo runs two parallel feature systems, the classic Features/ path and the CShells ShellFeatures/ path, and every module has to register in both. Nothing exercised either. The unit and integration suites construct services directly, so a module registered in one path and not the other, or a service missing from one container, passes every test and fails only when a host starts. Three bugs in #7980 were found by running these two hosts by hand, two of them shell-versus-classic divergences. Each host is booted through WebApplicationFactory, running its real Program with full feature registration, and asked for a handful of routes it is expected to serve behind a permission. A 404 means the module was never registered, a 5xx means the endpoint was found but its dependencies could not be constructed, and a 200 means no gate ran; only 401 passes. All routes are reported together, so a feature system that stops registering a group of modules reads as one failure rather than a queue of identical ones. Removing AddExternalAuthenticationServices from the shell feature -- the divergence this is built to catch -- fails the shell host on all five of its routes while the classic host stays green. The assertions go through HTTP rather than the container on purpose. The hosts have different topologies: the classic host's root provider holds everything and registers 125 routes, while CShells gives each shell its own provider and mounts routes per shell, leaving 6 in the root. A container or route-table assertion would have to encode that difference and would break whenever CShells changed internally. Behaviour at the edge is host-agnostic, and it is what actually has to match. Each host gains a namespaced entry-point marker because both already declare a Program in the global namespace, which a test project referencing both cannot tell apart. Coverage is off for this project: it references both hosts, so every module either pulls in would enter its denominator without adding real coverage, and coverlet cannot instrument a graph that size. TreatAsLocalProperty keeps CI's /p:CollectCoverage=true from overriding that. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This repo runs two parallel feature systems, the classic Features/ path and the CShells ShellFeatures/ path, and every module has to register in both. Nothing exercised either. The unit and integration suites construct services directly, so a module registered in one path and not the other, or a service missing from one container, passes every test and fails only when a host starts. Three bugs in #7980 were found by running these two hosts by hand, two of them shell-versus-classic divergences. Each host is booted through WebApplicationFactory, running its real Program with full feature registration, and asked for a handful of routes it is expected to serve behind a permission. A 404 means the module was never registered, a 5xx means the endpoint was found but its dependencies could not be constructed, and a 200 means no gate ran; only 401 passes. All routes are reported together, so a feature system that stops registering a group of modules reads as one failure rather than a queue of identical ones. Removing AddExternalAuthenticationServices from the shell feature -- the divergence this is built to catch -- fails the shell host on all five of its routes while the classic host stays green. The assertions go through HTTP rather than the container on purpose. The hosts have different topologies: the classic host's root provider holds everything and registers 125 routes, while CShells gives each shell its own provider and mounts routes per shell, leaving 6 in the root. A container or route-table assertion would have to encode that difference and would break whenever CShells changed internally. Behaviour at the edge is host-agnostic, and it is what actually has to match. Each host gains a namespaced entry-point marker because both already declare a Program in the global namespace, which a test project referencing both cannot tell apart. Coverage is off for this project: it references both hosts, so every module either pulls in would enter its denominator without adding real coverage, and coverlet cannot instrument a graph that size. TreatAsLocalProperty keeps CI's /p:CollectCoverage=true from overriding that. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This repo runs two parallel feature systems, the classic Features/ path and the CShells ShellFeatures/ path, and every module has to register in both. Nothing exercised either. The unit and integration suites construct services directly, so a module registered in one path and not the other, or a service missing from one container, passes every test and fails only when a host starts. Three bugs in #7980 were found by running these two hosts by hand, two of them shell-versus-classic divergences. Each host is booted through WebApplicationFactory, running its real Program with full feature registration, and asked for a handful of routes it is expected to serve behind a permission. A 404 means the module was never registered, a 5xx means the endpoint was found but its dependencies could not be constructed, and a 200 means no gate ran; only 401 passes. All routes are reported together, so a feature system that stops registering a group of modules reads as one failure rather than a queue of identical ones. Removing AddExternalAuthenticationServices from the shell feature -- the divergence this is built to catch -- fails the shell host on all five of its routes while the classic host stays green. The assertions go through HTTP rather than the container on purpose. The hosts have different topologies: the classic host's root provider holds everything and registers 125 routes, while CShells gives each shell its own provider and mounts routes per shell, leaving 6 in the root. A container or route-table assertion would have to encode that difference and would break whenever CShells changed internally. Behaviour at the edge is host-agnostic, and it is what actually has to match. Each host gains a namespaced entry-point marker because both already declare a Program in the global namespace, which a test project referencing both cannot tell apart. Coverage is off for this project: it references both hosts, so every module either pulls in would enter its denominator without adding real coverage, and coverlet cannot instrument a graph that size. TreatAsLocalProperty keeps CI's /p:CollectCoverage=true from overriding that. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
âĶartup smoke tests for both hosts (#7985) * fix(external-auth)!: match permission grant boundaries as patterns The deployment allow/deny boundary and the delegation authorizer compared permission strings with ordinal equality, so under the {resource}:{verb} vocabulary they could not see wildcards. A deny list naming 'workflows/*:delete' did not deny 'workflows/definitions:delete', and a grant of 'workflows/*:delete' outflanked a deny naming that leaf. The bypass was reachable. ElsaRolePermissionGrantSource passes a role's permissions to the boundary verbatim, survivors land in the issued token as permission claims, and PermissionEvaluator does expand wildcards there. So an ordinary role plus a deny list was enough, on every external sign-in, with no privileged actor involved. Restoring the ordinal boundary under the new tests fails seven of them. Deny is now matched in both directions, allow one-directionally, both through PermissionMatcher. A grant that is not a well-formed permission is dropped with a warning rather than carried into a token it cannot authorize anything in. Five non-endpoint checks -- delegation, role-reference removal, unsafe settings confirmation, the recovery override and the boundary itself -- also still compared against the legacy ExternalAuthenticationPermissions constants. Those carry two colons, so Permission.TryParse rejects them and no principal can hold one, while the migration guide tells operators to replace exactly those strings. All five now route through IPermissionEvaluator, and the module registers AddElsaAuthorization itself instead of depending on host ordering. Non-core verbs move to ExternalAuthenticationVerbs, declared beside the resources they apply to so a delegation check cannot spell one differently from the endpoint it guards. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: apply IDE code cleanup to the diagnostics and identity modules Redundant namespace qualifiers and usings removed, and primary-constructor and record syntax applied, across Elsa.Diagnostics.ConsoleLogs, Elsa.Diagnostics.StructuredLogs, Elsa.Expressions.JavaScript and Elsa.Identity. Produced by a solution-wide IDE cleanup that ran alongside the authorization work; separated from it so the permission changes can be reviewed on their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(hosts): boot both hosts and assert their gated routes challenge This repo runs two parallel feature systems, the classic Features/ path and the CShells ShellFeatures/ path, and every module has to register in both. Nothing exercised either. The unit and integration suites construct services directly, so a module registered in one path and not the other, or a service missing from one container, passes every test and fails only when a host starts. Three bugs in #7980 were found by running these two hosts by hand, two of them shell-versus-classic divergences. Each host is booted through WebApplicationFactory, running its real Program with full feature registration, and asked for a handful of routes it is expected to serve behind a permission. A 404 means the module was never registered, a 5xx means the endpoint was found but its dependencies could not be constructed, and a 200 means no gate ran; only 401 passes. All routes are reported together, so a feature system that stops registering a group of modules reads as one failure rather than a queue of identical ones. Removing AddExternalAuthenticationServices from the shell feature -- the divergence this is built to catch -- fails the shell host on all five of its routes while the classic host stays green. The assertions go through HTTP rather than the container on purpose. The hosts have different topologies: the classic host's root provider holds everything and registers 125 routes, while CShells gives each shell its own provider and mounts routes per shell, leaving 6 in the root. A container or route-table assertion would have to encode that difference and would break whenever CShells changed internally. Behaviour at the edge is host-agnostic, and it is what actually has to match. Each host gains a namespaced entry-point marker because both already declare a Program in the global namespace, which a test project referencing both cannot tell apart. Coverage is off for this project: it references both hosts, so every module either pulls in would enter its denominator without adding real coverage, and coverlet cannot instrument a graph that size. TreatAsLocalProperty keeps CI's /p:CollectCoverage=true from overriding that. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(external-auth)!: fail closed on an unparseable grant boundary Two findings from review, both real. The grant boundary parsed its allow and deny lists and silently dropped what would not parse. An allow list of nothing but malformed entries therefore reduced to an empty set, and an empty allow list means unrestricted -- so a typo turned the boundary off entirely and let external grant sources put permissions straight into issued tokens. The deny side had the mirror of it: a malformed entry quietly stopped denying what it named. A boundary that does not parse now admits nothing, and ExternalAuthenticationOptionsValidator rejects the configuration at startup, so the mistake reaches an operator rather than a token. Failing startup is what makes the runtime behaviour safe to be strict about: it cannot be hit by someone mid-edit, only by validation having been bypassed. ConnectionEndpointSupport.HasPermission was a sixth ad-hoc permission check, missed when the other five were converted. It compared claim values against the legacy ExternalAuthenticationPermissions constants at four call sites -- policy management on create and update, session revocation, and unsafe settings confirmation -- and those constants carry two colons, so nothing can hold one once a deployment follows the migration guide. It now routes through IPermissionEvaluator like the rest, resolved from the request with a fallback to the shared evaluator, the same way EndpointSecurity does it. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style(external-auth): filter permission patterns with Where Addresses a review nit on ValidatePermissionPatterns. Behaviour is unchanged: a null list still iterates nothing, only malformed entries are reported, and the message text is identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(external-auth)!: apply the grant boundary to role permissions too Token issuance concatenated the user's Elsa role permissions raw alongside the boundary-filtered external grants. A permission the boundary had just excluded during grant resolution therefore reappeared in the issued token from the same roles, which made the deny list unenforceable for anything a role carried and left ElsaRolePermissionGrantSource filtering nothing that was not added back a moment later. The bypass did not even need that grant source configured: role permissions reached the token regardless of which sources a connection selected. Both origins now pass the same boundary. Re-applying it at issuance also picks up a boundary that changed since sign-in, since refreshing reissues. This is a behaviour change for deployments that configured a boundary expecting it to bound only claim-mapped permissions: an external login may now carry fewer permissions than before. Deployments with no boundary configured, the default, are unaffected -- every well-formed permission passes. The migration guide describes both directions. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
âĶ descriptor types (#7987) * refactor(auth)!: retire the legacy permission constants and duplicate descriptors Completes the cutover started in #7980. Seven `<Module>Permissions` classes holding `verb:resource` strings are removed: AIPermissions, ConsoleLogs, Dashboard, ExternalAuthentication, OpenTelemetry, Secrets and StructuredLogs. AIPermissions was not in #7982's list, which was written before the cutover finished; it is dead by the same measure as the rest. Removed rather than marked obsolete, which #7982 asked to be an explicit decision. Every string these classes held carries two colons, so it does not parse under the new grammar and authorizes nothing. Keeping them obsolete would leave code that compiles, still reads as a permission check, and silently grants no access -- a warning that is easy to suppress in front of a runtime failure that is invisible. A compile error names the call site and can be fixed against the migration guide's mapping table. Classes their own modules still reference, WorkflowPermissions and IdentityPermissions among them, are untouched. External Authentication's parallel descriptor system is collapsed onto the core types: its own PermissionDescriptor record, its IPermissionDescriptorProvider and IPermissionDescriptorRegistry, and DefaultPermissionDescriptorRegistry. That was not only tidiness. The module's registry was fed exclusively by its legacy names, so after the cutover every well-formed grant failed the `unknown_permission_descriptor` check and the warning fired constantly for correct configuration. The resolver now consults the core catalog, which is keyed by resource and lists the verbs each accepts, and a wildcard is treated as advertised because it names a pattern rather than a resource to look up. The descriptor endpoint serves the core catalog too: choosing what an external mapping may confer means choosing from everything Elsa declares. The module contributes its resource descriptors explicitly rather than relying on the host's assembly scan, for the same reason it registers AddElsaAuthorization itself. The two naming tests now pin the new resource name instead of the legacy string. The convention worth holding was always that the module is called 'diagnostics/console-logs', not that a retired constant kept its old value. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(client): match the permission descriptor client model to the catalog Moving the descriptor endpoint onto the core catalog changed its shape from a single permission string to a resource plus the verbs that resource accepts, and the Refit client model kept the old one. It still deserialized and still compiled, handing callers a blank Name and no way to reach the verbs -- the data went missing without anything failing. The client model now mirrors the served descriptor, and a contract test compares the two property sets so the next divergence is a test failure rather than an empty field. NonCoreVerbs is excluded: the server derives it from SupportedVerbs, so a client holding the verbs can compute it. Found by review, not by the suites: nothing here throws. Refs #7982 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Implements the design in #7978 / spec 013, end to end. Tracking issue: #7974.
BREAKING: legacy permission strings no longer authorize. See the migration guide â read it before upgrading, it leads with the three things that are not a simple rename.
What changed
A permission is now
{resource}:{verb}â a hierarchical resource path paired with a verb, both axes open and contributed by modules. A trailing*matches the named node and every descendant, soworkflows/*:viewis one grant covering every resource beneathworkflows/, including ones added later.*:*is superuser; a bare*normalizes to it at parse time, which is what keeps a seeded administrator working across the migration.Rationale for both axes being open, and why a closed verb enumeration was drafted and rejected, is in ADR 0012.
By phase
1 â Model and evaluator.
Permission,CoreVerbs,PermissionMatcher,IPermissionEvaluator, the authorization handler, and the descriptor catalog promoted intoElsa.Api.Common.2 â Catalog coverage. 47 resources across 15 modules. Descriptors are discovered from the same assemblies as a module's endpoints, so the catalog necessarily describes the endpoints that exist rather than drifting from them. Adds
GET /identity/permissions,/permissions/reachand/me/permissions, plus grant validation on the role write paths â which previously persistedrequest.Permissionsafter only the caller-subset check.3 â Cutover. All 168 declaration call sites across 151 files now use
RequirePermission(resource, verb)with their module's constants, so a typo is a compile error. Enforcement consolidated onto the evaluator. Adds the fail-closed gate: every endpoint must declare a permission,AllowAnonymous, orRequireAuthenticatedOnly, with no exemption list.4 â Revocation and audit. Access-token lifetime 1h â 15 min, which is the revocation bound. An optional permission stamp is derived from the user's roles rather than stored, so it needs no schema change and no cross-node cache invalidation â which matters, because Elsa has none. Role changes publish typed notifications per ADR 0007.
5 â Tenancy hardening. Per-tenant composite unique indexes with migrations for all five providers; two tenants could not previously both hold a role named
Admin. The in-memory user and role stores now isolate â isolation previously existed only on the EF path, and only when multitenancy was enabled.6 â Documentation. Migration guide, ADR 0012, and the security wiki's authorization section.
Behavior changes worth reviewing
read:*andexec:*become materially more powerful as*:viewand*:execute. Today they authorize twelve of roughly forty read endpoints. Any role holding them needs review by hand.workflows/runtime:viewalone, which is least privilege.ContinueLogoutis now anonymous, fixing a latent 401 on the upstream-logout callback (ContinueLogout requires authentication, which can 401 the upstream logout callback #7976, also fixed standalone in fix(external-auth): make the upstream logout continuation reachable #7979 â expect a trivial conflict depending on merge order).Verification
dotnet build Elsa.slnâ 0 errors. Every test project in the solution passes.Run against real servers
Both host models were started and exercised, which found three bugs the suites could not:
RoleSecurityNotifierand the stamp services were registered only on the shell path, soElsa.Server.Webcrashed at startup while mapping endpoints. Unit tests construct services directly and never exercise either feature system.Elsa.ModularServer.Webthe catalog came up empty. Authorization still worked â the evaluator reads claims, not descriptors â which is exactly why nothing failed loudly. Role authoring, introspection and the stored-permission validator were all silently blind.AddElsaAuthorizationgot 403 everywhere with nothing to explain it. Now evaluated inline against a stateless evaluator, with a host-registered one still winning.On the modular host, which installs far more modules:
workflows/*â 20,external-authentication/*â 8,*â 47**:*, resolved to concrete verbs on all 47dashboard:view+workflows/*:viewinvented/resource:view+secrets:publishOn
Elsa.Server.Web, a role holding onlydashboard:viewgot 200 on the dashboard and 403 on roles, users, workflow-definitions and the catalog, while/identity/me/permissionsreturned 200 â confirming FR-019's third declaration state. That response listed all 27 registered resources with 26 carrying empty verb lists, so "denied" stays distinguishable from "unknown".The coverage gate runs over Workflows.Api and Identity; extending it to another module is one test calling
EndpointCoverage.AssertEveryEndpointDeclaresAccess.Not done here
Elsa.ExternalAuthentication's own legacy descriptor types â it keeps them alongside the core catalog; harmless, but worth a follow-up.