Skip to content

fix(security): act-as - the armed state expires, is visible everywhere, and never stamps the acted-as person on a role-addressed task (#6694) - #6705

Merged
delchev merged 1 commit into
masterfrom
fix/act-as-expiry-and-visibility
Aug 13, 2026
Merged

fix(security): act-as - the armed state expires, is visible everywhere, and never stamps the acted-as person on a role-addressed task (#6694)#6705
delchev merged 1 commit into
masterfrom
fix/act-as-expiry-and-visibility

Conversation

@delchev

@delchev delchev commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #6694.

An act-as (delegated entry) arming is a session-scoped override that makes the personal surfaces and the Inbox's assignee query serve someone else's world. It worked as designed — except for the one thing nobody armed it for: being forgotten. An arming made at 18:03 for one delegated timesheet entry was still armed hours later, and from then on the Inbox served the acted-as person's tasks. A live invoice-approval step looked like a broken process (the task was there all along — ACT_RU_TASK had to be read in prod to prove it), and the steps that were worked through the armed session were completed with assignee = <acted-as person>, attributing approval decisions to an employee who never made them.

Three changes, one per ask in the issue.

1. The armed state expires on its own

ActAsFacade now stamps the arming time in the session and drops the override once DIRIGIBLE_ACT_AS_TTL_SECONDS (default 1800 — 30 minutes) have passed, logging the expiry. The window is absolute: it starts at arming and no activity renews it. A sliding window would be useless here — the Inbox polls every 30s, so the state would never expire while a tab is open, which is exactly the situation the incident describes. The expiry decision fails closed: an arming whose timestamp is missing or unparsable is treated as expired.

GET /services/core/actas now also returns expiresAt (epoch millis), and both shells render it in the banner ("… - until 18:33").

2. The Inbox says what the arming is hiding

New GET /services/inbox/act-as{ actingAs, hiddenTasks }, where hiddenTasks counts the tasks assigned to the real user — the ones the armed assignee query is not serving. The shared Inbox page renders a persistent strip ("Acting as X — N of your own tasks are hidden", with Exit) and the empty state says whose list is being shown instead of "You have no pending tasks."

The Applications shell also gains the banner, the exit action and the "Stop acting as X" menu entry that until now only the Personal shell had. The armed identity follows the session into the back office; the absence of any on-screen trace there is what let the arming be forgotten in the first place.

3. A back-office group task is claimed by the REAL user

CLAIM used to assign to ActAsFacade.effectiveUser() unconditionally. It now assigns to the acting identity only for a task addressed to that person — one where they are a candidate user, i.e. their own work the delegate is entering on their behalf. A task the caller reached through their own roles (approve, issue, send) is claimed by the real user, whatever is armed. Stamping the acted-as person there both corrupts the audit trail and strands the task on an identity that the claim has just removed from every group-candidate query.

This is the answer to the issue's third ask ("should back-office group tasks be served to an armed session at all?"). Hiding them would be worse — they are the real user's own work, and making tasks disappear is the failure mode we are fixing. Serving them while attributing them correctly is the honest fix.

Not in scope

Auto-disarm on navigating out of the personal shell: the server cannot observe navigation, and a client-side disarm would fire on every iframe/hosted-app transition. The TTL plus a banner on every shell covers the same ground without a new failure mode.

Tests

  • ActAsFacadeTest (unit) — the expiry decision: fresh, elapsed, configured window, and the undatable-arming fail-closed case.
  • ActAsSessionIT (HTTP, untagged → runs in the PR smoke gate) — the deadline is reported; an arming with a 1s window stops being honoured with nobody disarming it; and, over a real one-user-task process addressed to ADMINISTRATOR: a claim from an armed session lands on admin, the armed Inbox reports it as hidden, and an unarmed session sees assignee = admin.

Verified locally: ActAsFacadeTest green, ActAsSessionIT green (H2, headless).

🤖 Generated with Claude Code

…e, and never stamps the acted-as person on a role-addressed task (#6694)

An act-as (delegated entry) arming worked as designed except for the one
thing nobody arms it for: being FORGOTTEN. An arming made for a single
delegated timesheet entry was still armed hours later, and from then on the
Inbox served the acted-as person's world - a live approval step looked like
a broken process, and the steps that were worked through the armed session
were completed with assignee = the acted-as person, attributing decisions to
an employee who never made them.

- Expiry: ActAsFacade stamps the arming time and drops the override after
  DIRIGIBLE_ACT_AS_TTL_SECONDS (default 1800). The window is ABSOLUTE - a
  sliding one would be renewed forever by the Inbox's own 30s poll - and the
  decision fails closed on an arming it cannot date. /services/core/actas
  now reports expiresAt, and both shells render the deadline.
- Visibility: GET /services/inbox/act-as reports the armed identity and how
  many of the REAL user's tasks the arming hides; the shared Inbox renders a
  persistent strip + exit and says whose list it is showing in its empty
  state. The Applications shell gains the banner, exit and "Stop acting as"
  entry that only the Personal shell had - the armed identity follows the
  session into the back office.
- Attribution: a CLAIM assigns to the acting identity ONLY for a task
  addressed to that person as a candidate user. A task reached through the
  real user's own roles is claimed by the real user, whatever is armed;
  stamping the acted-as person there corrupts the audit trail and strands
  the task on an identity the claim just removed from every group query.

ActAsFacadeTest covers the expiry decision; ActAsSessionIT (HTTP, in the PR
smoke gate) covers the reported deadline, an arming expiring with nobody
disarming it, and - over a real ADMINISTRATOR-addressed user task - a claim
from an armed session landing on the real user while the armed Inbox reports
it as hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@delchev
delchev merged commit 494c58a into master Aug 13, 2026
9 checks passed
@delchev
delchev deleted the fix/act-as-expiry-and-visibility branch August 13, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Act-as: forgotten armed state silently hides the real identity's tasks - needs expiry and a visible hint

1 participant