How to think about File Storage security
File Storage is a logical layer over ERPat's shared general_files registry. It manages only its own logical files (file_nodes) and is deliberately blind to files owned by other modules — profile photos, payslips, ticket attachments. That boundary is not a filter you can misconfigure; it is enforced by construction (see the isolation guarantee below).
Everything an administrator controls falls into four layers:
| Layer | What it decides | Where you manage it |
|---|---|---|
| Permissions | Who may use each capability at all | Roles & Permissions settings |
| ACL (per file/folder/space) | Who may see or act on a specific item | Set by users; computed server-side |
| Policies | The org-wide guard rails (sharing, expiry, retention) | Governance → Policies |
| Legal holds & purge gates | What may never be permanently destroyed | Governance → Legal Holds; safe-purge job |
The permission set
All permission keys live under the file_storage family and are gated by the module_file_storage setting. Grant the base permission plus the specific capabilities each role needs — capabilities are additive, and the high-risk governance keys should be reserved for administrators and compliance owners.
Base access
| Permission | Grants |
|---|---|
file_storage | Base access to the module — see the workspace, the tabs, and files the user is authorized for. Required for every other key to take effect. |
file_storage_create | Create folders and upload new files. |
file_storage_update | Rename, move, and upload new versions of existing items. |
file_storage_delete | Send items to Trash and restore them. |
Collaboration & sharing
| Permission | Grants |
|---|---|
file_storage_share_internal | Share files and folders with other ERPat users, groups, or departments (Shared with Me grants). |
file_storage_share_external | Create external share links (token URLs) that reach recipients outside ERPat. |
file_storage_manage_permissions | Change the ACL of an item — add or remove viewer/commenter/contributor/editor/manager/owner roles. |
file_storage_create_space | Create a Team Space (tenant-owned shared area). |
file_storage_manage_space | Manage a Team Space — membership, managers, and settings. |
file_storage_create_file_request | Create upload-only file requests that let outsiders send files in without seeing anything. |
Governance (high-risk — restrict to admins & compliance)
| Permission | Grants |
|---|---|
file_storage_governance | Access to the Governance admin console. |
file_storage_governance_view_activity | View the tenant-wide activity feed and audit trail. |
file_storage_governance_manage_quarantine | Review, release, or reject files held in upload quarantine. |
file_storage_governance_manage_policies | Change the org-wide policies (sharing, expiry, OTP, retention, minimum managers). |
file_storage_governance_legal_hold | Place and release legal holds that block permanent purge. |
file_storage_governance_purge | Permanently purge items — subject to the fail-closed safe-purge gate. |
file_storage_governance_legal_hold and file_storage_governance_purge on different people where you can. A legal hold must be able to survive the person who wants to destroy the data.The Governance console
The Governance console (FileStorage_admin) is a separate admin surface from the user workspace. It has four tabs, each gated by its own governance permission.
Policies
Org-wide guard rails, applied to every user of the tenant. Set these once and they constrain what sharing and lifecycle actions are even possible:
| Policy | What it controls |
|---|---|
| External sharing | Whether external share links may be created at all, and the widest scope allowed (restricted / tenant / external recipients / anyone / upload-only). |
| Link expiry | The default and/or maximum lifetime of a share link before it stops opening. |
| OTP default | Whether new external links require an email one-time passcode by default. |
| Trash retention | How long trashed items are kept before they become eligible for the safe-purge job. |
| Minimum managers | The minimum number of managers a Team Space must keep (default enforces at least two, so a space is never left ownerless). |
Quarantine review
Uploads pass through a validated intake and can be held for review before they become live files. The Quarantine tab is where an authorized reviewer inspects held uploads and either releases or rejects them.
File_scanner_service) for a real anti-virus / content-disarm engine, but no AV engine is enabled today — that is a planned future integration, not a current control.Legal Holds
Place a legal hold on files or spaces that must be preserved for litigation, audit, or regulatory reasons. A held item cannot be permanently purged — the safe-purge gate treats an active hold as a hard block. Holds are recorded in file_legal_holds with their scoped items in file_legal_hold_items, and releasing a hold is itself an audited action.
Risk dashboard
A tenant-wide view of exposure — surfaced by the daily risk-scan job (see Cron jobs). It highlights the things a compliance owner needs to watch: broadly-scoped or expiring external links, quarantine backlog, and items under legal hold. Use it as the standing review surface for the security posture of the storage estate.
Reference-aware trash & permanent purge
Deletion is two-stage. First, items are soft-deleted to Trash and remain fully recoverable. Only after the trash-retention window — and only via the safe-purge job — can bytes actually be destroyed, and even then a fail-closed gate stands in the way.
Because physical bytes are shared through ERPat's general_files registry, the module keeps a reference ledger (general_file_references). Permanent purge is reference-aware: a physical object is destroyed only when nothing still points to it. The gate blocks a purge if any of these is true:
| Blocking condition | Why it blocks |
|---|---|
| An active reference exists | Another logical file (or module) still relies on the bytes. |
| It is the current version | A live file still resolves to this object. |
| Trash retention has not elapsed | The recovery window is still open. |
| A legal hold covers the item | Preservation overrides destruction. |
The purpose-policy isolation guarantee
File Storage never shows or touches files that belong to other modules. This is guaranteed by two design choices working together:
- Owner-less registry rows. File Storage writes
general_filesrows with no owning-module identity of another feature. It only ever resolves logical files it created (file_nodesand theirfile_versions), so avatars, payslips, and ticket attachments are simply out of its reach — there is no query that could return them. - Purpose-policy registry. Every logical file carries a purpose, and the purpose-policy registry (
File_purpose_policy) governs what may be done with it. Purposes are isolated from one another, so a policy configured for one purpose cannot leak behavior onto another.
The practical consequence: enabling, configuring, or even misconfiguring File Storage cannot expose another module's files. Isolation is a property of the data model, not a permission you must remember to set.
The six cron jobs
File Storage runs six Advanced Cron jobs. Each runs per tenant and self-gates on module_file_storage — a job does nothing on a tenant where the module is disabled. They are the moving parts that keep intake, access, membership, the registry, and retention correct without manual work.
| Job slug | Schedule | What it does |
|---|---|---|
file_storage_upload_pipeline | every 1 min | Processes queued upload sessions — validates, materializes bytes into the file pipeline, and promotes them to live versions (or into quarantine). |
file_storage_access_expirer | every 5 min | Expires share links and time-bound access grants that have passed their expiry or max-opens limit. |
file_storage_membership_sync | daily | Reconciles Team Space membership against department membership so space access follows org changes. |
file_storage_registry_reconcile | hourly | Reconciles the reference ledger against general_files so reference counts stay accurate. |
file_storage_safe_purge | daily | Runs reference-aware permanent purge on retention-eligible trashed items — respecting every fail-closed block above. |
file_storage_risk_scan | daily | Computes the Governance Risk dashboard — exposure from external links, quarantine backlog, and held items. |
file_storage_safe_purge acts on it; an expiry policy only matters because file_storage_access_expirer revokes on it. Confirm the tenant's cron runner is active or none of it fires.The two REST APIs
File Storage exposes two distinct API surfaces with different audiences and different authorization models.
Integration API — /api/v1/file-storage/*
The machine-to-machine surface for integrations, served by FileStorage_api. Every action authorizes a bearer scope (authorizeScope):
| Scope | Grants |
|---|---|
filestorage:read | Read access — list and retrieve File Storage resources. |
filestorage:write | Write access — create and modify File Storage resources. |
End-User API — /v1/api/file-storage/*
The self-service surface for the signed-in end user, served by FileStorage_euapi. It is gated by requireModule + requirePermit, and — critically — identity is always resolved from the session via selfUserId(). A caller can only ever see their own files: self-service list, view, download, recent, and starred. There is no way to ask this API for someone else's data.
/api/v1/file-storage/* only.The demo seeder
The module ships a demo seeder, FileStorageDemo, that loads representative sample data (spaces, folders, files, versions, and shares) so an evaluator can see the workspace populated immediately. It is optional and operator-driven — run it only in demo or evaluation environments:
php erpat db:seed FileStorageDemo
The seeder is idempotent and reversible in line with ERPat's seeder contract; it does not install schema or functional configuration (those come from the module migrations).
Setup checklist
- Enable the module — turn on
module_file_storagein Manage Modules. - Run the module migrations:
php erpat migrate:modules. - Assign permissions per role — base access for everyone who needs files, collaboration keys for managers, governance keys for admins and compliance only.
- (Optional) Set your guard rails in Governance → Policies — external sharing scope, link expiry, OTP default, trash retention, and minimum managers.
- Confirm the tenant cron runner is active so the six jobs run.
- (Optional, non-production) Load sample data:
php erpat db:seed FileStorageDemo.