Three Independent Dimensions
Effective access in dForge is the intersection of three independent dimensions:
Access = Folder (rows) × Entity View (columns) × Role (operations)
- Folder decides which rows the user sees (row-level security via filter)
- Entity view decides which columns the user sees (column-level security)
- Role decides what operations the user can perform on what they see
Each dimension is configured independently. The same product table can show stock columns in one folder, pricing columns in another, and only the columns the user’s role permits to write — without ever duplicating data.
Rights Model
Letter Codes
Rights are stored as letter codes on security objects (sec_object).
Entities (object_type = 'E') support full CRUD + Clone:
| Code | Operation |
|---|---|
S | Select / Read |
I | Insert / Create |
U | Update |
D | Delete |
C | Clone / Copy |
Actions, Reports, Folders support a single binary right:
| Code | Operation |
|---|---|
E | Execute / Access |
Additive
Rights from multiple roles are merged (union), never revoked. There is no “deny” rule.
Role A grants: SU
Role B grants: SIC
Effective: SIUC
If no role grants a permission, it is denied. Users start with no access until roles are assigned (least privilege).
Roles
Module Roles
Roles are defined by module developers, not platform admins. They are namespaced to their module — for example, crm.sales_rep, hr.manager, wms.storekeeper. There are no built-in generic platform roles.
Each role declares which entities, actions, reports, and folders it grants rights on. When you install a module, its roles become available for assignment to users.
Role Assignments
A role assignment links a user to a role and is stored in user_role. The assignment can be:
- Global —
folder_id = NULL. The role applies in every folder where the module’s entities appear. - Folder-scoped —
folder_id = <uuid>. The role applies only in that folder (and its children if inheritance is enabled).
This is how a single user can be hr.manager for the company at large but only hr.viewer inside HR/Executives.
Direct User Rights
For one-off exceptions, admins can grant rights directly to a user on a security object via user_rights. This bypasses roles entirely. Use sparingly — most policy should live in roles.
Folders Are Filtered Views
Folders are not containers. They define a filter that determines which records are visible inside them:
-- folder_entity row
folder_id = "draft_invoices"
entity_id = "invoice"
row_filter = { "status": "draft" }
-- effective query when the user opens the folder
SELECT * FROM accounting.invoice
WHERE status = 'draft'
AND <user permissions>
A record can appear in many folders at once. Changing a record’s data can make it appear or disappear from folders automatically. There is no folder_path column on data tables.
Composing Filters
Every query is filtered by four layers ANDed at the database:
| Layer | Source | Purpose |
|---|---|---|
| 1. Folder filter | folder_entity.row_filter | Folder context (e.g., warehouse, division) |
| 2. View filter | data_view.view_json.filter | View-specific subset (e.g., amount > 1000) |
| 3. Security filter | Row-level security rules | Access control (e.g., own records only) |
| 4. User ad-hoc filter | UI filter bar | Temporary filters added at runtime |
Column-Level Security via Entity Views
An entity view lists which columns of an entity are accessible. Anything not listed is hidden — the user can’t query it, sort by it, or even know it exists.
Each folder binds its entities to a specific entity view. The same product table can be exposed two completely different ways:
Warehouse folder → entity_view = storekeeper_view
columns: name, sku, quantity, location, min_stock
Accounting folder → entity_view = accountant_view
columns: name, sku, price, cost, margin
Storekeepers and accountants share the table but cannot see each other’s columns.
Row-Level Security Formulas
Row-level security rules are formulas evaluated server-side at query time. They become additional WHERE clauses on every query the user runs.
-- Users only see records they created
[created_by] = CURRENT_USER_ID()
-- Department-scoped via folder setting
[department_id] = $[DepartmentId]
-- Combine with folder filter
[status] = 'active' AND [region] = $[UserRegion]
Settings referenced via $[SettingName] resolve from the current folder, walking up the parent chain. This is how the same role can scope data differently in different folders without writing per-folder rules.
Action Permissions
An action is executable for a user only when all of the following are true:
- The user has the
Eright on the action’ssec_object(via role or direct grant) - The action’s
canExecuteformula evaluates totruefor the target record(s) - The user has the required entity rights for the writes the action performs
- Folder column overrides do not forbid the columns the action touches
canExecute is evaluated on both the client (for instant button enable/disable) and the server (security re-check before execution). The client’s result is never trusted on its own.
For multi-record actions, every selected record must satisfy canExecute — if even one fails, the button is disabled.
Greyed-Out Folders
If a user has access to a sub-folder but not its parent, the parent is shown in the sidebar tree as non-selectable (greyed out, no click action). They can navigate down to the sub-folder they own without ever seeing parent data. This keeps the navigation tree consistent without exposing forbidden data.
Permission Evaluation Order
For every operation, the platform composes effective permissions from these layers in order:
- Module access
- User-specific folder grants
- Role-based folder grants
- Folder-level field overrides
- Entity-level row rules
- Field-level flags
- Action-level
canExecuteformulas
The final permission is the result of all layers combined. Permissions are computed dynamically per request from metadata and the current execution context — they are not encoded in the JWT token.
Auth Database Separation
User accounts and tenant access live in a separate auth database. Tenant data lives in per-tenant databases. Each tenant has its own database user with permissions limited to its own database, so a compromise of one tenant cannot expose another tenant’s data.
| Database | Tables | Purpose |
|---|---|---|
dforge_auth | user, tenant, user_tenant, session | Global identity and tenant access grants |
dforge_<tenant> | module_role, sec_object, user_role, user_rights, folder, folder_entity, entity_view, entity_v_column, plus all module schemas | Per-tenant security and data |
See Administration for the day-to-day workflow of managing users, roles, and folders.