Permissions, Roles & Policies
Goal: the most powerful permission system among OSS headless CMSs. Supports field-level, row-level, time-bound, IP-bound, attribute-based, and composable policies.
See also the detailed investigation/blueprint: permission-builder-directus-investigation.md.
The current audit of the implementation: permission-service-compose-audit.md.
Migrating role flags to policy flags: role-policy-flag-migration.md.
1. Model
User ──┬─► UserPolicy (direct attach, override)
└─► UserSite (role per site)
Role ──► RolePolicy (priority) ──► Policy ──► Permission[] per (collection, action)
- Role: a fixed set assigned to a user (per site).
- Policy: a reusable unit, attachable to multiple roles/users, with a priority order (lower
priority= runs first, higher ones override later). - Permission: a specific
(collection, action)rule withpermissions,validation,presets,fields.
Design note 2026-06-03: Directus v11 moved
admin_access,app_access,enforce_tfa,ip_accessoff the role and onto the policy. LumiBase still hasroles.adminAccess/appAccess; treat this as a compatibility layer and migrate to policy flags so the role is only a grouping. For the detailed strategy see Migrating role flags to policy flags.
2. Permission record (JSON DSL)
{
"collection": "posts",
"action": "update",
"fields": ["title", "body", "status"],
"permissions": {
"_and": [
{ "user_created": { "_eq": "$CURRENT_USER" } },
{ "status": { "_neq": "archived" } },
{ "_or": [
{ "site_id": { "_eq": "$CURRENT_SITE" } },
{ "_role": { "_in": ["admin"] } }
]}
]
},
"validation": {
"status": { "_in": ["draft", "review", "published"] }
},
"presets": { "updated_by": "$CURRENT_USER" }
}
Supported operators
- Logic:
_and,_or,_not. - Comparison:
_eq,_neq,_lt,_lte,_gt,_gte,_in,_nin,_contains,_starts_with,_ends_with,_between. - Date:
_dynamic, e.g.$NOW(-7 days). - Magic:
$CURRENT_USER,$CURRENT_ROLE,$CURRENT_SITE,$NOW,$IP,$HEADERS.x-foo. - Extended magic:
$CURRENT_ROLES,$CURRENT_POLICIES,$CURRENT_API_KEY,$CURRENT_USER.email,$CURRENT_USER.preferences.locale,$NOW(+2 hours),$NOW(-7 days). - Unknown magic vars fail closed; a rule using an unsupported variable will not match.
3. Field-level
fields: ["*"]= all.fields: ["title","body"]whitelist.fields: ["-secret"]blacklist (prefix-).- For
read: returns a field mask; the server does not serialize forbidden fields. - For
update/create: the server rejects the request if the payload contains a disallowed field.
4. Row-level
- Evaluate the
permissionsAST → SQL where (Drizzle) forread/list, or post-fetch check forupdate/delete(safe updates still need a WHERE). - Cache the "compiled rule" by
(policyId, action)in KV.
5. Time-bound & IP-bound
- A policy has optional fields at the
policylevel:
{ "activeWindow": { "from": "2025-01-01T00:00:00Z", "to": "2025-12-31T23:59:59Z" }, "allowIps": ["10.0.0.0/8"], "denyIps": [] }
- Evaluated before the rules; reject early if outside the window.
- Should support IPv4, IPv6, CIDR, and IP ranges.
ipDenybeatsipAllow. - If a policy fails the IP/time check, remove that policy from the chain rather than denying the whole principal.
5.1. App access, admin access, enforce TFA
adminAccess=truebypasses all permission checks; an admin policy does not need seeded permission rows.appAccess=truelets a principal use Studio, which is not the same as API access. An API-only user can still call the API if they have permission rows but is blocked from Studio.- The Studio client must send
X-Lumi-Client: studio; the backend uses the effectiveappAccessto gate these requests. - An API key may never use Studio even if the policy grants app access.
enforceTfa=truerequires the user to have enrolled and the session to have passed 2FA before a Studio request can be used.- Do not set these flags on the user; the user only holds identity/status/TFA enrollment.
6. Composition & precedence
- Merging multiple permissions with the same
(collection, action):fields: union (special case: if a blacklist appears, apply it after the union).permissionsrule: joined with_or(additive grants).validation/presets: merged with_and/ object spread bypriority.
- A role with
adminAccess=true→ bypass.
New recommendation: do not silently OR/union when attaching policies via Studio. The backend needs a conflict checker:
- Block if one policy grants
{}but another restricts the same collection/action. - Block if one policy grants
["*"]but another whitelists fields. - Block if
validationorpresetscover the same field with different values. - Warn if it only widens fields or OR-adds a conditional rule; the admin must confirm and it is audited.
- The DB should have a unique
(policyId, collection, action)so a policy cannot have multiple permission rows with the same key.
7. API
GET /permissions/me— returns the matrix{collection: { create, read, update, delete, share, fields, presets }}so Studio can render the UI (hide buttons, disable fields).POST /permissions/check— debug: input action+payload, output allow/deny + reason trace.
8. Studio UI
- The Access Control module:
- Roles page: list + create + assign users + attach policies.
- Policies page: list + JSON editor + GUI builder (a form per row).
- Permission Matrix page: a
collection × actiongrid; click a cell to open the detail (fields, rules, presets, validation). - Test sandbox page: simulate a user → see the field mask, allowed rows.
- API Keys page: create a key, rotate/revoke, attach roles/policies, preview effective permissions.
- Role/Key attach flow: preview conflicts before saving.
8.1. Import / Export
GET /access/exportexports roles, policies, permission rows, role-policy bindings, and API key metadata as versioned JSON.POST /access/import?dryRun=truereturns a diff/conflict and does not write the DB.- A real import runs in a transaction, uses stable keys, is fully audited, and never imports/exports a plaintext API key.
9. Caching & invalidation
- KV:
perm:{site}:{role}(compiled). TTL 5 minutes + invalidate when a policy/role changes. - WebSocket broadcasts a
permissions.changedevent → the Studio client reloads/permissions/me.
10. Audit
- Every denial is logged to
activitywith the actionpermission_denied+ the reason (rule path).
11. Tasks: Phase MVP-C, C2.
12. Compliance & user rights
This RBAC/audit machinery underpins several regulatory obligations (access control, provenance, breach detection). For how it maps to user data-subject rights and where gaps remain, see Compliance — gap analysis.