elsa-core/doc/adr/graph.dot

60 lines
4.6 KiB
Plaintext
Raw Normal View History

Refactor Fault Propagation and Simplify Bookmark Management (#6545) * Refactor bookmark management and add new features Streamlined bookmark handling by eliminating temporary storage in `ActivityExecutionContext` and directly managing bookmarks in `WorkflowExecutionContext`. Documented architectural decisions using ADRs. * Regenerate EF Core migrations * Refactor fault tracking to use AggregatedFaultCount property. Replaces FaultCount with AggregatedFaultCount across the codebase to improve clarity and consistency in fault tracking. Updates related methods, properties, and data mappings to align with the new terminology. Fixes initialization issue with nullable inputs in Fault class. * Add migration to track fault counts in runtime (V3.5) This migration adds a new column, "AggregatedFaultCount," to the "ActivityExecutionRecords" table. The column is an integer, non-nullable, with a default value of 0, and enables tracking aggregated fault occurrences. The migration also includes a rollback to remove this column if needed. * Fix typo in ADR 0004 regarding bookmark management convention Corrected a spelling mistake in the ADR documentation by changing "determins" to "determines." This ensures clarity and maintains the professionalism of the document. No functional changes were made. * Refine fault propagation logic for child-parent activities Replace automatic fault transitions of parent activities with an aggregate fault count for descendant activities. This avoids premature state changes while still indicating child activity faults, improving workflow resilience and accuracy. * Remove signal-driven fault propagation ADR and renumber bookmarks ADR The ADR for signal-driven fault propagation was deleted, and the direct bookmark management ADR was renamed and renumbered accordingly. Related references in the table of contents, graph, and solution file were updated to reflect these changes. * Refactor DeleteBookmarks to improve readability. Reformatted the BookmarkFilter initialization for better clarity and maintainability. This change ensures the code is more aligned with modern C# conventions and improves overall readability. No behavior or functionality has been altered. * Refactor naming for "AggregatedFaultCount" to "AggregateFaultCount" Standardized the terminology across the codebase and migrations by renaming all references of "AggregatedFaultCount" to "AggregateFaultCount" for improved consistency and readability. Updated relevant logic, models, migrations, and database contexts accordingly.
2025-04-10 13:01:49 +00:00
digraph {
Implement null TenantId for tenant-agnostic entities (#7226) * Add ADR for Null Tenant ID, implement tenant-agnostic logic Introduce ADR-0009 to document the use of `null` for tenant-agnostic entities, enhancing multitenancy handling. Update multitenancy features across the codebase, including EF Core query filters and ActivityRegistry, to handle null as a tenant ID, ensuring tenant-agnostic entities are accessible across all tenants. * Add multitenancy support in `ActivityTestFixture` by registering `ITenantAccessor`. * Update src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unused `using Elsa.Common.Multitenancy;` from WorkflowDefinitionActivityDescriptorFactory (#7230) * Initial plan * Remove unused using Elsa.Common.Multitenancy statement Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression (#7227) * Initial plan * Optimize Find(string type) to prefer tenant-specific descriptors with single-pass iteration Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Apply review feedback: combine if statements and add comprehensive unit tests Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Refactor tests for DRYness using theories and helper methods Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Clean up extra whitespace in test file Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Fix default tenant data visibility leak by removing NullIfEmpty conversion (#7229) * Initial plan * Remove NullIfEmpty conversion to align with ADR-0008 and ADR-0009 - Updated ElsaDbContextBase to use empty string for default tenant - Updated ApplyTenantId to stop converting empty string to null - Updated TenantAwareDbContextFactory to preserve empty string for default tenant - Updated Store.cs to preserve empty string for default tenant - This ensures: null = tenant-agnostic (visible to all), "" = default tenant Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add database migration to convert null TenantId to empty string for SqlServer - Added Management migration to convert null to "" for WorkflowDefinitions and WorkflowInstances - Added Runtime migration to convert null to "" for all runtime entities - This ensures existing default tenant data is properly migrated per ADR-0008 - Note: Similar migrations needed for PostgreSql, MySql, Sqlite, and Oracle providers Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Clarify tenant handling logic in `ElsaDbContextBase` with new ADR references. * Add tenant ID convention analysis documents and quick reference guide * Implement tenant-agnostic functionality across modules - Introduce `AgnosticTenantId` constant to manage tenant-agnostic entities. - Modify entity handling logic to respect tenant-agnostic designations. - Adjust workflow processing to include tenant-agnostic workflows. - Update caching and activity descriptor logic to accommodate the `AgnosticTenantId`. * Refactor tenant management and registry logic in `ActivityRegistry` for improved clarity and separation of tenant-specific and tenant-agnostic activity descriptors. Remove `TestTenantResolver` and update workflow definition handling for tenant support. * Refactor `ActivityRegistry`: prioritize tenant-specific descriptors over tenant-agnostic and simplify descriptor retrieval logic. * Improve async handling in `CommandHandlerInvokerMiddleware` to await tasks without blocking * Update ADR to use asterisk as sentinel value for tenant-agnostic entities Replace the previous convention of using `null` for tenant-agnostic entities with an asterisk (`"*"`) for improved clarity and system architecture. Updated ADR documentation, TOC, and dependency graph accordingly. * Remove migration `ConvertNullTenantIdToEmptyString` and its associated designer file to clean up the codebase. * Refactor `ActivityRegistry`: streamline activity descriptor removal logic and simplify tenant ID checks. * Update Elsa.sln Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `RefreshDescriptorsAsync` by removing unnecessary local variable `currentTenantId`. * Remove unused `currentTenantId` variable from `ActivityRegistry`. * Add detailed semantic flow and key points to ADR 0009 Document the tenant ID flow from entity creation to query, emphasizing normalization and tenant-agnostic workflows. Update semantic flow diagrams and provide testing considerations for preserving `"*"` values in multi-tenant scenarios. * Remove outdated Tenant ID Analysis and associated documents * Add security-by-default design for tenant-agnostic entities in ADR Enhance Architecture Decision Record to detail explicit requirements for tenant-agnostic database entities, highlighting differences between in-memory activity descriptors and persistent entities. Emphasize importance of setting `TenantId = "*"` to prevent accidental data leakage. * Normalize tenant ID grouping in `ActivityRegistry` to unify null and agnostic IDs, reducing redundant processing. * Refactor `SignalManager`: improve timeout handling and streamline signal task cancellation. * Update src/modules/Elsa.Workflows.Core/Models/TenantRegistryData.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor tests to use `Tenant.AgnosticTenantId` instead of `null` for tenant-agnostic descriptors. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Enhance logging in recurring tasks: add error handling and logger support to prevent crashes in scheduled timers. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-02-02 09:59:01 +00:00
node [shape=plaintext];
subgraph {
_1 [label="1. Record architecture decisions"; URL="0001-record-architecture-decisions.html"];
_2 [label="2. Fault Propagation from Child to Parent Activities"; URL="0002-fault-propagation-from-child-to-parent-activities.html"];
_1 -> _2 [style="dotted", weight=1];
_3 [label="3. Direct Bookmark Management in WorkflowExecutionContext"; URL="0003-direct-bookmark-management-in-workflowexecutioncontext.html"];
_2 -> _3 [style="dotted", weight=1];
_4 [label="4. Activity Execution Snapshots"; URL="0004-activity-execution-snapshots.html"];
_3 -> _4 [style="dotted", weight=1];
_5 [label="5. Token-Centric Flowchart Execution Model"; URL="0005-token-centric-flowchart-execution-model.html"];
_4 -> _5 [style="dotted", weight=1];
_6 [label="6. Tenant Deleted Event"; URL="0006-tenant-deleted-event.html"];
_5 -> _6 [style="dotted", weight=1];
_7 [label="7. Adoption of Explicit Merge Modes for Flowchart Joins"; URL="0007-adoption-of-explicit-merge-modes-for-flowchart-joins.html"];
_6 -> _7 [style="dotted", weight=1];
_8 [label="8. Empty String as Default Tenant ID"; URL="0008-empty-string-as-default-tenant-id.html"];
_7 -> _8 [style="dotted", weight=1];
_9 [label="9. Asterisk Sentinel Value for Tenant-Agnostic Entities"; URL="0009-asterisk-sentinel-value-for-tenant-agnostic-entities.html"];
_8 -> _9 [style="dotted", weight=1];
feat: extend shells integration and modular server support (#7399) * refactor(deps): use local CShells project refs Replace CShells NuGet package references with direct project references to the local CShells source to enable developing and testing against local changes and simplify build integration across modules. * Handle assembly load errors in feature discovery Added error handling for assembly load failures in feature discovery to improve resilience. Also updated configuration for identity token options and removed unused service bus consumer dependencies. Simplified project structure by moving and cleaning up `Directory.Build.targets` files. * Refactor configuration and service extension methods. Moved `ShellSettingsExtensions` and `ShellConfiguration` to `CShells.Abstractions` for better modularity. Added new `ServiceCollectionFeatureExtensions` to improve options registration. Updated appsettings and references to support these changes. * Introduce ManagementServiceCollectionExtensions to streamline activity and variable registration Added `ManagementServiceCollectionExtensions` for registering Elsa activity types and variable descriptors, providing a modular and shell-feature-compatible approach to configuration. Updated relevant features to utilize these new extension methods, enhancing code modularity and reducing redundancy. * Add resilience strategy registration to HTTP feature Introduced `ResilienceServiceCollectionExtensions` to register resilience strategies within the `Elsa.Resilience.Core` module. Updated `HttpFeature` to incorporate resilience strategies, enhancing HTTP-related resilience configuration leveraging the new extension methods. * Add new configuration options to JavaScriptFeature Implemented multiple properties in `JavaScriptFeature` to enhance JavaScript execution: `AllowClrAccess`, `AllowConfigurationAccess`, `ScriptCacheTimeout`, `DisableWrappers`, and `DisableVariableCopying`. These additions enable more flexible and secure configuration of the Jint JavaScript engine. * refactor(workflows): unify graph caching Resolve workflow definitions first and store graphs under stable per-version-ID cache keys so different lookup paths share entries. Centralize cache creation and change-token registration to remove duplicated caching logic. Skip materializer-unavailable definitions to avoid caching null graphs and simplify flow. * refactor(tests): centralize default IDs and materializer setup Introduce constants for default definition and version IDs, and materializer name. Refactor tests to use these constants, streamline graph and definition resolution, and improve cache key creation by sharing logic across tests. Extend tests to check scenarios with unavailable materializers, ensuring caching only occurs for valid cases. * extend(tests): enhance cache key verification in AutoUpdateTests Added checks for both workflow definition and version cache keys in AutoUpdateTests to ensure comprehensive cache validation, improving test reliability and coverage. * refactor(projects): update CShells project paths and solution configuration Revised project reference paths in `Elsa.ModularServer.Web.csproj` for CShells projects and updated `Elsa.sln` to include new CShells projects, streamlining project organization and build configuration. * Add `IWorkflowReferenceGraphBuilder` to `WorkflowManagementFeature`; rename `ResilienceShellFeature` to `ResilienceFeature`. * Refactor `HttpFeature` to use `IMiddlewareShellFeature`, include `HttpWorkflowsMiddleware`, and update `HttpActivityOptions` defaults. * Add `AddTypeAlias` and `AddVariableTypeAndAlias` extension methods to service collections - Introduced `AddTypeAlias<T>` method in `ServiceCollectionExtensions.cs` for adding type aliases. - Added `AddVariableTypeAndAlias<T>` method in `ManagementServiceCollectionExtensions.cs` to add variable types with aliases. * Remove shell reload API endpoints, orchestrator, and associated tests from the codebase. * Introduce `DefaultAdminUser` options and refactor `AdminUserInitializer` to use `IOptions`. * Add user management endpoints: Delete, List, Update with enhanced user store functionality * Implement `DefaultAdminUser` feature for initial admin bootstrap, decouple `SecurityRoot` from user management endpoints, update related documentation and permissions. * Add role management endpoints: Delete, List, and Update, including role data models and handle obsolete SecurityRoot policy. * Update CShells package references to version 0.0.12-preview.66 and refactor `TenantTaskManager` for improved task lifecycle management. * Replace project references with package references in csproj files and remove unused folders. * Integrate Nuplane features, add sample packages, and update dependency handling within ModularServer Web. * Improve `CShells` startup endpoint registration and resolver handling - Address duplicate endpoint registration by adding state-aware tracking and deduplication - Resolve `WebRoutingShellResolver` constructor ambiguity by switching to factory-based registration - Implement a startup-specific filter to prevent redundant endpoint remapping during `ShellsReloaded` - Update project to use project references for `CShells` and `Nuplane` components in csproj files. * Update logging configuration in appsettings for Development and Production - Change default log level to 'Warning' in Development settings - Adjust Microsoft.Hosting and Elsa.SamplePackage log levels to 'Information' - Remove Microsoft.EntityFrameworkCore log level entry from Production settings * Refactor assembly retrieval methods and update endpoint calls for consistency. * Add `SampleEndpointFeature` and enhance logging and service integration - Implement `SampleEndpointFeature` with a new endpoint for handling requests. - Log endpoint access and integrate `ISampleService` with method `DoSomething`. - Update logging configuration to include `CShells` and `Nuplane` log levels in Development settings. - Update `Elsa.SamplePackage` to version 1.0.1 and manage dependencies with project and assembly references. - Modify JSON configuration for `SampleEndpoint`. * Update package versions for `CShells` to 0.0.13 and `Nuplane` to 0.0.1-preview.15 in props file. * Refactor `DefaultAdminUserFeature` by renaming `ConfigureServices` to `Apply` and adjusting service registration method. * Replace project references with package references across multiple projects and remove obsolete cshells-related solution entries. * Remove `SampleCatalogEndpointExtensions.cs` and related endpoint mappings. * Improve `TenantTaskManager` by using `TryRemove` for state clean-up and clarify `SemaphoreSlim` disposal behavior. * Remove hardcoded default admin credentials and add warning for unconfigured AdminRoleName in admin user setup. * Address unresolved review comments: fix doc comments, security defaults, compilation issue, and restore reload response contracts Agent-Logs-Url: https://github.com/elsa-workflows/elsa-core/sessions/34eb1e13-833f-4b3c-9db6-2e9221d221b9 Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Refine reload endpoints: use specific exceptions, add error messages, rename ReloadedAt to Timestamp, remove unused model Agent-Logs-Url: https://github.com/elsa-workflows/elsa-core/sessions/34eb1e13-833f-4b3c-9db6-2e9221d221b9 Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Add `ExceptionExtensions` with `IsFatal` method and simplify exception handling in `TenantTaskManager`. Remove unused properties from `Directory.Build.props`. * Add unit tests for `TenantTaskManager` and fix potential state orphaning issue. * Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Fix logger dependency in `SampleEndpointFeature` constructor to use correct type. * Add unit tests for Elsa Shells API endpoints and update solution configuration. * Refactor ShellReload models: remove ShellReloadItemResult, update ShellReloadResponse properties. * Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-18 12:33:34 +00:00
_10 [label="10. Default Admin User Bootstrap for Initial Identity Access"; URL="0010-default-admin-user-bootstrap-for-initial-identity-access.html"];
_9 -> _10 [style="dotted", weight=1];
feat(auth)!: structured authorization model, phases 1-6 (#7980) * feat(auth): add the permission model and evaluator (Phase 1) 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> * feat(auth): contribute the permission catalog from every module (Phase 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> * feat(auth)!: cut every endpoint over to the permission model (Phase 3) 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> * feat(auth): revocation bound and role audit notifications (Phase 4) 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> * feat(auth): tenancy hardening for identity (Phase 5) 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> * docs: migration guide, ADR, and security wiki for the authorization model 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> * test(auth): restore the suites after the tenancy and evaluator changes 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> * fix(auth): register the identity services on the classic feature path 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> * fix(auth): discover permission descriptors on the shell host path 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> * wip: bpmn test vocabulary * fix(auth): make enforcement DI-independent and finish the hub cutover 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> * fix(auth): scope the permission-stamp cache to the tenant 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> * fix(docs): correct path in log message for authorization model migration link Aligns the log message path to the correct documentation directory, changing `docs` to `doc` to avoid confusion and incorrect linking during log output. * fix(auth): update permissions method to use new syntax * docs: consolidate docs/ into doc/ 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> * refactor(auth): simplify syntax in PermissionEvaluator and related classes 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. * ci(bounty): point the footer step at the file's real path The bounty workflow read docs/bounty-footer.md, the path the file had when the workflow was added in b421b00e1. 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> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:44:55 +00:00
_11 [label="11. Output Conversion Occurs Synchronously at the Binding Boundary"; URL="0011-output-conversion-at-binding-is-synchronous.html"];
_10 -> _11 [style="dotted", weight=1];
_12 [label="12. Output Converters Use Explicit Stable Identities"; URL="0012-output-converters-use-explicit-stable-identities.html"];
_11 -> _12 [style="dotted", weight=1];
_13 [label="13. Output Converter Discovery Is Server-Owned"; URL="0013-output-converter-discovery-is-server-owned.html"];
_12 -> _13 [style="dotted", weight=1];
_14 [label="14. Broker external sign-in through Elsa Server"; URL="0014-broker-external-sign-in-through-elsa-server.html"];
_13 -> _14 [style="dotted", weight=1];
_15 [label="15. Compose a scoped connection registry"; URL="0015-compose-a-scoped-connection-registry.html"];
_14 -> _15 [style="dotted", weight=1];
_16 [label="16. Extend authentication through deployed descriptor providers"; URL="0016-extend-authentication-through-deployed-descriptor-providers.html"];
_15 -> _16 [style="dotted", weight=1];
_17 [label="17. Separate external identity from Elsa authorization"; URL="0017-separate-external-identity-from-elsa-authorization.html"];
_16 -> _17 [style="dotted", weight=1];
_18 [label="18. Separate provider trust from broker invariants"; URL="0018-separate-provider-trust-from-broker-invariants.html"];
_17 -> _18 [style="dotted", weight=1];
_19 [label="19. Bind sessions to shared state and connection revisions"; URL="0019-bind-sessions-to-shared-state-and-connection-revisions.html"];
_18 -> _19 [style="dotted", weight=1];
_20 [label="20. Publish audit-ready security notifications"; URL="0020-publish-audit-ready-security-notifications.html"];
_19 -> _20 [style="dotted", weight=1];
_21 [label="21. Identify host connections by logical key and use explicit overrides"; URL="0021-identify-host-connections-by-logical-key-and-use-explicit-overrides.html"];
_20 -> _21 [style="dotted", weight=1];
_22 [label="22. Match unlinked identities with trusted user matchers"; URL="0022-match-unlinked-identities-with-trusted-user-matchers.html"];
_21 -> _22 [style="dotted", weight=1];
_23 [label="23. Separate authentication UI composition from security administration"; URL="0023-separate-authentication-ui-composition-from-security-administration.html"];
_22 -> _23 [style="dotted", weight=1];
_24 [label="24. Use exact OIDC discovery and deployment-derived callbacks"; URL="0024-use-exact-oidc-discovery-and-deployment-derived-callbacks.html"];
_23 -> _24 [style="dotted", weight=1];
_25 [label="25. Two-axis authorization model with open resources and open verbs"; URL="0025-two-axis-authorization-model.html"];
_24 -> _25 [style="dotted", weight=1];
Implement null TenantId for tenant-agnostic entities (#7226) * Add ADR for Null Tenant ID, implement tenant-agnostic logic Introduce ADR-0009 to document the use of `null` for tenant-agnostic entities, enhancing multitenancy handling. Update multitenancy features across the codebase, including EF Core query filters and ActivityRegistry, to handle null as a tenant ID, ensuring tenant-agnostic entities are accessible across all tenants. * Add multitenancy support in `ActivityTestFixture` by registering `ITenantAccessor`. * Update src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unused `using Elsa.Common.Multitenancy;` from WorkflowDefinitionActivityDescriptorFactory (#7230) * Initial plan * Remove unused using Elsa.Common.Multitenancy statement Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression (#7227) * Initial plan * Optimize Find(string type) to prefer tenant-specific descriptors with single-pass iteration Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Apply review feedback: combine if statements and add comprehensive unit tests Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Refactor tests for DRYness using theories and helper methods Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Clean up extra whitespace in test file Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Fix default tenant data visibility leak by removing NullIfEmpty conversion (#7229) * Initial plan * Remove NullIfEmpty conversion to align with ADR-0008 and ADR-0009 - Updated ElsaDbContextBase to use empty string for default tenant - Updated ApplyTenantId to stop converting empty string to null - Updated TenantAwareDbContextFactory to preserve empty string for default tenant - Updated Store.cs to preserve empty string for default tenant - This ensures: null = tenant-agnostic (visible to all), "" = default tenant Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add database migration to convert null TenantId to empty string for SqlServer - Added Management migration to convert null to "" for WorkflowDefinitions and WorkflowInstances - Added Runtime migration to convert null to "" for all runtime entities - This ensures existing default tenant data is properly migrated per ADR-0008 - Note: Similar migrations needed for PostgreSql, MySql, Sqlite, and Oracle providers Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Clarify tenant handling logic in `ElsaDbContextBase` with new ADR references. * Add tenant ID convention analysis documents and quick reference guide * Implement tenant-agnostic functionality across modules - Introduce `AgnosticTenantId` constant to manage tenant-agnostic entities. - Modify entity handling logic to respect tenant-agnostic designations. - Adjust workflow processing to include tenant-agnostic workflows. - Update caching and activity descriptor logic to accommodate the `AgnosticTenantId`. * Refactor tenant management and registry logic in `ActivityRegistry` for improved clarity and separation of tenant-specific and tenant-agnostic activity descriptors. Remove `TestTenantResolver` and update workflow definition handling for tenant support. * Refactor `ActivityRegistry`: prioritize tenant-specific descriptors over tenant-agnostic and simplify descriptor retrieval logic. * Improve async handling in `CommandHandlerInvokerMiddleware` to await tasks without blocking * Update ADR to use asterisk as sentinel value for tenant-agnostic entities Replace the previous convention of using `null` for tenant-agnostic entities with an asterisk (`"*"`) for improved clarity and system architecture. Updated ADR documentation, TOC, and dependency graph accordingly. * Remove migration `ConvertNullTenantIdToEmptyString` and its associated designer file to clean up the codebase. * Refactor `ActivityRegistry`: streamline activity descriptor removal logic and simplify tenant ID checks. * Update Elsa.sln Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `RefreshDescriptorsAsync` by removing unnecessary local variable `currentTenantId`. * Remove unused `currentTenantId` variable from `ActivityRegistry`. * Add detailed semantic flow and key points to ADR 0009 Document the tenant ID flow from entity creation to query, emphasizing normalization and tenant-agnostic workflows. Update semantic flow diagrams and provide testing considerations for preserving `"*"` values in multi-tenant scenarios. * Remove outdated Tenant ID Analysis and associated documents * Add security-by-default design for tenant-agnostic entities in ADR Enhance Architecture Decision Record to detail explicit requirements for tenant-agnostic database entities, highlighting differences between in-memory activity descriptors and persistent entities. Emphasize importance of setting `TenantId = "*"` to prevent accidental data leakage. * Normalize tenant ID grouping in `ActivityRegistry` to unify null and agnostic IDs, reducing redundant processing. * Refactor `SignalManager`: improve timeout handling and streamline signal task cancellation. * Update src/modules/Elsa.Workflows.Core/Models/TenantRegistryData.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor tests to use `Tenant.AgnosticTenantId` instead of `null` for tenant-agnostic descriptors. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Enhance logging in recurring tasks: add error handling and logger support to prevent crashes in scheduled timers. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-02-02 09:59:01 +00:00
}
feat(auth)!: structured authorization model, phases 1-6 (#7980) * feat(auth): add the permission model and evaluator (Phase 1) 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> * feat(auth): contribute the permission catalog from every module (Phase 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> * feat(auth)!: cut every endpoint over to the permission model (Phase 3) 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> * feat(auth): revocation bound and role audit notifications (Phase 4) 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> * feat(auth): tenancy hardening for identity (Phase 5) 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> * docs: migration guide, ADR, and security wiki for the authorization model 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> * test(auth): restore the suites after the tenancy and evaluator changes 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> * fix(auth): register the identity services on the classic feature path 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> * fix(auth): discover permission descriptors on the shell host path 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> * wip: bpmn test vocabulary * fix(auth): make enforcement DI-independent and finish the hub cutover 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> * fix(auth): scope the permission-stamp cache to the tenant 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> * fix(docs): correct path in log message for authorization model migration link Aligns the log message path to the correct documentation directory, changing `docs` to `doc` to avoid confusion and incorrect linking during log output. * fix(auth): update permissions method to use new syntax * docs: consolidate docs/ into doc/ 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> * refactor(auth): simplify syntax in PermissionEvaluator and related classes 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. * ci(bounty): point the footer step at the file's real path The bounty workflow read docs/bounty-footer.md, the path the file had when the workflow was added in b421b00e1. 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> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:44:55 +00:00
_21 -> _15 [label="Partially supersedes", weight=0];
_21 -> _19 [label="Partially supersedes", weight=0];
_22 -> _16 [label="Partially supersedes", weight=0];
_22 -> _17 [label="Partially supersedes", weight=0];
_24 -> _18 [label="Refines; partially supersedes", weight=0];
}