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
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_TASKhad to be read in prod to prove it), and the steps that were worked through the armed session were completed withassignee = <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
ActAsFacadenow stamps the arming time in the session and drops the override onceDIRIGIBLE_ACT_AS_TTL_SECONDS(default1800— 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/actasnow also returnsexpiresAt(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 }, wherehiddenTaskscounts 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
CLAIMused to assign toActAsFacade.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 toADMINISTRATOR: a claim from an armed session lands onadmin, the armed Inbox reports it as hidden, and an unarmed session seesassignee = admin.Verified locally:
ActAsFacadeTestgreen,ActAsSessionITgreen (H2, headless).🤖 Generated with Claude Code