UCP · Admin-as-user impersonation
v1.0.0 feature. The manager's per-PBX UCP tab lets an admin land in the User Control Panel logged in as any FreePBX user on the paired PBX — no password required. Troubleshooting-only: reproduce a "my voicemail won't play" or "my BLF is stuck" report as the affected user, without phoning them for their password.
Requirements
- Manager: v1.0.0+, PBX paired.
- PBX: customsso module v1.0.0+ installed. Existing pairs at 0.5.4 auto-upgrade via ↻ Refresh module list on the PBX Status tab.
Admin → SSO Pairing → Allow framing from the managermust be enabled (same requirement as Native UI). - Manager role:
adminorsuperuser. Operator/readonly users see a 403 message. - Target FreePBX user:
Allow UCP Login?must be effectively true (per-user override, else group default). The picker filters on this signal — users with UCP login disabled don't appear.
How it works
- Manager admin opens the UCP tab on any PBX detail page. A searchable table lists every pickable user (filtered by effective
ucp|Global.allowLogin). - Click Open UCP → on a row. The tab's iframe body loads
/pbx/{uuid}/ucp/loginwith the target user in a hiddentarget_userfield (CSRF-signed POST). - Manager mints an HMAC-signed SSO token with
variant='ucp',target_path='/ucp/',target_user=<extension>, 30 s TTL. Nonce is one-shot. - Token is
302redirected tohttps://<pbx>/customsso-sso?token=…inside the iframe. - PBX-side
sso_entry.phpverifies the HMAC signature, nonce atomicity, TTL, target-path/variant match, and effectiveucp|Global.allowLoginfor the target user. - On success:
\FreePBX::Ucp()->storeToken($ucpToken, $uid, $srcIp)establishes a UCP session bound to the admin's browser IP. Browser lands in/ucp/logged in as that user.
The ↺ Switch user button in the header restores the picker overlay; the iframe is reset to about:blank so a stale impersonation session isn't left visible.
Freshness — three ingest paths
The picker is driven by the manager-side cache table pbx_ucp_users, kept in sync three ways:
- Sidecar SSH probe every 60 s — runs a small PHP one-liner over SSH that calls
Userman::getCombinedModuleSettingByID($uid, 'ucp|Global', 'allowLogin')per user. Independent try/except from the status probe; a broken userman query does not mark the PBX offline. - On-demand refresh — the ↻ Refresh user list button on the UCP tab nulls
last_status_atfor that PBX so the sidecar re-probes on its next 5 s tick. Fresh list lands in ~10–20 s. Reload the page after that. - Push on
fwconsole reload— customsso 1.0.0'sdoDialplanHookincludes the currentucp_usersarray in its HMAC-signed status push. So toggling Allow UCP Login? in FreePBX admin propagates the moment the operator saves (which triggers reload).
All three paths use the exact same getCombinedModuleSettingByID signal for filtering, so what the picker offers is guaranteed to match what the PBX-side gate accepts.
Filtering: what counts as "pickable"
A user is emitted to the manager cache and shown in the picker if and only if Userman::getCombinedModuleSettingByID($uid, 'ucp|Global', 'allowLogin') returns a truthy value. FreePBX computes this as: per-user override (if explicitly set), else OR-merged across all groups the user belongs to, else the "All Users" group default (which FreePBX ships as 1 at install time).
Consequence: on a stock FreePBX 17 install, every User Manager user is pickable by default. To restrict: - Toggle Admin → User Management → user → UCP → Allow UCP Login? to No for individual users. - Or flip the group-level default in Admin → Groups → All Users → UCP → Allow UCP Login? to No and let users inherit.
Users with default_extension unset or set to 'none' (User Manager's sentinel for "no linked extension") are always excluded.
Security controls
Same defense-in-depth as the admin SSO path, plus:
| Control | Where enforced |
|---|---|
| Manager admin role required | Sso::launchUcp → Session::requireRole('admin') (chains CSRF) |
| POST + CSRF token (stronger than admin variant's GET + Referer gate) | Session::requireRole default |
| Cache validation before token mint | PbxUcpUser::byUsername + allow_login check |
HMAC-SHA256 signature over {nonce, ts, target_user, target_path, pbx_uuid, variant} |
Sso::mintAndLaunch |
| 30 s TTL + O_EXCL nonce (one-shot) | sso_entry.php |
| Variant/target-path cross-validation | sso_entry.php (variant='ucp' must target /ucp/) |
PBX-side re-check of effective allowLogin |
sso_entry.php UCP branch |
| UCP session IP-pinned to the admin's browser IP | \FreePBX::Ucp()->storeToken(..., $srcIp) |
| Every reject audit-logged (bad_target_user, user_not_found, user_login_disabled, pbx_not_found) | Sso::auditUcpReject() |
Referrer-Policy: no-referrer on the 302 |
sso_entry.php |
CSP frame-ancestors 'self' <manager> on /ucp/ |
customsso-headers.conf.tpl |
Known residual (deferred to v1.0.1): the UCP session row in asterisk.ucp_sessions persists until UCP's own TTL after Switch-user or tab close. IP-pinned, but the pin is the admin's browser IP — a walk-away workstation risk. A signed logout_ucp teardown endpoint + JS beforeunload sendBeacon is planned for v1.0.1.
Failure modes
"UCP module not installed" in the iframe — customsso module is < 1.0.0 on the PBX (missing the variant=ucp branch in sso_entry.php), OR the Ucp BMO class file couldn't be loaded under apache SAPI. Fix: install customsso 1.0.0 on the PBX via ↻ Refresh module list; the sidecar's self-heal SFTP will also push the current sso_entry.php on the next probe.
"UCP login disabled for this user" — the target user has effective Allow UCP Login? = No. Enable it in the FreePBX admin (see Filtering above) and hit ↻ Refresh user list on the manager.
Iframe stays blank / "refused to connect" — same causes as Native UI (untrusted cert, framing disabled, incognito third-party cookie block). See NATIVE_UI_HTTPS.md.
Picker table empty — the sidecar's probe hasn't run yet on a fresh pair. Wait 60 s or hit ↻ Refresh user list. If still empty, check that the PBX has User Manager users configured (Admin → User Management).
Audit
Every UCP-token issuance and every reject writes an audit row:
- Success:
action = sso.issue,detail.variant = ucp,detail.target_user = <ext>,target = /ucp/ - Reject:
action = sso.ucp_blocked,detail.reason = <bad_target_user|user_not_found|user_login_disabled|pbx_not_found>,detail.target_user,detail.remote_addr,detail.user_agent
Query the audit log via Settings → Audit log or SQL: SELECT ts, user_id, pbx_uuid, action, target, JSON_EXTRACT(detail, '$.target_user') FROM audit WHERE action LIKE 'sso.ucp%' ORDER BY ts DESC.