Skip to content

feat(security): act as (delegated entry) - work the personal surfaces in a chosen identity's name - #6529

Merged
delchev merged 1 commit into
masterfrom
feat/act-as-delegated-entry
Aug 5, 2026
Merged

feat(security): act as (delegated entry) - work the personal surfaces in a chosen identity's name#6529
delchev merged 1 commit into
masterfrom
feat/act-as-delegated-entry

Conversation

@delchev

@delchev delchev commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

The scenario

Small real-world businesses (construction crews, workshops, field teams) often have ONE person - the manager - entering everything: the workers do not use computers. The personal ("My") surfaces are exactly the right UX for that data (my timesheet, my leave request, my expenses), but they are hard-scoped to the logged-in user, and a personal-assigned process task (`assignee: personal`) is visible only in the owner's own Inbox - the manager cannot drive the flow at all.

The feature

An entitled user (ADMINISTRATOR) arms an acting identity for the current session and works the personal surfaces in that person's name:

  • `ActAsFacade` (api-security): the override lives in the server-side HTTP session (never a client-supplied header); entitlement is re-checked on EVERY read, so a revoked role kills it mid-session; arm/disarm are audit-logged.
  • `/services/core/actas` (GET state / PUT arm / DELETE disarm): GET is open (the shells render from it), arming 403s for the unentitled.
  • SDK `User.getEffectiveName()`: the acting identity when armed, else the real login - consumed ONLY where the question is "whose records / whose tasks".
  • Generated personal controllers: `me()` and `/me` resolve against the effective user. Everything else holds unchanged underneath: `sensitive:` scrubbing, the forced owner FK, `personalReadOnly` 403s.
  • Inbox: the ASSIGNEE task query serves the acting identity's tasks - a personal-assigned submit task becomes completable by the delegate; candidate-group visibility stays the REAL user's roles (groups are never impersonated); CLAIM assigns to the acting identity.
  • Shells: the My shell gains the switcher, a loud persistent banner and a one-click exit; the Applications shell gains an "Enter data as..." entry point that arms and lands in the My shell. en + bg catalog entries.

Deliberately NOT authentication-level impersonation. Roles, security checks and audit stamping stay the REAL user's - `CreatedBy`/`UpdatedBy` keep recording who really performed the write while the record's owner reference carries the acting identity, so a delegated record shows whose it is AND who really entered it, forever.

Coverage (IT, outermost layer)

`IntentEmissionCoverageIT`, session-pinned sequence: arm -> the my list serves the acting person's rows with the sensitive field still stripped; a delegated POST carries owner = the acting person AND `CreatedBy` = the real user; `personalReadOnly` still 403s while armed; the Inbox assignee query serves the acting person's personal-assigned confirm task; disarm restores self. Emission assert: the generated my controller resolves via `User.getEffectiveName()`. Ran green locally (1/1). PERSONALIZATION_PLAN.md gains phase G.

🤖 Generated with Claude Code

… personal surfaces in a chosen identity's name

The delegated-data-entry scenario: one manager enters timesheets, leave
requests and expenses for workers who do not use computers. Instead of
manager-twin UIs per module, an ADMINISTRATOR arms an acting identity for
the SESSION and the existing personal machinery serves that person's world:

- ActAsFacade (api-security): server-side session override, entitlement
  re-checked on EVERY read (a revoked role kills it mid-session), arm/disarm
  audit-logged. Deliberately NOT authentication-level impersonation - roles,
  security checks and audit stamping stay the REAL user's.
- /services/core/actas (core-configurations): GET state (any user - the
  shells render from it), PUT arm (403 unentitled), DELETE disarm.
- SDK User.getEffectiveName(): the acting identity when armed, else the real
  login - for personal-identity resolution ONLY.
- Generated personal controllers (EntityMyController template): me() and
  /me resolve against the effective user; sensitive scrubbing, forced owner
  FK and personalReadOnly 403s hold unchanged underneath.
- Inbox: the ASSIGNEE task query serves the acting identity's tasks (a
  personal-assigned submit task becomes completable by the delegate);
  candidate-group visibility stays the REAL user's roles - groups are never
  impersonated. CLAIM assigns to the acting identity.
- Shells: My shell gains the switcher, a loud persistent banner and
  one-click exit; the Applications shell gains the 'Enter data as...' entry
  point that arms and lands in the My shell. en + bg catalog entries.

IntentEmissionCoverageIT (outermost layer, session-pinned): arm -> the my
list serves the acting person's rows with sensitive still stripped; a
delegated POST carries owner = acting person AND CreatedBy = the real user;
personalReadOnly still 403s while armed; the Inbox assignee query serves
the acting person's personal-assigned confirm task; disarm restores self.
Emission: the my controller resolves via User.getEffectiveName().
PERSONALIZATION_PLAN.md phase G.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
}
String acting = username.trim();
HttpSessionFacade.setAttribute(SESSION_ATTRIBUTE, acting);
logger.info("Act-as ARMED: [{}] now acts as [{}] for this session", UserFacade.getName(), acting);
String acting = HttpSessionFacade.getAttribute(SESSION_ATTRIBUTE);
HttpSessionFacade.removeAttribute(SESSION_ATTRIBUTE);
if (acting != null && !acting.isBlank()) {
logger.info("Act-as DISARMED: [{}] no longer acts as [{}]", UserFacade.getName(), acting);
@delchev
delchev merged commit 210aa43 into master Aug 5, 2026
10 checks passed
@delchev
delchev deleted the feat/act-as-delegated-entry branch August 5, 2026 11:45
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.

2 participants