AccountedPro - Enforcement and security
The guiding principle: the server is authoritative, and enforcement hooks the document lifecycle — not the browser. The same decision applies whether a record is reached through the Desk, the REST API, or an MCP connector. The client script only makes the screen match what the server already returned.
The two permission hooks
Every non-core DocType (minus an explicit list of core / meta ones) is wired to Frappe's two hooks:
permission_query_conditions→ a SQLWHEREthat filters lists / reports to the rows a person may see.has_permission→ the per-document yes/no for a specific action.
Wiring all DocTypes (not just governed ones) is deliberate: the engine no-ops for any DocType without an enabled rule, so enforcement never depends on a fragile boot-time enumeration, and editing a rule for an already-governed DocType takes effect live. (Adding a rule for a brand-new DocType needs a cache clear + restart, because the hook map is built once per worker boot.)
Both hooks fail safe: an unexpected error is logged, and permission_query_conditions falls closed (hide rows) while has_permission defers to native — a bug can never silently expose data or 500 the request.
Field and child-row redaction
Row access is necessary but not sufficient — the contents are redacted too:
- On load, only metadata flags are set (which fields to hide/blur, which child rows) — never the values, because mutating a document mid-load corrupts Frappe's dynamic-link plumbing.
- After the core read, the response document is masked — values blanked or partial-masked, hidden child rows removed/blurred, timeline stripped when the door hides it. If redaction itself errors, the document is blanked closed rather than leaked.
- The
frappe.client.getand report-view read paths are overridden too, so REST and MCP payloads (single doc and list rows) are redacted the same way the Desk is. - On save, masked fields and hidden child rows are re-hydrated from the database, so a person editing a partly-masked record can't blank the real data.
Masking is type-safe: text/link/select → empty, numeric → 0, dates → null — never a raw null on a link cell, which would corrupt the row.
Files, print, email
These paths bypass has_permission in Frappe, so they're guarded at the request layer:
- File download — a
/private/files/…request is blocked when the file's document has a door that blocks attachments, or when the file is referenced only by hidden child rows. - Print / PDF — the print, PDF and print-HTML routes are blocked when every opening door sets Block printing.
- Email — the email-send routes are blocked when every opening door sets Block email (there are several send paths; the guard covers them centrally).
The matching door toggles are Block attached files, Block printing, Block email, plus Hide activity & comments (timeline) and Hide on list.
Hide on list
A door flagged Hide on list still grants direct (URL) access, but its rows are dropped from the DocType's unfiltered list — and reappear the moment a filter is applied (e.g. by customer). “You can open any of these by link, but browsing the list should only show the ones related to you.” It only affects the genuine list path for that DocType; nested has-access / cascade checks are unaffected.
Anti-scraping and audit
- A throttle counts reads / exports per person per window and blocks + logs when a cap is exceeded.
- An access log records consequential decisions (user / channel / action / result / rule).
Custom code must go through the engine
Server Scripts, custom reports and integrations that read with raw SQL or an un-redacted get_all bypass field/child redaction. Route those reads through the helpers in the Developer reference so they honour the same rules.
Next: The Access Simulator.