back to docs
[reference] security permissions roles folders row-level-security

Security Model

How dForge controls access: composable security across folders, entity views, and roles. Folder-scoped roles, additive rights, row-level filters, and column-level visibility.

published

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:

CodeOperation
SSelect / Read
IInsert / Create
UUpdate
DDelete
CClone / Copy

Actions, Reports, Folders support a single binary right:

CodeOperation
EExecute / 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:

  • Globalfolder_id = NULL. The role applies in every folder where the module’s entities appear.
  • Folder-scopedfolder_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:

LayerSourcePurpose
1. Folder filterfolder_entity.row_filterFolder context (e.g., warehouse, division)
2. View filterdata_view.view_json.filterView-specific subset (e.g., amount > 1000)
3. Security filterRow-level security rulesAccess control (e.g., own records only)
4. User ad-hoc filterUI filter barTemporary 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:

  1. The user has the E right on the action’s sec_object (via role or direct grant)
  2. The action’s canExecute formula evaluates to true for the target record(s)
  3. The user has the required entity rights for the writes the action performs
  4. 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:

  1. Module access
  2. User-specific folder grants
  3. Role-based folder grants
  4. Folder-level field overrides
  5. Entity-level row rules
  6. Field-level flags
  7. Action-level canExecute formulas

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.

DatabaseTablesPurpose
dforge_authuser, tenant, user_tenant, sessionGlobal 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 schemasPer-tenant security and data

See Administration for the day-to-day workflow of managing users, roles, and folders.

/ was this helpful?

Stuck on something?
Tell us.

We read every message and update the docs based on what readers ask. The fastest way to improve the docs is to write to us.