The Engagement Report tab wrote STAFF_REPORT_KEY into the iframe src for every user with "access CiviCRM", without checking contact permissions. Anyone could copy it from the DOM and read any org's report and files, bypassing Civi ACLs; the key never expired and landed in access logs. - Extension (0.4.0): Tab.php checks Contact_Permission::allow(VIEW) and mints sig = base64url(HMAC-SHA256(key, "webform-mw.staff-report.v1: <org>:<exp>")) with an 8h exp. The secret no longer leaves the server. - App: lib/staff-token.mjs verifies the token (constant-time, org must match, rejects expired or >12h tokens). /staff/report, /api/staff/report and /api/staff/file share staffAuthQuery(); client links carry exp/sig. - Legacy ?key= is still accepted (constant-time compare, logs a warning) so the app can deploy before the extension. Remove it once every Civi install runs extension 0.4.0. - PHP/Node signature compatibility is pinned by a PHP-generated vector.
106 lines
4.5 KiB
Markdown
106 lines
4.5 KiB
Markdown
# WebForm-mw — CiviCRM extension
|
|
|
|
Adds an **Engagement Report** tab to Organization contact-view pages that
|
|
embeds the FCI Co-op Survey staff report
|
|
(`https://survey.fci.coop/staff/report`) in an iframe. The tab shows the
|
|
report for the current organization, scoped by the contact id in the URL.
|
|
|
|
The embedded app handles its own auth via a shared staff secret. The
|
|
extension is otherwise read-only and adds no Civi tables, custom fields,
|
|
or scheduled jobs.
|
|
|
|
## Install
|
|
|
|
1. Copy the entire `webform-mw/` directory to your CiviCRM extensions
|
|
directory (typically `<civi-root>/sites/default/ext/` or wherever
|
|
`[civicrm.extensionsDir]` points in your `civicrm.settings.php`).
|
|
- The directory name on disk must be `webform-mw` (matching `<key>` in
|
|
`info.xml`).
|
|
2. In CiviCRM:
|
|
`Administer → System Settings → Extensions → Add new → Refresh`,
|
|
then click **Install** next to "WebForm-mw".
|
|
3. Configure (see next section). The tab will be hidden until both
|
|
settings are present.
|
|
|
|
## Configuration
|
|
|
|
The extension reads two values, in this order:
|
|
|
|
1. Constants in `civicrm.settings.php` (preferred):
|
|
|
|
```php
|
|
define('WEBFORM_MW_APP_URL', 'https://survey.fci.coop');
|
|
define('WEBFORM_MW_STAFF_KEY', '...the STAFF_REPORT_KEY shared with the app...');
|
|
```
|
|
|
|
2. Or environment variables (`WEBFORM_MW_APP_URL`, `WEBFORM_MW_STAFF_KEY`)
|
|
set wherever PHP-FPM / the web server reads its environment from.
|
|
|
|
`WEBFORM_MW_STAFF_KEY` must match the `STAFF_REPORT_KEY` configured on the
|
|
Next.js app (Amplify environment / SSM Parameter Store). The two are the
|
|
same shared secret; rotate them together.
|
|
|
|
`WEBFORM_MW_APP_URL` is the public base URL of the WebForm-mw deployment
|
|
(no trailing slash). Production: `https://survey.fci.coop`.
|
|
|
|
When either value is missing, the tab body shows a help banner with the
|
|
exact configuration snippet to paste, so anyone installing the extension
|
|
without prior context can self-serve.
|
|
|
|
## Behaviour
|
|
|
|
- Tab title: **Engagement Report**.
|
|
- Visible only on **Organization** contacts (Individuals and Households
|
|
see no tab). The check happens server-side in the tabset hook.
|
|
- Tab body is an iframe pointing at
|
|
`${WEBFORM_MW_APP_URL}/staff/report?org=<cid>&exp=<unix>&sig=<hmac>&frame=1`.
|
|
- `frame=1` tells the embedded app to suppress its site header/footer
|
|
and emit a `postMessage({type:"webform-mw-height", height})` payload
|
|
on render and on resize. The tab's small inline script listens for
|
|
this message and auto-sizes the iframe so there's no nested scrollbar.
|
|
- The script validates the postMessage `event.origin` against the
|
|
configured `WEBFORM_MW_APP_URL` origin before resizing.
|
|
|
|
## Security notes
|
|
|
|
- The shared secret never leaves the server. Each tab render mints a
|
|
signed token, `HMAC-SHA256(secret, "webform-mw.staff-report.v1:<org>:<exp>")`,
|
|
valid for one org for 8 hours. The app rejects tokens for other orgs,
|
|
expired tokens, and anything valid for longer than 12 hours.
|
|
- Tokens are only minted after `CRM_Contact_BAO_Contact_Permission::allow()`
|
|
confirms the current user can view the org, so Civi contact ACLs carry
|
|
through to the embedded report.
|
|
- Rotating the secret: change `WEBFORM_MW_STAFF_KEY` and the app's
|
|
`STAFF_REPORT_KEY` together; outstanding tokens stop working at once.
|
|
- The extension uses `access CiviCRM` as its access argument — anyone
|
|
with that permission sees the tab on Organization contacts. Tighten by
|
|
changing the `<access_arguments>` value in `xml/Menu/webform_mw.xml`
|
|
to a more specific permission (e.g., `view all contacts`) and
|
|
reinstalling.
|
|
- The embedded app's CSP (`frame-ancestors`) must include the CiviCRM
|
|
origin or the iframe will refuse to render. The Next app reads
|
|
`CIVI_BASE_URL` at build time and adds its origin to the staff route's
|
|
CSP automatically. Confirm with `curl -I <app>/staff/report` after
|
|
deploy — you should see `Content-Security-Policy: ... frame-ancestors
|
|
'self' https://<your-civi-host> ...`.
|
|
|
|
## Files
|
|
|
|
- `info.xml` — extension manifest. Key `webform-mw`, type `module`.
|
|
- `webform_mw.php` — `hook_civicrm_tabset` + config readers.
|
|
- `CRM/WebformMw/Page/Tab.php` — page controller for the tab snippet.
|
|
- `templates/CRM/WebformMw/Page/Tab.tpl` — iframe + height-listener.
|
|
- `xml/Menu/webform_mw.xml` — registers the
|
|
`civicrm/contact/view/engagement-report` URL.
|
|
|
|
## Versions tested
|
|
|
|
- CiviCRM 5.50+
|
|
- PHP 7.4+ / 8.x
|
|
- Tested against Backdrop-as-host and Drupal-as-host CiviCRM deployments.
|
|
|
|
## Uninstalling
|
|
|
|
`Administer → Extensions → Disable`, then `Uninstall`. Removes nothing
|
|
from the database; the extension stores no Civi data of its own.
|