Commit graph

4 commits

Author SHA1 Message Date
Sipke Schoorstra f6d2d38536
test(user-tasks): add a persistence conformance suite with fault injection (#7986)
Runs one suite unchanged against every implementation of IUserTaskRepository,
IUserTaskGuestSessionIssuer, and IUserTaskInvitationOutbox, plus a fault-injection
suite driving the real DefaultUserTaskManager and DefaultUserTaskInvitationService
against a real store. Gated providers report as skipped with a reason rather than
passing vacuously; ConformanceCoverageTests fails when a provider that must run is
unreachable or its variable is set but empty.

The suite found three defects, fixed here:

- VNextUserTaskRepository supplied no index values for WorkflowDefinitionId,
  WorkflowInstanceId, ActivityInstanceId, CreatedAt, or CompletedAt, all declared by
  its own schema provider, so every write through the VNext provider threw.
- The same provider resolved invitation token hashes by scanning on Status alone,
  which matched no declared index, so anonymous invitation verification always threw.
- EFCoreUserTaskInvitationOutbox persisted the delivery recipient but never read it
  back, so durably queued invitations reached the dispatcher with no address.

Also switches new ADRs to date-prefixed identifiers and generates doc/adr/toc.md via
scripts/adr/generate-toc.sh, with a --check mode and pull-request workflow so the
index is never hand-edited again.
2026-08-25 04:36:18 +02:00
Sipke Schoorstra 9e079b27db
fix(user-tasks): let managers revoke a consumed guest invitation (#7984)
* fix(user-tasks): let managers revoke a consumed guest invitation

Verification marks the winning invitation Consumed, which is what issues the
guest session — but RevokeAsync rejected Consumed and never touched sessions at
all. A manager therefore could not withdraw a live guest credential: it stayed
authorized until its TTL elapsed or the task closed. The invitations contract
specifies a revocable, task-scoped session, so this was a real gap.

RevokeAsync now accepts a consumed invitation, rejecting only the already
terminal Revoked and Expired states, and revokes the sessions that invitation
issued. Revocation is scoped to one invitation rather than the whole task, so
other guests keep working: UserTaskGuestSession carries its InvitationId and
IUserTaskGuestSessionIssuer gains RevokeForInvitationAsync, implemented for both
the in-memory and EF Core stores.

Reassignment already cut a guest off, because the policy requires the guest to
still be the assignee. That remains the recovery path for abandoned guest work;
this restores the documented direct revocation alongside it.

Adds three tests. The first fails against the previous behavior.

Reported by Greptile on #7955.

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

* fix(user-tasks): make guest-session revocation fail closed and retryable

Greptile review of the previous commit found three real problems with it.

Revocation committed the invitation as Revoked before revoking its sessions, so
a session-store failure left a live credential behind a guard that rejected the
retry. Sessions are now swept before the terminal state is committed: a failure
commits nothing, leaves the invitation revocable, and a retry repairs it. A
retry against an already-revoked invitation is idempotently successful and
re-runs the sweep, so a caller repairing a partial failure is never told no.

Verification could also hand back a credential that outlived a concurrent
revoke: the manager's sweep ran before the session reached the store and found
nothing. VerifyAsync now re-reads the committed invitation after issuing and
withdraws the credential unless it is still the consumed one it verified.

Invitation-scoped revocation queried an unindexed column, so every revoke
scanned a growing tenant partition of retained session rows. Adds the
(TenantId, InvitationId) index to the EF model and migration, and advertises the
same index from the VNext schema provider.

Adds three tests covering the injected store failure, the idempotent retry, and
the revoke-during-verify race. RevokingAnAlreadyRevokedInvitationIsRefused
asserted the behavior this commit deliberately changes, so it is repurposed to
cover the refusal that remains: an unknown invitation.

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

* fix(user-tasks): sweep guest sessions on both sides of the revoke commit

Moving the sweep before the commit closed the fail-open failure path but opened
its mirror: a concurrent verification can issue a session after the sweep, still
read Consumed at its settled-state check because the revoke has not committed
yet, and hand back a credential that outlives a successful revoke.

Revocation now sweeps after the commit as well. Anything issued in that window
is caught by the second sweep, and any verification that issues after the commit
sees the revoked state at its own settled-state check and withdraws its own
credential. The first sweep still runs before the commit, so a session-store
failure commits nothing and stays retryable.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 02:38:12 +02:00
Sipke Schoorstra 5862bb84e3
fix(build): make ConfigureAwait.Fody weaving actually take effect (#7983)
* fix(build): make ConfigureAwait.Fody weaving actually take effect

ConfigureAwait.Fody only rewrites awaits when it is handed an explicit
ContinueOnCapturedContext value. A bare <ConfigureAwait /> element parses
cleanly, emits no warning, and weaves nothing.

Of the 98 FodyWeavers.xml files under src/, only 22 set the attribute. The
other 76 carried a bare element, so those projects compiled with no weaving
at all while looking correctly configured. Verified on Debug net10.0 builds:
Elsa.Secrets (attribute set) referenced ConfiguredTaskAwaitable, while
Elsa.Alterations (bare element) did not.

Elsa ships as a library and can be hosted where a SynchronizationContext
exists, so weave everywhere rather than dropping the packages.

Fody reads the WeaverConfiguration MSBuild property in preference to any
FodyWeavers.xml, so the directive now lives in a single file, src/Fody.props,
alongside the package references it belongs with. All 98 per-project XML files
are deleted; they would otherwise be dead and misleading.

src/apps has its own props root that does not chain up to
src/Directory.Build.props, so it imports src/Fody.props directly instead of
redeclaring the Fody package references. This second gap was found by the
guard below, not by inspection.

Guard: Directory.Build.targets fails the build for any project that references
ConfigureAwait.Fody without an effective directive (ELSA0001) or that
reintroduces a FodyWeavers.xml alongside it (ELSA0002). Both were verified to
fire, including on the exact original bug shape.

The 22 already-weaving projects are unaffected: their effective directive is
identical before and after, and that set is disjoint from the four projects
holding explicit .ConfigureAwait( calls. All 25 such calls pass false, matching
what the weaver now applies, so they become redundant rather than contradictory
and are left in place.

Also repoints two security-assessment claims that cited the presence of
FodyWeavers.xml as evidence of weaving — the inference that masked this bug.

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

* fix(build): drop FodyWeavers.xml from the new UserTasks modules

Merging main brought in eight new projects. Elsa.UserTasks carried a bare
<ConfigureAwait /> — the same latent no-op this branch removes elsewhere, added
while the fix was in review. Its seven persistence siblings set the attribute.

The guard caught it: ELSA0002 failed CI on the PR merge commit for all three
TFMs, on a file that never existed in the branch's own worktree.

All eight are redundant now that src/Fody.props supplies the directive.
Verified Elsa.UserTasks resolves it and its net10.0 build references
ConfiguredTaskAwaitable.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 01:37:23 +02:00
Sipke Schoorstra ffff359756
feat(user-tasks): add identity-neutral workflow-bound human tasks (#7955)
Adds durable, identity-neutral, workflow-bound human tasks, and reconciles the
REST surface with the approved Studio contract.

- Flat summary/detail DTOs, a global capability descriptor, and workflow context
  captured at activation.
- Scope is part of the list authorization predicate; manager decisions require
  manage:user-tasks; a denied command answers 404 so it cannot prove a task exists.
- Guest sessions are task-scoped, action-allowlisted, and revoked when the task
  closes. Invitations resolve by token hash through the repository, wait in a
  Data Protection encrypted outbox, and are rate limited per caller.
- Masked form values are disclosed only through an audited reveal command.
- Store-specific concurrency failures are translated into a single
  UserTaskRevisionConflictException, so a concurrent edit returns the documented
  revision-conflict result behind any provider instead of a 500.

EF Core (SQLite, SQL Server, PostgreSQL, MySQL, Oracle) and VNext persistence,
hosted due/reconciliation/delivery workers, docs, and 49 tests.

Note: this branch also carries two commits inherited from its branch point that
are not part of User Tasks and are squashed in here — the revert-version
allocation change from #7917 (WorkflowDefinitionPublisher.RevertVersionAsync now
allocates from the last version rather than the latest) and an NU1903 package
pin. Merged deliberately rather than rebased out.
2026-08-25 00:09:06 +02:00