A Rocket.Chat app that blocks messages sharing an encrypted TDF file unless the sender is entitled to share it, as decided by an OpenTDF/Virtru platform against the file's own policy.
The app implements the IPreMessageSentPrevent hook, so it runs before any message is delivered. For each message:
-
Room gate — enforcement only applies in ABAC rooms: private groups carrying a non-empty
abacAttributesarray. Everywhere else the message passes untouched. -
Upload collection (
TdfDetector) — gathers every file the message references: the primarymessage.fileplus file-upload attachments (upload id parsed from the/file-upload/<id>link). Files whose name or mimetype look like a TDF (.tdf,application/tdf, …) are flagged as candidates. -
Content-based TDF detection (
ManifestReader) — each upload's bytes are read and inspected rather than trusting the filename, so a renamed TDF is still caught:- not a ZIP (magic-byte check) → not a TDF, skipped instantly;
- a ZIP without a
manifest.json(e.g. a docx) → not a TDF; - a ZIP whose manifest carries an
encryptionInformation.policy→ a real TDF; its base64 policy is decoded and thedataAttributesare extracted and normalized to full attribute FQNs (bare identifiers are resolved against the configured namespace).
With the Inspect every upload setting off, only name/mimetype candidates are inspected — lighter, but a renamed TDF can slip through.
-
Entitlement check (
EntitlementClient) — when a real TDF is present:- an OIDC token is minted via the client-credentials grant against the configured Keycloak realm;
- the OpenTDF authorization service (
GetDecisionBulk) is asked whether the sender's email may perform the configured action (defaultread) on a resource carrying the TDF's policy attributes.
-
Verdict —
PERMITfor every TDF lets the message through. Anything else throws anAppsEngineException, which prevents the message and shows the sender the reason (not entitled, integration unconfigured, unreadable file, …).
Fail closed: a TDF-looking file that can't be read, a malformed manifest/policy, missing configuration, a sender without an email, or any error from the OIDC/authorization services all block the message rather than letting it through.
The app reads its actual values from core Rocket.Chat server settings (so they can be managed centrally); the app's own settings only hold the ids of those server settings:
| App setting | Default server setting id | Holds |
|---|---|---|
| Platform URL | ABAC_Virtru_Base_URL |
OpenTDF/Virtru platform base URL |
| OIDC client id | ABAC_Virtru_Client_ID |
Client-credentials client id |
| OIDC client secret | ABAC_Virtru_Client_Secret |
Client-credentials secret |
| OIDC endpoint | ABAC_Virtru_OIDC_Endpoint |
Keycloak realm URL (/protocol/openid-connect/token is appended) |
| Attribute namespace | ABAC_Virtru_Attribute_Namespace |
Base namespace used to normalize non-FQN policy attributes |
Plus two direct settings:
- Action evaluated — OpenTDF action checked against the policy (default
read). - Inspect every upload — content-sniff all uploads in ABAC rooms (default on; each file is read into memory once, non-ZIP files are skipped instantly).
Required permissions: networking, message.read, user.read, upload.read, abac.read, server-setting.read.
npm install
npm test # jest suite
npm run build # packages the app (rc-apps) from app/Deploy the packaged zip from app/dist/ via rc-apps deploy or the admin Apps UI. Requires Rocket.Chat apps-engine ^1.44.0.
app/TdfEntitlementApp.ts # hook orchestration: gate -> detect -> decide -> block/allow
app/src/AbacRoom.ts # ABAC room predicate
app/src/TdfDetector.ts # upload collection + name/mime hinting
app/src/ManifestReader.ts # content-based TDF detection + policy attribute extraction
app/src/EntitlementClient.ts # OIDC token minting + GetDecisionBulk call
app/src/serverSettings.ts # core server-setting reader (fail-closed)
tests/ # jest suite
Proprietary — see LICENSE. No use without prior written approval.