Commit graph

7346 commits

Author SHA1 Message Date
Sipke Schoorstra 3bee566657
feat(bpmn): give BPMN interchange refusals stable error codes (#8067)
* feat(bpmn): give BPMN import/export/document refusals stable error codes

Studio has to recognise a BPMN import/export refusal, and extract capability
names and element ids, by matching the server's message text, so any rewording
silently degrades it to a generic error. Add BpmnErrorCodes with a stable code
per refusal (capability-unsupported, binding-invalid, export not-imported/
source-stale/source-version-unknown, and the document PUT's not-found and
precondition codes), sent through an additive BpmnErrorResponse envelope that
keeps today's statusCode/message/errors shape unchanged and adds code/data
alongside it, since FastEndpoints' own error response has no way to surface a
ValidationFailure's error code in this deployment's configuration. Rename the
Import/Export exception cascades to *ErrorResponses to say what they now do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(bpmn): fold BpmnErrorResponseFactory and BpmnErrorResponseSender into BpmnErrorResponse

Both were thin, single-purpose wrappers around BpmnErrorResponse used only by
the import/export error mapping and one endpoint — a Middle Man chain. Create
and SendAsync now live as static members on BpmnErrorResponse itself; call
sites are unchanged otherwise, and the wire output is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 00:10:12 -07:00
Sipke Schoorstra e54ced5662
feat(bpmn): declare BpmnProcess's Done and Cancelled outcomes as flow ports (#8066)
Elsa.Bpmn.Activities.BpmnProcess completed with the interpreter's Done or
Cancelled outcome but declared no outcomes, so a Flowchart composing it only
saw Studio's synthesized default port and the Cancelled outcome was
unreachable. Declares both via [FlowNode(BpmnInterpreter.DoneOutcomeName,
BpmnInterpreter.CancelledOutcomeName)] (both const in Bpmn.Semantics 0.2.0),
adds a descriptor test asserting the two flow ports, and a composition test
routing a cancelled transaction down the Cancelled port.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:16:21 -07:00
Sipke Schoorstra d0dd7c9ef5
fix(bpmn): detect designer-edited drafts as stale BPMN source (#8065)
An unpublished draft is saved in place, so a designer save that edits a
bound activity's inputs changes StringData without changing Version,
which the export/document staleness check missed. Import now also
records a content hash of the graph (Bpmn:SourceGraphHash) and
ResolveSourceXml refuses as stale when it no longer matches, falling
back to the version-only check for definitions imported before this
marker existed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:46:58 -07:00
Sipke Schoorstra 22c1aea472
fix(bpmn): the document PUT preserves the definition's non-BPMN metadata (#8063)
* fix(bpmn): preserve definition metadata when the document PUT rebinds it

The document PUT edits a definition's BPMN document, not the whole definition, so it must not
reset the author's name, description, variables, options and custom properties the way a
whole-definition import intentionally does. BpmnInterchangeDocumentService.ImportAsync now accepts
the existing definition to preserve that metadata from, which ImportDocumentAsync passes; POST
bpmn/import stays a whole-definition import.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): preserve IsReadonly and restore ImportAsync's public signature

Review findings on the prior metadata-preservation fix: IsReadonly was still
overwritten by the document PUT because it was missing from the preserved
field set, and the public ImportAsync(string, string?, string?, string?,
CancellationToken) signature had been changed in place by appending an
optional preserveMetadataFrom parameter. Restore that signature exactly and
move the shared import logic into a private ImportCoreAsync(..., preserveMetadataFrom,
CancellationToken), called by both the public ImportAsync and
ImportDocumentAsync, with IsReadonly now carried alongside the other
non-BPMN metadata. Extend the endpoint tests to also set and assert inputs,
outputs, outcomes, tool version and IsReadonly on both the preserve and
replace paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): refuse document PUT when the target definition has vanished

ImportDocumentAsync fell back to the whole-definition import path when
its definition lookup returned null, silently creating a definition
under the requested id with reset metadata instead of reporting that
the PUT's target disappeared (e.g. deleted between the endpoint's
existence/ETag check and this lookup). It now throws
BpmnDefinitionNotFoundException, which the shared exception cascade
maps to 404 for the document PUT endpoint; POST bpmn/import is
unaffected since it never passes a preservation source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:15:47 -07:00
Sipke Schoorstra e28e35635b
feat(bpmn): GET and PUT the BPMN document as library-format JSON (#8060)
* feat(bpmn): add document GET/PUT endpoints for BPMN JSON round-tripping (W21)

Studio holds a BPMN process as the library's own JSON payload, not as .bpmn
XML, so binding a task (W11) or moving a shape (W14) had no write path back
to the server: bpmn/import only takes XML, and saving the activity JSON
through the ordinary definition save leaves Bpmn:SourceXml stale, so export
then refuses with 422.

Adds GET/PUT bpmn/definitions/{definitionId}/document, sharing the same
analyze-then-commit path Import runs (BpmnInterchangeDocumentService.ReadDocument/
ImportDocumentAsync), so a document read by GET and written back unchanged by
PUT can never disagree with what Import or Export would do with the same
bytes. Records which process a document was imported from
(Bpmn:SourceProcessId) so a multi-process document keeps importing the same
process on every edit. Binds and writes the document body with plain
System.Text.Json defaults, not FastEndpoints' configured serializer, since
Bpmn.Model's JSON payload format (integer enums, explicit property names)
disagrees with Elsa's own API-wide JSON conventions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(bpmn): deduplicate document endpoint response and exception handling

Import and the document Put endpoint shared a byte-identical Response type and
an identical exception-to-status-code cascade; Export and the document Get
endpoint shared an identical cascade too. Extract both into a shared
BpmnImportResponse and two small cascade helpers under Endpoints/Bpmn, used by
all four endpoints with unchanged status codes and error messages. Also add
endpoint coverage for PUT against a definition imported before
Bpmn:SourceProcessId existed, for both the single- and two-process cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): add optimistic concurrency to the document endpoints and restore Import.Response

GET bpmn/definitions/{id}/document now returns a strong ETag derived from the
definition's Version, its Bpmn:SourceVersion custom property, and a new
Bpmn:DocumentRevision counter (needed because an unpublished draft is edited
in place, so Version/SourceVersion alone do not always change on save). PUT
requires a matching If-Match: missing returns 428, stale returns 412 (checked
before any import work), and a successful PUT returns a new, different ETag.

Also restores the public Elsa.Bpmn.Interchange.Endpoints.Bpmn.Import.Response
type the prior dedupe commit renamed to BpmnImportResponse without cause,
which would have broken source and binary consumers of the preview package;
Import and the document Put endpoint now both use Import.Response again.

Also replaces a foreach-with-continue over a JSON array with .Where(...) in
the document Put test helper, per CodeQL, with no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): derive the document ETag from the stored document and graph

The document ETag combined the definition's Version, Bpmn:SourceVersion and a
Bpmn:DocumentRevision counter only the document PUT incremented. An unpublished
draft is edited in place under the same version, and both the workflow
definition importer and the designer's save replace CustomProperties
wholesale, so a POST bpmn/import into the same definition or a designer save
of the draft left all three unchanged. A client holding the pre-write ETag
could then PUT and silently overwrite that write.

The ETag is now a SHA-256 hash over the stored definition's id, version, BPMN
source (Bpmn:SourceXml) and activity graph (StringData), computed in one place
for GET and for PUT's precondition check. Every writer changes at least one
input: a document PUT or an import rewrites the source, a designer save
rewrites the graph, a new draft version changes the id and version. Identical
stored content now yields an identical ETag, so a PUT of unchanged content
returns the ETag GET did.

Bpmn:DocumentRevision and the PUT's pre-import read of it are gone. The ETag a
PUT returns is computed from the definition its import persisted. If-Match
must equal the current strong ETag exactly (weak tags and lists never match,
412), and the wildcard "*" is refused along with a missing header (428),
since it matches whatever is stored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:16:32 -07:00
Sipke Schoorstra 952dfa05ff
feat(bpmn): project interpreter diagnostics onto the scope's execution log (#8058)
* feat(bpmn): project interpreter diagnostics onto the scope's execution log

Under Option A only bound work carries an activity id, so gateways, events and
flows had no per-element trace in the journal. BpmnScopeHost now projects each
new BpmnExecutionState.Diagnostics entry onto the scope's own execution log
before Prune() runs, keyed by element id, with a persisted high-water mark so
a resumed scope never re-emits one. The scope's own start and completion stay
out, since they are already journaled as the activity's own lifecycle. Event
names and the payload shape are documented as a public compatibility surface
for elsa-studio#1000 to mirror.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): project element and flow diagnostics dropped by the FlowId exclusion

The diagnostics exclusion keyed on Kind == TokenEmitted && FlowId is null/empty
also dropped an error or cancel boundary's own token emission, since a
boundary fires without an inbound flow. Narrow the rule to skip only
diagnostics that name neither an element nor a flow -- the scope's own
terminal Completed summary -- so every diagnostic keyed on an element or a
flow, including a start event's and a boundary's, is projected.

Also make DiagnosticSequence resilient: TryParse instead of Parse, logging a
warning and skipping projection for an id that doesn't match diag:N rather
than faulting the evaluation. Add a reflection-based test that keeps
BpmnDiagnosticEventNames in lockstep with BpmnDiagnosticKind, and record the
diagnostics volume measurement in the wiki.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): require exact diag:N ids and seed the diagnostics cursor from prior state

Reject any diagnostic id that is not the exact "diag:" prefix followed by a
non-negative integer, so a malformed id can no longer poison the durable
projection cursor and cause later, genuinely valid, lower-sequence
diagnostics to be skipped forever.

Also seed a missing cursor from the highest valid sequence in the scope's
prior persisted state instead of treating it as zero, so a scope persisted
before diagnostics projection existed does not replay every retained
historical diagnostic as new on its next evaluation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 19:01:06 -07:00
Sipke Schoorstra 0157a83cd0
docs: refresh roadmap status 2026-09-09 12:49:24 +02:00
Shivam Kumar f50d6e984e
fix: preserve single latest draft when saving a new identity (#7918)
Co-authored-by: Shivamkmr8 <shivam.k@surya-fintech.com>
2026-09-08 01:03:30 +02:00
Sipke Schoorstra 83af7309f2
fix(identity): stop returning password hashes and salts from user creation (#8041)
The POST /identity/users response serialized the plain-text password
(including one the caller supplied), the password hash, and the salt.
The response now carries only id, name, roles, tenantId and a nullable
generatedPassword that is populated once, and only when Core generated
the password because none was supplied.

- CreateUserResult gains IsPasswordGenerated so the endpoint can tell a
  generated password from a supplied one without re-deriving it.
- Response.FromResult centralises the mapping and omits credential
  material.
- Expose Elsa.Identity internals to Elsa.Identity.UnitTests and add
  contract tests covering the response shape, the no-echo rule, the
  serialized JSON, and UserManager's generated-password flag.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 00:26:47 +02:00
Sipke Schoorstra faf9d57b8b
fix(efcore-oracle): migrate LOB columns in V3_6 without in-place datatype alteration (#8040)
* fix(efcore-oracle): migrate LOB columns in V3_6 without in-place datatype alteration

Both Oracle V3_6 migrations were generated as in-place `ALTER TABLE ... MODIFY`
statements that change a column's datatype to or from a LOB type: NCLOB to JSON
for `WorkflowDefinitions.StringData`, and NVARCHAR2(450) to NCLOB for
`ActivityNodeId` on `WorkflowExecutionLogRecords` and `ActivityExecutionRecords`.
Oracle refuses both (ORA-22858 / ORA-22859), so neither migration could ever
apply and the reported ORA-22858 was unavoidable.

Convert the columns the way the ORA-22858 message prescribes instead: add a
temporary column of the target type, copy the values across, drop the original
and rename the temporary one. Because Oracle commits DDL implicitly, a run that
fails partway leaves its earlier statements applied - the reporter's already
committed `OriginalSource` column is exactly that - so every step is guarded
against the state a previous attempt can have left behind. The conversion block
derives what still needs doing from `ALL_TAB_COLUMNS`, skips a conversion that
already completed (including one applied by hand), and raises rather than copy
out of and drop a column whose datatype it does not recognize.

Add an offline regression test that generates the Oracle Management and Runtime
V3_6 scripts through `IMigrator.GenerateScript` without a connection and asserts
that no in-place datatype `MODIFY` is emitted for either column, that the
add/copy/drop/rename sequence appears in order, and that the re-run guards are
present. All 19 cases fail against the previous migrations.

Refs #8011

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(efcore-oracle): tighten migration helper visibility and index guard, share the script-generation test harness

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(efcore-oracle): brace the foreach bodies in the V3_6 migration tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(efcore-oracle): refuse to truncate node IDs on downgrade and validate same-named indexes

The Runtime V3_6 downgrade converted ActivityNodeId from NCLOB back to
NVARCHAR2(450) by copying DBMS_LOB.SUBSTR(..., 450, 1), silently
truncating any value the upgraded schema had allowed to grow past 450
characters. EnsureLobLengthAtMost now checks for oversized values while
the column is still a LOB and raises before any data is copied.

CreateIndexIfMissing also treated any index with a matching name as
already done. It now validates the existing index's table, uniqueness
and ordered column list against ALL_INDEXES/ALL_IND_COLUMNS so a
same-named index left behind by schema drift or manual recovery is not
mistaken for the one the migration means to create.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(efcore-oracle): correct the dynamic SQL rationale on the LOB length guard

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(efcore-oracle): preflight both tables before downgrading and escape schema names in migration SQL

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(efcore-oracle): copy every row NULL-preservingly so retries reproduce the current source

The filtered copy `WHERE "<column>" IS NOT NULL` skipped rows whose
nullable source had since become NULL. If a prior conversion committed
the copy and then failed before dropping the source column, an
operator clearing a value before retrying would find the predicate
skip that row, and the stale converted value would be renamed into
place. Replace the filtered UPDATE with an unconditional, NULL-
preserving CASE expression so a retry always reproduces the current
source exactly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 02:09:08 -07:00
Sipke Schoorstra fda3987c0b
fix(efcore): terminate raw SQL statements in the PostgreSQL and SQLite Runtime V3_6 migrations (#8039)
* fix(efcore): terminate raw SQL statements in the PostgreSQL and SQLite Runtime V3_6 migrations

The V3_6 Runtime migration's two DROP INDEX statements were emitted without a
trailing semicolon on PostgreSQL and SQLite, so `dotnet ef migrations script`
produced syntactically invalid SQL (an unterminated statement inside the
idempotent DO $EF$ block, and unseparated statements in the plain script).
MigrateAsync() was unaffected because EF executes each Sql() call individually.

Add the missing terminators and a regression test that generates the Runtime
migration script offline for both providers (idempotent and plain forms) and
asserts the DROP INDEX statements are properly terminated.

Refs #7912

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(efcore): share migration-script setup and cover the schema-prefixed statements

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(efcore): cover schema-prefixed migration statements without static state

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:27:43 -07:00
Sipke Schoorstra 308f013ad1
test(workflows-api): pin that a new read-only workflow can be imported (#8038)
Add regression coverage for both the single and bulk import endpoints
confirming that importing a workflow definition with a fresh
DefinitionId and IsReadonly=true succeeds and persists as read-only.
NotReadOnlyPolicy is only meant to block edits of existing read-only
workflows; on main it already resolves against the stored definition
(null for a new one), so these tests pin that behavior against
regression.

Refs #7981

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 22:51:17 -07:00
Sipke Schoorstra 85e5fc083c
fix(external-authentication): scope role-deletion impact to the role's tenant (#8036)
* fix(external-authentication): scope role-deletion impact to the role's tenant

ExternalAuthenticationRoleDeletionDependencyContributor scanned every stored
connection with an empty ConnectionFilter and every configured connection
regardless of its tenant, so a role ID that exists in two tenants could report
another tenant's references as its own impact -- and a configuration entry
owned by another tenant could block a role deletion outright. Remediation had
the same reach: it loaded a dependency's connection by the caller-supplied
owner ID without checking which tenant owned it.

Impact, prevalidation and remediation now only see connections in the role's
tenant context, which is the tenant active on ITenantAccessor while the
role-deletion coordinator runs. Host-scoped connections stay in scope for every
tenant, because the connection registry resolves the host scope for every
signing-in tenant and the provisioner resolves a connection's default role IDs
in the signing-in user's tenant, so a host connection naming a role ID really
does reference that tenant's role. Configuration entries that leave the tenant
blank are host-scoped for the same reason the configuration source materializes
them there. A connection carrying another tenant's ID is out of scope in both
directions, and a connection loaded for remediation that is not in the role's
tenant is treated as absent, which fails the request rather than mutating it.

The stored connections are fetched per applicable scope so another tenant's
rows are never materialized, and both connection stores already honor
ConnectionFilter.Scope; the durable store now has a test pinning that, since
the tenant boundary rests on it.

Refs #8013

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): scan every tenant when deleting a tenant-agnostic role

Role stores expose tenant-agnostic roles (TenantId == "*") from every tenant, but the
role-deletion contributor derived its dependency scan boundary from the ambient tenant only,
so deleting an agnostic role while tenant A was active left references from other tenants
dangling. Resolve the role being deleted once per operation, through the active role store,
and scan every connection and configuration entry regardless of tenant when it is agnostic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(external-authentication): share the active role store lookup

Extract the duplicated "active role store is the last registration"
resolution into a single ActiveRoleStore accessor and rename ToScope to
ToConnectionScope for clarity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): read one connection snapshot and prefer the agnostic role

Reading the host and tenant scopes as two separate store queries let a connection
whose TenantId changed mid-flight fall between the reads and escape both, letting
role deletion proceed while a reference remained. FindConnectionsInRoleTenantScopeAsync
now reads one snapshot and filters it in memory. IsAgnosticRoleAsync resolved a role
by an unqualified ID lookup, which could return the ambient tenant's role instead of
an agnostic role sharing its ID, silently narrowing impact scanning and leaving
JIT-policy references in other tenants dangling; it now checks every role sharing the
ID and gives the agnostic scope deterministic precedence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(external-authentication): correct the scope-filter test comment

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): fail closed when a role ID resolves to more than one role

A same-ID collision between a tenant-scoped role and an agnostic role can only
occur in MemoryRoleStore (durable persistence keys roles by ID alone). In that
case the coordinator's own deletion target is already ambiguous, so widening
or narrowing the scope by guessing is wrong in either direction; throw instead
of picking a side.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): scope role-deletion impact by the resolved role's tenant

Replace the isAgnosticRole flag with ResolveRoleTenantIdAsync, which returns the
resolved role's own TenantId and falls back to the ambient tenant only when the
role cannot be resolved. With multitenancy disabled the EF role store installs
no tenant query filter and can resolve a tenant-owned role by ID regardless of
the ambient tenant, so scoping by the ambient tenant alone left that role's
connection references out of scan while the coordinator deleted it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): require an agnostic replacement when remediating an agnostic role

Authorization for a replacement role still resolves through the ambient tenant's
role services, so a deletion initiated in tenant A could authorize a tenant-A-only
replacement and then write it into tenant B's connection policy, where that role
does not exist. When the deletion target is agnostic, require the replacement
role to be agnostic too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): require agnostic replacements for host connections and reject ambiguous ones

Extend the agnostic-replacement requirement to host-scoped connections, since a host
connection is served to every signing-in tenant and a tenant-scoped replacement would
resolve in the authorizing tenant but fail to resolve in every other tenant it serves.
Recheck the replacement at removal time through the same agnostic-role resolution used
at validation, instead of trusting whichever same-ID role a plain FindAsync happens to
return, so a replacement collision introduced between validation and mutation is
rejected. Resolve IsAgnosticRoleAsync's candidate directly and return true only when
exactly one matching role is agnostic, so an ambiguous replacement ID is reported as
replacement_role_unavailable_or_unauthorized instead of escaping as an exception.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(external-authentication): keep host-connection replacements allowed for default-tenant roles

Revert the host-scope replacement guard added for host-scoped connections.
IdentityProviderConnectionManagementService forces every managed connection
to host scope, and in a deployment without multitenancy roles are created
scoped to the default tenant rather than agnostic, so requiring an agnostic
replacement for host-scoped connections would make every replacement
remediation impossible in the default deployment. The replacement guard
applies only when the deletion target itself is agnostic, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 22:34:44 -07:00
Rostislav Statko 54c8fda65f
fix(alterations): preserve tenant context for background alteration jobs (#7962)
* Fix tenant context propagation for alteration jobs

Capture the current tenant when dispatching a background alteration job
and restore it while executing the queued callback.

Add regression coverage verifying that the dispatch-time tenant is used
and the worker's previous tenant context is restored.

Fixes #7961

* test(alterations): cover tenant restore on failure and concurrent tenant isolation

Add coverage for the spec lines that were previously untested: the
worker's tenant context is restored after execution even when the
job runner throws, and concurrent jobs dispatched under different
tenants do not exchange tenant context. Extract the shared service
provider / job queue / recording runner arrange logic into
constructor-initialized fields so the three tests stay DRY.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(alterations): cover default-tenant dispatch and share dispatcher test setup

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 20:07:36 -07:00
Copilot 536cee3fc2
fix(identity): publish the role security notification after role deletion (#8026)
* Initial plan

* Publish security notification after role deletion

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Fix atomic role deletion notifications

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* refactor(identity): delegate MemoryRoleStore.DeleteAsync to TryDeleteAsync

Mirrors EFCoreRoleStore.DeleteAsync so the deletion logic exists once
instead of being duplicated between DeleteAsync and TryDeleteAsync.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(identity): scope the atomic role delete capability to a single role ID

Narrow IRoleStoreWithAtomicDelete.TryDeleteAsync to accept a single role
ID instead of a RoleFilter, closing a race where two concurrent deletes
matching multiple roles could each remove one and both report success.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.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: Sipke Schoorstra <sipkeschoorstra@outlook.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:44:36 -07:00
Sipke Schoorstra cbdc3f7e9d
Honor label filters when listing workflow definitions (#8035)
* Honor label filters when listing workflow definitions

* Authorize label lookups and require filter resolution
2026-09-05 22:13:16 -07:00
Sipke Schoorstra 5f692b956d
Merge pull request #8029 from elsa-workflows/codex/issue-8028-role-remediation-contract
Support selective role deletion remediation
2026-09-05 19:53:52 -07:00
Sipke Schoorstra e059400bf1
Merge pull request #8034 from elsa-workflows/codex/issue-8033-role-e2e-fixture
test(modular-host): add opt-in role management fixture
2026-09-05 19:45:32 -07:00
Sipke Schoorstra 77da232bba
fix(identity): select active role store for remediation 2026-09-06 04:32:01 +02:00
Sipke Schoorstra 5442e62112
test(modular-host): add opt-in role management fixture 2026-09-06 04:25:09 +02:00
Sipke Schoorstra f9d41ee85b
Merge remote-tracking branch 'origin/main' into codex/issue-8028-role-remediation-contract 2026-09-06 04:11:47 +02:00
Sipke Schoorstra 07f788afb8
fix(identity): isolate in-memory roles by tenant (#8032)
* fix(identity): isolate in-memory roles by tenant

* fix(identity): retain legacy default-tenant roles
2026-09-05 19:01:42 -07:00
Sipke Schoorstra 62fcc302c7
Guard optional Identity role services 2026-09-06 03:54:35 +02:00
Sipke Schoorstra 602ad38afa
fix: preserve Sequence ownership when retrying child activities (#8027)
* fix: preserve Sequence ownership when retrying child activities

* fix: allow FastEndpoints EmptyRequest in invitation wrapper
2026-09-05 18:42:28 -07:00
Sipke Schoorstra c1ea463e0e
Merge remote-tracking branch 'origin/main' into codex/issue-8028-role-remediation-contract 2026-09-06 03:20:25 +02:00
Sipke Schoorstra dab194b44e
Fix UserTasks net10 EmptyRequest compilation (#8031) 2026-09-05 18:20:07 -07:00
Sipke Schoorstra 4bfb0a1f14
Support selective role deletion remediation 2026-09-06 03:09:37 +02:00
Sipke Schoorstra e7def4f1e9
Merge remote-tracking branch 'origin/release/3.8.0'
# Conflicts:
#	.github/workflows/packages.yml
#	.specify/feature.json
#	CONTEXT.md
#	Directory.Packages.props
#	doc/adr/toc.md
#	src/modules/Elsa.Workflows.Api/Endpoints/OutputConverters/List/Endpoint.cs
#	test/unit/Elsa.Workflows.Api.UnitTests/OutputConverters/OutputConverterEndpointTests.cs
2026-09-06 00:06:14 +02:00
Sipke Schoorstra be5bdae501
release: anchor recovery workflow validation (#8025) 2026-09-05 10:30:06 -07:00
Sipke Schoorstra b5ec54fc9b
Verify NuGet recovery without rebuilding release artifacts (#8024)
* release: support validated NuGet artifact recovery

* Address recovery validation review findings
2026-09-05 10:14:39 -07:00
Sipke Schoorstra 0b1186ee07
Document template browser release checks (#8023) 2026-09-05 08:58:18 -07:00
Sipke Schoorstra aeb982dcef
Extend release train with Elsa templates (#8022) 2026-09-05 07:44:02 -07:00
Sipke Schoorstra ce918c4971
Include website and documentation updates in Elsa releases (#8021)
* feat(release): add post-release site gates

* Align post-release content guidance and roadmap with Elsa 3.8
2026-09-05 06:47:06 -07:00
Sipke Schoorstra 633af5a77c
Make Elsa release skills self-contained and resumable (#8020) 2026-09-05 06:18:52 -07:00
Sipke Schoorstra 8191ae3055
Merge pull request #8019 from elsa-workflows/copilot/update-fastendpoints-version
Update FastEndpoints to 8.2 for .NET 10
2026-09-05 03:13:31 -07:00
copilot-swe-agent[bot] e7debff3f2
Address resume endpoint review feedback
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-09-04 22:37:05 +00:00
copilot-swe-agent[bot] 016ffe5d8d
Allow FastEndpoints EmptyRequest in Elsa endpoint wrappers
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-09-04 21:43:29 +00:00
copilot-swe-agent[bot] 750549c797
Use FastEndpoints empty-request endpoint base
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-09-04 21:41:56 +00:00
copilot-swe-agent[bot] 4efdf5fbff
Update FastEndpoints and empty resume request
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-09-04 21:40:45 +00:00
copilot-swe-agent[bot] 8804dda467
Initial plan 2026-09-04 21:39:25 +00:00
Sipke Schoorstra 53595b95d2
docs: refresh roadmap 2026-09-02 00:12:31 +02:00
Sipke Schoorstra 01db86ec21
Merge pull request #8010 from elsa-workflows/codex/state-machine-wf4-runtime
Align StateMachine runtime with WF4 lifecycle semantics
2026-08-31 05:10:25 +02:00
Sipke Schoorstra 2787ff6bd0
Preserve StateMachine composite transition continuations 2026-08-31 04:51:34 +02:00
Sipke Schoorstra da5ccca2be
Yield triggerless StateMachine cycles 2026-08-31 03:30:17 +02:00
Sipke Schoorstra 5a820ce33e
Expand StateMachine conformance coverage 2026-08-31 03:11:19 +02:00
Sipke Schoorstra 6a2e530d77
Enforce StateMachine trigger identity boundaries 2026-08-31 02:54:01 +02:00
Sipke Schoorstra 051e12f864
Fix StateMachine transition lifecycle ordering 2026-08-31 02:47:29 +02:00
Xu Jianxiang 484f7a0e5f
fix(core): clear stale blocked tokens when a flowchart activity completes (#7993) (#7994) 2026-08-31 00:59:05 +02:00
Sipke Schoorstra 7db6ff0e0c
Auto stash before merge of "main" and "origin/main" 2026-08-28 22:32:50 +02:00
Sipke Schoorstra 767c97aad5
refactor(identity)!: retire the SecurityRoot policy in favour of endpoint permissions (#8003)
* refactor(identity)!: retire the SecurityRoot policy in favour of endpoint permissions

Completes T040. ADR 0010 already decided SecurityRoot was overloaded and that
endpoints should be authorized by their own permissions; this removes the last
of it.

Roles/Create and Applications/Create carried Policies(SecurityRoot) alongside
an existing RequirePermission, so the policy was redundant there and the line
is simply dropped.

Secrets/Hash carried only the policy. By default SecurityRoot resolved to
RequireAuthenticatedUser(), so any signed-in caller could exercise the password
hasher. It now declares identity/users:create, on the grounds that hashing a
secret is a step in provisioning a credential. This is a tightening: callers
who could hash before and hold no user-creation permission will now be refused.

The policy, its two registration paths and the IdentityPolicyNames constant are
removed. ConfigureAuthorizationOptions stays public and now defaults to a no-op
so hosts that add their own policies are unaffected.

BREAKING CHANGE: the SecurityRoot authorization policy and the
IdentityPolicyNames class are removed. Hosts referencing either should rely on
endpoint permissions, and use DefaultAdminUserFeature for initial bootstrap.

Note: SecurityRoot was the only attachment point for LocalHostPermissionRequirement,
so the localhost permission grant is now inert. The requirement type and the
EnableLocalHostPermissionGrantForSecurityRoot toggles are left in place rather
than deleted, but they no longer gate anything -- see the PR for why that path
was already incoherent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(identity)!: delete the localhost bootstrap grant and its machinery

Follows the SecurityRoot removal in the previous commit. SecurityRoot was the
only attachment point for LocalHostPermissionRequirement, so the localhost
permission grant is now removed outright rather than left inert:
LocalHostPermissionRequirement, LocalHostRequirement (already dead -- registered
as a handler but consumed by no policy), LocalHostPermissionRequirementOptions
and the two feature toggles all go.

The grant was the weakest of the three bootstrap mechanisms Elsa already has. It
trusted network position, which stops meaning anything behind a reverse proxy,
inside a container, or across a port-forward; it granted unauthenticated access,
so the bootstrap action carried no identity; it covered only localhost, so it
did nothing for a deployed environment; and it could not perform its headline
job, because it granted identity/users:create while POST /identity/users does
not carry the policy that injected it.

The replacements already exist and both work in deployed environments:
UseDefaultAdmin(...) seeds an admin role and user at startup, idempotently, and
UseAdminApiKey(...) accepts an out-of-band key. What the localhost grant did
usefully provide was a hint that something needed configuring, so
IdentityBootstrapDiagnostic replaces that: when the user store is empty and
neither mechanism is configured, startup logs an error naming both, instead of
every endpoint answering 403 with no explanation.

BREAKING CHANGE: LocalHostRequirement, LocalHostPermissionRequirement,
LocalHostPermissionRequirementOptions and the
Enable/DisableLocalHostPermissionGrantForSecurityRoot toggles are removed. Use
UseDefaultAdmin or UseAdminApiKey to bootstrap an instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(identity): scope the hash endpoint's documentation to users, and pin the declarations

The hash endpoint's remarks said the callers that need it are "the ones standing up
users and applications", while the endpoint requires identity/users:create alone. An
application provisioner reading that would have been sent into a 403.

The documentation was the part that was wrong. `POST /identity/applications` generates
and hashes the client secret and the API key itself and returns both the plaintext and
the hash, so identity/applications:create is already sufficient to create an application
and the hash endpoint is not on that path at all. Say so, in the endpoint and in the
migration guide, rather than widening a grant nobody needs.

Adds EndpointPermissionTests over the three endpoints that carried the retired
SecurityRoot policy: the two that only lost a redundant policy line must keep the
permission they already declared, and Secrets/Hash must keep the one it gained. The
coverage gate only asks whether an endpoint declares something, so either half could
otherwise change unnoticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(identity): state the hash endpoint's user-only scope in the summary, and complete the removal list

Moves the user-only scoping into the endpoint's <summary>, which is the part that reaches
the generated API description, rather than leaving it to a paragraph further down. The
remark now says outright that no application-provisioning flow reaches this endpoint and
none is documented to, with the reason: POST /identity/applications generates the client
secret and the API key itself, hashes both, and returns each plaintext alongside its hash.

The migration guide's removal list was partial — it named the requirements and the two
toggles but not the handlers, the options type, the EnableLocalHostPermissionGrant property
on either feature, or the already-obsolete DisableLocalHostRequirement() alias. A reader
hitting a compile error on any of those would not have found it in the guide. It also now
records that ConfigureAuthorizationOptions survives as a no-op default.

Adds the store-failure case to IdentityBootstrapDiagnosticTests: the broad catch is
load-bearing — an unmigrated database must not stop the host from starting — and nothing
was holding it in place. Disposes the test service provider, and folds the repeated arrange
blocks in DefaultAuthenticationFeatureTests into fields.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 22:28:29 +02:00