Commit graph

3 commits

Author SHA1 Message Date
Sipke Schoorstra 292e4bd3ea
feat(user-tasks)!: migrate endpoints to structured permissions (#7999)
Re-authors the nine User Tasks permissions as verbs on the user-tasks and
user-tasks/participants resources with a descriptor provider, replacing the
legacy verb:resource strings (UserTasksPermissions is removed along with the
other legacy constant classes). All 17 endpoints declare access through
RequirePermission, and UserTaskActor.HasPermission matches through
PermissionMatcher instead of string equality, so pattern grants reach these
endpoints for the first time. manage:user-tasks becomes user-tasks:supervise
to reflect that it grants oversight, not an aggregate. The migration guide
and contract specs carry the full mapping.

BREAKING CHANGE: legacy user-tasks permission strings no longer authorize
anything. Rewrite grants using the mapping table in
doc/migrations/authorization-model.md.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 12:06:52 +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 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