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,PATCHandDELETE, including job starts (exports, imports, risk-of-bias runs) and upload signatures; GETrequests 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,StoppedAnnotatingandHeartbeat.
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.
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.- Send the handle as the
syrf-imp-edit-contextheader 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 achange-admittedaudit record before the change runs; if the audit cannot be written, the change is refused (503). DELETE /api/impersonation/edit-context?reason=closedends all your edit contexts (a deliberate switch back to read-only).reason=resetis used when impersonation ends, the target changes or the page reloads.GET /api/impersonation/sessionreportsread-onlyoreditfor 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: truealongsidesyrfOwnedApplicationRolesandsyrfOwnedApplicationRolesEnforcedin 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 (theadministratorclaim 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 |