Skip to content

Support impersonation under target authority (M2b)

Support staff use impersonation to see SyRF as a user sees it, and occasionally to fix something on that user's behalf. Milestone M2b of the application-authority transition keeps impersonation and puts it under explicit, audited authority, as agreed in #3335 (S1–S7).

Status: dark. The contract runs only with featureFlags.impersonationReadOnlyEnforcement on, which needs enforced application authority (both application-authority flags). Every environment is off, so impersonation behaves exactly as before. Nobody holds SupportImpersonator. Holders are approved separately through M0b (#3852), never assigned to administrators by assumption.

Who may impersonate whom

Rule What it means
The actor needs ImpersonateUsers Granted only by the PM application role SupportImpersonator. Administrator alone does not imply it, and the old administrator claim confers nothing.
Target permissions only (S3) An impersonated request is decided on the target's application, project and stage permissions, including active membership. Your own claims and roles add nothing: syrf_groups is not visible while impersonating.
No escalation You cannot impersonate someone who holds an application role you do not hold, for example an Administrator when you are not one (impersonation-target-privileged).
Active targets only A deactivated target, or one whose account deletion is reserved (the M1c pending-deactivation marker; M4 suspension will use the same marker), is refused (impersonation-target-inactive). You cannot impersonate yourself.
Checked on every request Your ImpersonateUsers and the target's state are re-read (uncached, linearizable) on every impersonated request and every SignalR hub invocation. A revocation denies your next request on every API replica.

Live pushes (M4b): with authority enforced, pushes on an already-open SignalR connection (project details and summaries, statistics, presence, export jobs) are re-checked when they are sent: your ImpersonateUsers and the target's state are re-read, so after a revocation the next push is withheld on every replica (realtime delivery). Known limit: the target's own claim-release notice is addressed to the target's user, not to a connection, so it is not checked against your permission; it carries only the target's own study and stage identifiers.

The target's name and email come from PM, not from what the browser sends. Role administration and the capabilities read are refused while impersonating, as before. The Admin Console's picker lists people through GET /api/impersonation/candidates, which needs ImpersonateUsers (not ListUsers, so a support holder need not be an administrator) and returns only names, email and whether the account is deactivated.

Read-only by default (S4)

Impersonation starts read-only. The server refuses anything that could change state, not just buttons in the UI:

  • every POST, PUT, PATCH and DELETE, including job starts (exports, imports, risk-of-bias runs) and upload signatures;
  • GET requests with side effects, marked [ImpersonationSideEffect]: the four review-allocation reads (next study, specific study, for review and reconciliation), which reserve review work;
  • SignalR review presence: JoinStudyReview, LeaveStudyReview, StartedAnnotating, StoppedAnnotating and Heartbeat.

Two body queries are marked [ImpersonationReadOnlySafe] and stay available: the study table query and the email lookup. Reads never record bookkeeping for the target: support impersonation records no sign-in and no project view for them.

A refused change answers 403 impersonation-read-only and is audited.

Edit mode (S4–S6)

Edit mode is a deliberate, short-lived operating mode. It grants nothing: it only lifts the read-only restriction for changes the target could make themselves, and business rules still apply.

  1. POST /api/impersonation/edit-context (while impersonating) opens an edit context for you and the current target. It returns a handle, once. Keep it in the tab's memory only: never store it.
  2. Send the handle as the syrf-imp-edit-context header on each change (for the SignalR connection, as the same query parameter when connecting; a query parameter can appear in ingress access logs, which is acceptable because the handle is bound to you and the target, checked against your own sign-in, and expires within 30 minutes). Each change re-validates it and writes a change-admitted audit record before the change runs; if the audit cannot be written, the change is refused (503).
  3. DELETE /api/impersonation/edit-context?reason=closed ends all your edit contexts (a deliberate switch back to read-only). reason=reset is used when impersonation ends, the target changes or the page reloads.
  4. GET /api/impersonation/session reports read-only or edit for the handle presented, and the maximum lifetime.

Lifetime: at most 30 minutes (a proposal, not a framework default). A context is never extended; open a new one. A handle is refused (impersonation-edit-context-invalid) once it has ended or expired, or when it is used for another target or by another person. Contexts live in pmImpersonationEditContext (only the handle's SHA-256 is stored); a TTL index removes them a day after expiry.

In the web app (#3870): where the contract is on, the impersonation banner shows Read-only and an Edit as this user button; in edit mode it shows Editing as this user until HH:MM and Stop editing. Where the contract is off (the API answers 404), the banner is exactly as before. The handle lives only in the tab's memory: a reload, a new tab, a different target or Stop all start read-only, and the app also ends your server contexts (reason=reset). That reset ends every edit context you hold, so reloading or opening another impersonating tab also returns your other tabs to read-only; this is deliberate (fail safe), and those tabs say so on their next change. When the time limit passes, the tab returns to read-only. If the API refuses a change because of the impersonation (read-only, stale context, revoked actor, inactive target), the tab drops its handle and says why.

Not yet in the web app (follow-ups): the impersonation picker still lists people through ListUsers, so a holder who is not an administrator cannot use it yet (the candidates endpoint exists); the SignalR connection is not reconnected with the edit handle, so review presence (joining a study for review) stays read-only even in edit mode; and no editor registers its unsaved changes yet, so the Save/Discard/Cancel prompt below appears only once an editor registers with SupportImpersonationDrafts.

Unsaved changes (S6): when you deliberately turn edit mode off while a registered editor has unsaved changes, the web app offers Save (save under your current permissions and edit context, then switch to read-only; a failed save is never shown as successful), Discard (drop the changes, switch to read-only) or Cancel (stay in edit mode). There is no final-save exception: after a revocation, expiry or reload, the next save is refused like any other change.

Audit (dual attribution)

pmImpersonationAudit records both people on every edit-mode event: ActorInvestigatorId (you), TargetInvestigatorId, Event (edit-opened, edit-ended, change-admitted, change-refused), Method, Route (the route template or hub:<method>, never identifiers from the URL), EditContextId, Code and AtUtc. IDs are CSUUID: query them with CSUUID("…"), never UUID(…). Application logs record only the outcome, code, method and route template.

Problem codes

Status Code Meaning
403 impersonation-not-authorized You do not hold ImpersonateUsers now (never granted, or revoked).
403 impersonation-target-inactive The target is deactivated or has a pending deactivation.
403 impersonation-target-privileged The target holds an application role you do not hold.
403 impersonation-target-unknown, impersonation-target-is-actor No such person, or yourself.
403 impersonation-read-only A change without an edit context.
403 impersonation-edit-context-invalid The handle is unknown, ended, expired, or for another target or person.
400 impersonation-target-invalid The target header is not an ID.
409 impersonation-not-active An edit-context call without a target.
503 authority-unavailable, impersonation-audit-unavailable Authority or the audit could not be established; nothing ran. Retry shortly.

Operating the flag

  • Enable (after M0b approves holders and M6 enforces authority): set featureFlags.impersonationReadOnlyEnforcement: true alongside syrfOwnedApplicationRoles and syrfOwnedApplicationRolesEnforced in the environment's values. The API refuses to start if the flag is on without enforced authority. It is a deployment flag, deliberately not runtime-overridable, so every replica runs one contract.
  • Rollback: set it to false. Impersonation returns to the legacy behaviour exactly (the administrator claim gate, target substitution, no read-only mode); open edit contexts are simply unused and expire.
  • Grant a holder: only through an approved M0b holder list. The role editor still refuses SupportImpersonator.

Where it lives

Part Path
Pure rules (admission, no escalation, edit-context validity, 30-minute maximum) src/libs/project-management/SyRF.ProjectManagement.Core/Authorization/SupportImpersonation.cs
Edit contexts and audit (Mongo, linearizable reads) src/libs/project-management/SyRF.ProjectManagement.Mongo.Data/Authorization/MongoSupportImpersonationStore.cs
Middleware seam (legacy path unchanged when off) src/libs/webhostconfig/SyRF.WebHostConfig.Common/Infrastructure/UserImpersonationMiddleware.cs, ISupportImpersonationAdmission.cs, CurrentUserService.cs
API contract, hub filter, attributes, settings src/services/api/SyRF.API.Endpoint/Authorization/SupportImpersonation/
Web: tab handle, mode, S5 resets, S6 prompt, draft registry src/services/web/src/app/core/auth/support-impersonation/, core/http-interceptors/impersonation-interceptor.service.ts, core/components/impersonation-banner/
Control endpoints src/services/api/SyRF.API.Endpoint/Controllers/SupportImpersonationController.cs
Tests SupportImpersonationRulesTests (PM Core), MongoSupportImpersonationStoreTests (replica set), SupportImpersonationContractTests and SupportImpersonationHubAndClassificationTests (API), support-impersonation.service.spec.ts, impersonation-interceptor.service.spec.ts and the banner spec (web), the M2b block of e2e/authority/tests/authority-m1a-application-roles.spec.ts