Appearance
Permission screens reference
The Studio Explorer folder Security > Roles & Permissions contains seventeen screens. Fourteen of them author one kind of rule (a role, a permission set, a grant on one kind of resource, a field rule, a row filter, an approval limit, an API rule, a template or a time-boxed grant), Access Policies authors attribute-based policies, and two screens (Effective Permission Viewer and Permission Audit Log) read the result. This page documents every screen: its fields, the exact resource strings it writes, where the rule is enforced, the REST calls behind it and the messages it can return. How a decision is reached when several rules apply is described in Permissions - how an access decision is made; read that page first.
Behaviour shared by all screens
Permission matrix editor. Screens that grant or deny an action on a list of resources (Screen, Widget, Menu, Report, Print, Workflow, and Field permissions) share one editor.
| Element | Behaviour |
|---|---|
| Mode toggle By role / By resource | By role: pick a role in the left list (a badge shows how many cells are set for it), then see every resource with its action chips. By resource: pick one item with the Item search box, then see every role. |
| Action chip | One chip per action. Selecting the chip cycles not set (grey outline), allow (green), deny (red), then not set again. Each click is saved immediately through PUT or DELETE /api/v1/role-permissions. |
| Bullet in a chip label | A chip labelled with a bullet (for example "Visible •") is an action that a server or runtime checkpoint consults today. A chip without a bullet is stored but has no consumer yet. |
| Filter All / Granted / Denied / Not set | Narrows the rows shown. "Granted" means at least one action is allow for that row; "Denied" at least one action is deny. |
| Search box | "Search items" (by role) or "Search roles" (by resource). Matches the label and the resource key. |
| Show N more (M left) | The list is rendered 60 rows at a time. |
| Bulk bar Action, Allow N, Deny N, Clear N | Applies one action to every row currently shown (after search and filter). A confirmation dialog titled "Allow Visible?" (or Deny, Clear) states "Allow "Visible" on 12 items for HR Assistant." and offers Cancel and Allow 12. If every row already has the value the dialog reads "Everything shown already has this setting." and the confirm button is disabled. Field Permissions has no bulk bar. |
| History icon beside each chip | Opens the version history of that cell (see below). |
Version history dialog. Every create, change and delete of a role, role grant, permission set, user override, temporary permission, field rule, API rule and approval limit is stored as a numbered version. The dialog titled "History" followed by the item name lists Version (v1, v2, ...), Action (create, update, delete, restore), By and When. Compare from and Compare to select two versions and Compare lists the fields that differ as field: before -> after (or "No differences - the two versions have identical field values."). Restore re-applies the stored state live and records a new restore version; history is never rewritten. Restore requires the PUBLISH capability on artifact type role; without it the button is disabled and shows the denial reason as its tooltip. Endpoints: GET /api/v1/permission-versions?entityType=...&entityKey=..., GET /api/v1/permission-versions/{id}, POST /api/v1/permission-versions/{id}/restore.
entityType | entityKey format |
|---|---|
role | role id, for example 7 |
role_permission | roleId|resource|action|orgUnitId with * when tenant-wide, for example 7|page:leave-settings|view|* |
permission_set | permission set id |
user_permission | actor|resource|action|orgUnitId with * when tenant-wide |
temporary_permission | temporary permission id |
field_permission | roleId|entity|fieldName|action |
api_permission | roleId|routePattern|httpMethod |
approval_permission | roleId|object |
Record scope rules and named ACL templates are not versioned.
Authoring gate. Every create, update, delete, set and clear on the screens below is checked with AuthoringPolicy.require(tenant, actor, AUTHOR, "role"). Version restore, and publishing, activating, deactivating, archiving and deleting an access policy, require PUBLISH. Reads are open. The policy bean is replaceable; the bundled default allows every actor. A denial returns HTTP 403 with code capability-denied.
Request headers. X-Tenant-Id selects the workspace. X-Actor names the caller; when absent it is anonymous.
Error format. Authoring endpoints return {"code": ..., "message": ..., "detail": {...}}.
| HTTP status | code | Meaning |
|---|---|---|
| 400 | invalid-definition | A validation rule failed. The messages are listed per screen. |
| 403 | capability-denied | The caller lacks AUTHOR or PUBLISH. detail names the capability. |
| 404 | not-found | The role, set, template or row id does not exist for this tenant. |
| 409 | lifecycle-violation | The operation is not allowed in the current state (for example editing a read-only role). |
CLI. The erp CLI has no dedicated permission commands. Use erp api get|post|put|delete <path> --tenant <id> --body <json-or-@file> against the endpoints listed per screen, after erp login. See Sign in and environments.
Roles
A role is a named group of people. Grants are made to roles, and people receive roles on the Users screen. Roles form a tree through a parent. Administrators of the workspace use this screen to maintain the catalog; implementers use it before any permission screen, because every grid below needs at least one role.
Where to find it. Security > Roles & Permissions > Roles. Page key roles.
Key concepts.
| Term | Definition |
|---|---|
| Name | Display label. Unique per workspace (compared after trimming). It can be renamed. |
| Code | Stable slug of the role. Assigned when the role is created, unique per workspace, never changed by an edit. Role assignments and approval limits are matched by code, so renaming a role does not break them. Not shown on this screen. |
| Parent role | Optional. A person holding a role is evaluated against that role and all of its ancestors, so a deny on a parent applies to every child. A child can never gain access that the parent is denied. |
| Priority | Integer, default 0. Orders siblings in the tree (higher first, then by name). It also decides which role-scoped password policy override wins for a person with several roles. It does not change permission precedence. |
| Active | Stored flag, default on. The permission resolver does not read it in this release; a role marked inactive still contributes its grants. |
| Plugin-owned role | A role installed by a plugin (for example an HR administrator role). It is read-only; create a role that extends it instead. |
Fields and options.
New role panel:
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| New role name | Text | empty | Yes | Helper text e.g. "HR Manager". New role stays disabled until the trimmed name is not empty. |
| Parent role | Select | (none - top level) | No | Lists every existing role. |
Role grid (Role, Priority, Status). Rows are shown as a tree: indentation equals depth, ordered by priority descending then name within each parent. The subtitle is the description. Status shows active or inactive and is filterable. Search box: "Search roles". Empty state: "No roles yet" with "Create one above."
Expanded row (select a row):
| Field | Type or allowed values | Default | Required | Description |
|---|---|---|---|---|
| Name | Text | current | Yes | Save is disabled while empty. |
| Description | Text | current | No | Shown as the grid subtitle. |
| Parent role | Select | current | No | Excludes the role itself. Choosing "(none - top level)" clears the parent. |
| Priority | Number | current | No | A non-numeric entry is saved as 0. |
| Active | Checkbox | checked | No | See Key concepts. |
Fields available only through the API: effectiveAt and expiryAt (timestamps stored on the role; not evaluated by the resolver) and extendsRoleId (the id of a role this one extends; existence and tenant are checked, nothing else).
Actions.
| Action | Effect | API |
|---|---|---|
| New role | Creates a role | POST /api/v1/roles |
| Save (expanded row) | Replaces name, description, parent, priority and active | PUT /api/v1/roles/{id} |
| History icon | Opens the version history, entity type role | GET /api/v1/permission-versions |
| Delete icon | Opens a dialog titled Delete followed by the role name in quotes and a question mark, with the text "This permanently removes the role - there is no undo. A role with children cannot be deleted." Cancel and Delete. A server error is shown inside the dialog. | DELETE /api/v1/roles/{id} |
Procedure. Create a manager role under an employee role.
- In New role name enter
Leave Manager. - In Parent role select
Employee. - Select New role. The role appears indented below
Employeewith statusactive. - Expand the row, set Priority to
10and select Save.
Statuses and lifecycle. A role has no lifecycle. active is a stored flag only.
Permissions. Mutations require AUTHOR on artifact type role. Reads are open.
API.
| Method and path | Purpose |
|---|---|
GET /api/v1/roles | All roles of the tenant (not paginated). |
GET /api/v1/roles/{id} | One role. |
POST /api/v1/roles | Create. Returns 201. |
PUT /api/v1/roles/{id} | Update. Every field is replaced, so an omitted parentRoleId clears the parent. |
DELETE /api/v1/roles/{id} | Delete. Returns 204. |
json
{ "name": "Leave Manager", "description": "Approves leave of own team", "parentRoleId": 3, "priority": 10, "active": true }bash
erp api post /api/v1/roles --tenant 2 --body '{"name":"Leave Manager","parentRoleId":3,"priority":10}'Every create, update and delete is versioned and recorded in the Permission Audit Log as entity type role.
Errors.
| Message | Cause | Fix |
|---|---|---|
| Role name is required | Blank name | Enter a name. |
| A role named "Leave Manager" already exists | Duplicate name | Choose another name. |
| A role cannot be its own parent | Parent equals the role | Select another parent. |
| Parent role 99 does not exist | Unknown parent id | Select an existing role. |
| Setting this parent would create a role hierarchy cycle | The chosen parent is a descendant | Select a role outside the subtree. |
| Cannot delete a role that has child roles - reassign or delete them first | Role has children | Re-parent or delete the children. |
| Role "HR Administrator" is owned by plugin "hcm-foundation" and is read-only - create a role that extends it instead of editing it directly | Editing or deleting a plugin-owned role (HTTP 409) | Create a new role with extendsRoleId. |
Related. Permission Sets, Users, Add app-owned roles and permissions.
Permission Sets
A permission set is a named list of (resource, action, effect) items that is copied into the grants of a role in one step. Use it when several roles need the same list, for example "HR Operations".
Where to find it. Security > Roles & Permissions > Permission Sets. Page key permission-sets.
Key concepts.
| Term | Definition |
|---|---|
| Item | One (resource, action, effect). A set holds at most one item per (resource, action); adding the same pair again replaces its effect. |
| Attach | Copies every item of the set into the role's own grants, tagged with the set id, at the chosen scope. It is a copy, not a live link: items added to the set afterwards do not reach roles already attached; attach again to refresh them. Attaching twice is idempotent. |
| Detach | Removes only the grants that this set contributed to the role at exactly the chosen scope. A grant later edited directly in a matrix screen (which clears its tag) and grants from the same set attached at another scope are kept. |
| Scope | Org unit of the attached grants. "(Global - no scope)" attaches tenant-wide. A grant scoped to an org unit applies to people assigned to that unit or any unit below it. |
Fields and options.
| Field | Type or allowed values | Default | Required | Description |
|---|---|---|---|---|
| New set name | Text | empty | Yes | Helper text e.g. "HR Operations". Unique per workspace. |
| Resource (expanded row) | Select: employee, org_unit, job_classification, position | employee | Yes | The Studio list is fixed. The API accepts any non-empty resource string, for example page:payroll-run. |
| Action | Select: create, read, update, delete, view, approve, reject, cancel, submit, clone, archive, restore, print, export, import, upload, download, share, assign, execute | create | Yes | Any non-empty action is accepted by the API. |
| Effect | Select: allow, deny | allow | Yes | |
| Role | Select "(choose a role)" | none | For attach and detach | |
| Scope | Org unit select | (Global - no scope) | No |
Items are shown as chips resource.action: effect (green allow, red deny); the chip's delete control removes the item. The description of a set can be set only through the API.
Actions.
| Action | Effect | API |
|---|---|---|
| New set | Creates the set | POST /api/v1/permission-sets body {name, description} |
| Add item | Adds or replaces an item | PUT /api/v1/permission-sets/{id}/items body {resource, action, effect} |
| Chip delete | Removes the item | DELETE /api/v1/permission-sets/{id}/items body {resource, action} |
| Attach to role | Copies the items into the role | POST /api/v1/permission-sets/{id}/attach/{roleId}?orgUnitId=... |
| Detach from role | Removes this set's grants from the role | POST /api/v1/permission-sets/{id}/detach/{roleId}?orgUnitId=... |
| History icon | Version history, entity type permission_set | |
| Delete icon | Deletes the set and its items (no confirmation) | DELETE /api/v1/permission-sets/{id} |
| Rename | API only | PUT /api/v1/permission-sets/{id} body {name, description} |
Procedure. Give the role "Payroll Viewer" read access to payslip data in one step.
- Create the set
Payroll Read. - Expand it. Add
employee/read/allow, thenposition/read/allow. - Select the role
Payroll Viewerin Role, leave Scope on "(Global - no scope)" and select Attach to role. - Open Roles > the matrix screens or the Effective Permission Viewer for a member of the role and confirm
employee.readresolves to allowed by layer "Role".
Permissions. AUTHOR on role for every mutation.
Limits and behaviour. Detach the set from every role before deleting it: a role grant that still references the set blocks the delete at the database and the call fails with a server error. Set create, update, delete and item changes are versioned and appear in the Permission Audit Log as entity type permission_set. Attach and detach are neither versioned nor audited as separate events; the resulting grants are ordinary role grants.
Errors.
| Message | Cause | Fix |
|---|---|---|
| Permission set name is required | Blank name | Enter a name. |
| A permission set named "Payroll Read" already exists | Duplicate name | Rename. |
| resource is required, action is required | Blank item field (API) | Supply both. |
| effect must be one of [allow, deny] | Other effect (API) | Use allow or deny. |
| No permission set 12 for tenant 2 | Unknown id (404) | Reload the list. |
| No role 9 for tenant 2 | Attach to an unknown role (404) | Select an existing role. |
Related. Roles, Temporary Permissions.
Field Permissions
Field permissions control, per role, whether one field of an entity may be changed or must be filled when a record is saved. Implementers use it for fields such as salary, medical notes or approval comments.
Where to find it. Security > Roles & Permissions > Field Permissions. Page key field-permissions.
Key concepts.
| Term | Definition |
|---|---|
| Entity | The business entity whose fields are listed. The Studio selector offers employee, org_unit, job_classification and position; the API accepts any entity name. |
| Field | A field name declared by at least one published form of the entity. The matrix columns are exactly these names. |
| Action | view, edit, require or mask (labelled Visible, Editable, Required, Masked). |
Effect of each cell.
| Chip | Effect set | Result on save for a person whose roles (including parents) carry it |
|---|---|---|
| Visible • | deny | The field is treated as hidden: a save that changes its value is rejected. |
| Visible • | allow | No effect. |
| Editable • | deny | The field is read-only: a save that changes its value is rejected. |
| Editable • | allow | No effect. |
| Required • | allow | The field is required: a save with an empty value is rejected. |
| Required • | deny | No effect. |
| Masked | either | Stored only. No component reads it in this release. |
If a person has several roles, the most restrictive result applies: any role that hides, locks or requires the field is enough. Field rules read roles only; user overrides, temporary permissions and org scope do not apply to them. A person with no roles is unrestricted. On create, any non-empty value counts as a change; on update the new value is compared with the stored one, so re-submitting the existing value is accepted. The same checks run for the older inline roleRules of a form field.
Fields and options.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| Entity | Select | employee | Yes | Changing it reloads roles, fields and rules. |
| Matrix | See Behaviour shared by all screens | The history icon uses entity type field_permission. There is no bulk bar. |
Empty states: "No roles yet - create one in Roles first." and "No published form declares any field for this entity yet."
Actions.
| Action | API |
|---|---|
| Load rules | GET /api/v1/field-permissions?entity=employee |
| Load fields | GET /api/v1/field-permissions/fields?entity=employee |
| Set a cell | PUT /api/v1/field-permissions body {roleId, entity, fieldName, action, effect} |
| Clear a cell | DELETE /api/v1/field-permissions body {roleId, entity, fieldName, action} |
Procedure. Make salary read-only for HR Assistant.
- Select entity
employee. - By role: select
HR Assistant, searchsalary. - Select the Editable chip twice: not set, allow, deny. The chip turns red.
- An HR Assistant who submits an employee record with a changed
salaryreceives the field errorsalary is read-only for your role.
Permissions. AUTHOR on role. The rules are applied by the record-save validator on the server. Document management applies the same rules to document metadata with entity name dms.document.<documentTypeCode>.
Limits and behaviour. Rules are not cached. Changes are versioned but not written to the Permission Audit Log. A field rule does not remove the field from read responses; hiding on screen is a function of the form or page.
Errors.
| Message | Cause | Fix |
|---|---|---|
salary is read-only for your role. (field error roleReadOnly) | A role hides or locks the field and the value changed | Remove the deny or save without changing the value. |
salary is required for your role. (field error roleRequired) | A role requires the field and it is empty | Fill the field. |
| entity is required, fieldName is required | Blank value (API) | Supply both. |
| action must be one of [view, edit, require, mask] | Other action (API) | Use a listed action. |
| effect must be one of [allow, deny] | Other effect (API) | Use allow or deny. |
Related. Record Permissions, Roles.
Record Permissions
Record permissions (the screen is titled "Data Scope Rules") restrict which rows of an entity a role may see. A rule is a boolean expression evaluated against each row and against the signed-in person's organisational context.
Where to find it. Security > Roles & Permissions > Record Permissions. Page key data-scope-rules.
Key concepts.
| Term | Definition |
|---|---|
| Rule | One expression for one (role, entity). Several rules of a role are alternatives: a row is visible if any rule is true. |
| Row context | The row's own fields under the capitalised entity name, for example Employee.Department or Document.region. |
| Person context | CurrentUser.<UnitType> is the id of the nearest org unit of that type at or above the person's own org unit assignment. Unit types: Company, BusinessUnit, Department, Location, CostCenter. A person with no assignment has an empty context, so comparisons do not match. |
| Unrestricted | A role with no rule for the entity sees every row. If a person holds several roles and any one has no rule, that person sees every row. A person with no roles is unrestricted. |
Fields and options.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| Entity | Select: employee | employee | Yes | The Studio selector lists one preset. The expression for another entity is authored through the API. |
| Role | Select | empty | Yes | |
| Expression | Text | empty | Yes | Placeholder Employee.Department == CurrentUser.Department. Helper text "Compares the row's own fields against the actor's resolved org context". |
| Description | Text | empty | No | Placeholder "Own department only". |
Add rule is disabled until a role is chosen and the expression is not empty. The grid shows Role, Expression, Description; the delete icon removes a rule. Empty state: "No data scope rules configured for "employee" yet - every role sees every row."
Actions.
| Action | API |
|---|---|
| List | GET /api/v1/record-scope-rules?entity=employee |
| Add rule | POST /api/v1/record-scope-rules body {roleId, entity, expression, description}, returns 201 |
| Delete rule | DELETE /api/v1/record-scope-rules/{id} |
Procedure. Limit Department Manager to employees of their own department.
- Entity
employee, roleDepartment Manager. - Expression
Employee.Department == CurrentUser.Department, descriptionOwn department only. - Select Add rule. The rule appears in the grid.
- Sign in as a department manager: the employee directory returns only employees whose department id equals the manager's department id.
Where it is enforced. Entity employee in the HCM employee list, query and organisation chart endpoints; entities document and file in document management (the metadata fields of the document form the row context). Other entities accept rules but nothing consults them yet.
Limits and behaviour. The expression is checked for syntax only when saved. A rule that parses but fails when evaluated (for example a misspelt field) excludes the row and logs a warning, so the safe failure is less visibility. Filtering happens in the application after the rows are fetched and pagination follows the filtered list. Rule changes are neither versioned nor audited.
Errors.
| Message | Cause | Fix |
|---|---|---|
| Invalid expression: ... | The expression does not parse | Correct the syntax. |
| entity is required, expression is required | Blank value (API) | Supply both. |
| No role 9 for tenant 2 | Unknown role (404) | Select an existing role. |
Related. Field Permissions, Access Policies.
Screen Permissions
Screen permissions decide which roles may open a page. Authors of applications use them to close administrative pages to ordinary users.
Where to find it. Security > Roles & Permissions > Screen Permissions. Page key screen-permissions.
| Item | Value |
|---|---|
| Resource string | page:<page name> where the name is the stable page name, not a version id. A new publish of the page keeps its grants. |
| Action | view (chip "View •", enforced) |
| Rows | Every distinct page name in the workspace, sorted alphabetically. |
Matrix and bulk behaviour are described under Behaviour shared by all screens. History key roleId|page:<name>|view|*. Empty states: "No roles yet - create one in Roles first." and "No Page artifacts yet - create one in Pages first."
Enforcement. When a page definition is requested, the server checks page:<name> / view through the permission resolver. A denied person receives HTTP 403 with code screen-access-denied and the message Actor "alice" lacks Screen access to page "leave-settings" for tenant 2; the tenant runtime host treats the route as not found, so the page is not revealed to the caller. This protects the page definition itself, not the data APIs the page calls.
Procedure. Close the page leave-settings to everyone except HR Admin.
- By resource: select
leave-settings. - Set View to deny for each broad role (for example
Employee,Manager). - Leave
HR Adminnot set. Because an unset cell allows, no grant is needed. - Check with the Effective Permission Viewer: actor, resource
page:leave-settings, actionview.
Permissions and API. AUTHOR on role. GET /api/v1/role-permissions?resources=page:leave-settings lists the cells; PUT and DELETE /api/v1/role-permissions body {roleId, resource, action, effect, orgUnitId} set and clear. The Studio matrices always write tenant-wide cells (orgUnitId null); org-scoped cells are written through the API or by attaching a permission set with a scope.
bash
erp api put /api/v1/role-permissions --tenant 2 --body '{"roleId":7,"resource":"page:leave-settings","action":"view","effect":"deny"}'Errors. screen-access-denied (403) as above; resource, action and effect validation messages are the same as for Roles grants: "resource is required", "action is required", "effect must be one of [allow, deny]", "No role 7 for tenant 2".
Widget Permissions
Widget permissions control how a block type is rendered for a role. The resource list is the catalog of core block types (for example core.text-input), sorted alphabetically.
Where to find it. Security > Roles & Permissions > Widget Permissions. Page key widget-permissions.
| Item | Value |
|---|---|
| Resource string | widget:<block type>, for example widget:core.text-input |
| Actions | visible, enabled, masked, all enforced (bullet) |
| Visible | Deny hides every block of that type. Unset or allow shows it. |
| Enabled | Deny renders the block disabled. |
| Masked | An explicit allow renders the block masked; deny or not set leaves it unmasked. Because "not set" resolves to allowed everywhere else, the runtime treats a result that was not decided by an explicit rule as "not masked". |
The runtime host asks POST /api/v1/permission-checks once per mount for every block type and action, so a change takes effect at the next mount of the page host, not during a session. If the request fails the page renders without widget restrictions. The checkpoint hides or disables controls; it does not protect the data behind them (see Record Permissions, Field Permissions and API Permissions).
History key roleId|widget:<type>|<action>|*. Authoring gate and endpoints as in Screen Permissions.
Procedure. Mask all number inputs for Intern.
- By role: select
Intern, searchnumber. - Set Masked on
core.number-inputto allow. - Reload a page that contains a number input as an intern: the value is replaced by a masked placeholder.
Menu Permissions
Menu permissions hide navigation entries for a role.
Where to find it. Security > Roles & Permissions > Menu Permissions. Page key menu-permissions.
| Item | Value |
|---|---|
| Menu selector | Select Menu: every menu that has a published version. Empty state "No published Menu artifacts yet - publish one in Menus first." and, for a menu without nodes, "This menu has no nodes yet." |
| Resource string | menu:<menu name>:<node id> for every node of the latest published version; child nodes are indented with dashes in the list |
| Action | view (enforced) |
The navigation block fetches the grants with POST /api/v1/permission-checks and removes a node whose result is denied. It is combined with, not replaced by, the role targeting configured on the node in the Menu Designer: either one can hide the node. Hiding a node does not close the page; use Screen Permissions for that. A node id that has no grant is unset, which allows it, so a node added in a later menu version is visible to every role until you set it.
History key roleId|menu:<name>:<node id>|view|*.
Report Permissions
Where to find it. Security > Roles & Permissions > Report Permissions. Page key report-permissions.
| Item | Value |
|---|---|
| Resource string | report:<report name> |
| Actions | view, export, email, print |
| Enforcement | None. The page states "Captured as real data - no report execution endpoint exists yet to enforce any action here." No chip carries a bullet. |
The grid lets an administrator record the intended matrix now; the rules take effect only if a future report execution checkpoint reads them. Do not rely on this screen for protection. Printing a print template is protected separately by Print Permissions. Empty state "No Report artifacts yet - create one in Reports first."
Print Permissions
Print permissions decide who may produce a document from a print template.
Where to find it. Security > Roles & Permissions > Print Permissions. Page key print-permissions.
| Item | Value |
|---|---|
| Resource string | print_template:<template name> |
export (enforced) | PDF, Word, Excel and thermal file downloads. |
print (enforced) | Sending the document to a networked printer only. It is granted separately, so a clerk can print invoices without downloading raw exports. |
| Rows | Every print template name. Empty state "No Print Template artifacts yet - create one in Print Templates first." |
The five export and print endpoints check the grant before producing output. A denied caller receives HTTP 403, code print-access-denied, message Actor "alice" lacks export access to print template "invoice" for tenant 2. The previous behaviour (requiring drafting rights to print) no longer applies.
History key roleId|print_template:<name>|<action>|*.
Procedure. Allow Cashier to print but not download invoices.
- By resource: select
invoice. - For
Cashierset Print to allow and Export to deny. - As a cashier, a PDF download returns
print-access-denied; sending to the receipt printer succeeds.
Workflow Permissions
Workflow permissions control who may start, act in and administer a workflow.
Where to find it. Security > Roles & Permissions > Workflow Permissions. Page key workflow-permissions.
| Item | Value |
|---|---|
| Resource string | workflow:<workflow name>. Rows are the names of published workflows ("No published workflows yet." when none). |
| Actions in the grid | submit, approve, reject, return, recall, reopen, cancel, escalate, delegate |
| Enforced (bullet) | submit, approve, reject, return, delegate |
| Stored only | recall, reopen, cancel, escalate (no engine transition checks them; escalate is a timer action with no person to check) |
Additional actions checked on the same resource but not listed in the grid: manage (webhook administration of the workflow) and migrate (moving running instances to a new version). Grant them with the API or Permission Sets.
| Checkpoint | Action |
|---|---|
Start an instance (POST /api/v1/workflows/instances, POST /api/v1/workflows/{name}/start) and insert an ad hoc step | submit |
| Record a decision on a human task | approve, reject or return according to the outcome |
| Delegate or reassign a task | delegate |
| Workflow webhooks | manage |
| Version migration of a run | migrate |
Being a candidate approver of a task is necessary but not sufficient: the role grant must also not deny the action. A refusal returns HTTP 403, code workflow-action-not-permitted, message Actor "bob" is not permitted to "approve" workflow "leave-request" (tenant 2). History key roleId|workflow:<name>|<action>|*. See How workflows work.
Procedure. Only Leave Manager may approve leave-request.
- By resource: select
leave-request. - For
Employeeset Approve and Reject to deny. - A person who holds both roles is still denied, because any deny wins; remove the
Employeerole from managers or grant through a user override.
Approval Permissions
Approval permissions answer "who may approve how much". They are a separate mechanism from the matrix screens: a tier is a numeric ceiling per role and approval object.
Where to find it. Security > Roles & Permissions > Approval Permissions. Page key approval-permissions.
Fields and options.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| Role | Select | empty | Yes | |
| Approval Object | Text | empty | Yes | Placeholder expense-approval. Free text; it must equal the approvalObject value of the workflow task. |
| Max Amount | Number | blank (placeholder "unlimited") | No | Blank means no ceiling. Must not be negative. |
Add tier is disabled until a role and an object are entered. Adding for an existing (role, object) replaces its ceiling. Grid: Role, Approval object, Max amount ("Unlimited" or the number; sorted with unlimited last). Icons: history (entity type approval_permission) and remove. Empty state "No approval tiers configured yet."
Resolution. A workflow human task whose payload carries approvalObject (and amount, default 0) takes its candidate approvers from this table instead of a static candidateApprovers list.
- Keep every role of that object whose ceiling is at least the amount, or unlimited.
- If any finite ceiling remains, keep the roles with the lowest finite ceiling. Roles sharing that lowest ceiling are all kept.
- Otherwise keep the unlimited roles.
- The candidates are the people directly assigned the kept roles (matched by role code). People who hold only a child role are not candidates.
- If nothing qualifies the task has no candidates.
| Role | Ceiling for expense-approval |
|---|---|
| Team Lead | 500 |
| Manager | 5000 |
| Director | unlimited |
A request of 400 goes to Team Lead, 1200 to Manager, 9000 to Director.
API. GET /api/v1/approval-permissions?objects=expense-approval, PUT /api/v1/approval-permissions body {roleId, object, maxAmount}, DELETE /api/v1/approval-permissions body {roleId, object}. These endpoints are served by the workflow service and are gated by AUTHOR on role.
bash
erp api put /api/v1/approval-permissions --tenant 2 --body '{"roleId":5,"object":"expense-approval","maxAmount":5000}'Errors. "object must not be blank"; "maxAmount must not be negative".
API Permissions
API permissions restrict, rate-limit and IP-filter calls to /api/v1/** per role.
Where to find it. Security > Roles & Permissions > API Permissions. Page key api-permissions.
Fields and options.
| Field | Type or allowed values | Default | Required | Description |
|---|---|---|---|---|
| Role | Select | empty | Yes | |
| Route Pattern | Text | empty | Yes | Ant-style pattern, placeholder /api/v1/hcm/**. * matches within one path segment, ** across segments, ? one character. |
| Method | Select: *, GET, POST, PUT, PATCH, DELETE | GET | Yes | * matches every method. |
| Effect | Select: allow, deny | allow | Yes | |
| Rate Limit /min | Positive integer | blank (unlimited) | No | Calls per minute for this actor under this rule. |
| IP Allowlist | Text | blank (unrestricted) | No | Comma-separated addresses. |
Grid: Role, Route pattern, Method, Effect, Rate limit, IP allowlist with history and remove icons. A rule is identified by (role, route pattern, method); saving the same triple again replaces it. The API also stores oauthScope; the Studio form does not expose it and the resolver does not evaluate it.
Resolution. For every /api/v1/** request that carries X-Tenant-Id:
- The caller's roles, including ancestors, select the candidate rules. A caller with no roles is unrestricted.
- Rules whose pattern matches the request path and whose method is
*or equals the request method are kept. No match means unrestricted. - Any matching
denyrejects the request, regardless of other allows. - Otherwise the lowest rate limit among the matching allow rules applies, and the union of all IP allowlists applies: the caller's remote address must equal one entry exactly (no ranges). The address checked is the connection's remote address, not a forwarded header.
- Preflight
OPTIONSrequests are never checked.
The rate limiter is a fixed one-minute window kept in memory per server instance, keyed by tenant, actor and rule. With several instances behind a load balancer each instance counts separately.
| HTTP status | code | Message |
|---|---|---|
| 403 | api-access-denied | Actor "alice" is not permitted to DELETE /api/v1/employees/12 |
| 403 | api-ip-not-allowed | Remote address "10.0.0.5" is not on the allowlist for /api/v1/employees |
| 429 | api-rate-limit-exceeded | Actor "alice" exceeded the 60/minute rate limit for /api/v1/employees |
A separate tenant-wide ceiling is also applied to all /api/v1/** calls and answers 429 with code api-rate-limit-exceeded; it is not configured on this screen.
API. GET /api/v1/api-permissions; PUT /api/v1/api-permissions body {roleId, routePattern, httpMethod, effect, rateLimitPerMinute, ipAllowlist, oauthScope}; DELETE /api/v1/api-permissions body {roleId, routePattern, httpMethod}.
bash
erp api put /api/v1/api-permissions --tenant 2 --body '{"roleId":4,"routePattern":"/api/v1/employees/**","httpMethod":"*","effect":"allow","rateLimitPerMinute":120,"ipAllowlist":"203.0.113.10,203.0.113.11"}'Errors (HTTP 400). "routePattern must not be blank"; "httpMethod must not be blank"; "httpMethod must be one of [...]"; "effect must be one of [allow, deny]"; "rateLimitPerMinute must be positive".
Version history uses entity type api_permission. Changes are not written to the Permission Audit Log.
Named ACL Templates
A named ACL template is a reusable list of (who, action, effect) entries that can be applied to any resource string in one step. Its first consumer is document management, where one template protects many cabinets, folders and documents.
Where to find it. Security > Roles & Permissions > Named ACL Templates. Page key named-acls.
Fields and options.
| Field | Type or allowed values | Default | Required | Description |
|---|---|---|---|---|
| New template name | Text | empty | Yes | Unique per workspace. |
| Description (optional) | Text | empty | No | |
| Accessor | Select: Role, User | Role | Yes | |
| Role name / Actor | Text | empty | Yes | The label follows the accessor. A role is entered by its name and must exist; a user is a free-text actor name. |
| Action | Select: view, read, write, download, edit, delete, relate, version, execute, change_owner, change_permission | view | Yes | The API accepts any non-empty action. |
| Effect | Select: Allow, Deny | Allow | Yes |
The left panel lists templates; the delete icon asks Delete template "Standard cabinet"? This does not revoke any permissions it already applied. The entry grid shows Accessor ("Role: Facilities Staff" or "User: alice"), Action and Effect. Placeholders: "No templates yet.", "Select or create a template to author its entries.", "No entries yet - add one above."
Applying a template. Templates are applied from the object, not from this screen. In Files & Documents, the Permissions action of a cabinet or document opens a dialog titled "Permissions" followed by the object name, showing the resource string (dms.cabinet:12, dms.document:47), a list Apply a named ACL template with a Template select and Apply, and the role and user grants of that resource. Applying shows Applied "Standard cabinet" - 3 entries materialized. Each role entry becomes a role grant and each user entry a user override on that exact resource, tenant-wide. Applying twice rewrites the same rows. Cabinet actions offered by the dialog: write (Create inside), edit, delete. Document actions: edit, delete, relate, execute, change_owner. The resolver falls back from dms.document:47 to dms.document (see Per-record access).
API.
| Method and path | Purpose |
|---|---|
GET /api/v1/named-acls, GET /api/v1/named-acls/{id} | Read |
POST /api/v1/named-acls body {name, description} | Create, returns 201 |
DELETE /api/v1/named-acls/{id} | Delete the template and its entries (grants it already created stay) |
GET /api/v1/named-acls/{id}/entries | List entries |
POST /api/v1/named-acls/{id}/entries body {accessorType, accessorKey, action, effect} | Add an entry, returns 201 |
DELETE /api/v1/named-acls/{id}/entries/{entryId} | Remove an entry |
POST /api/v1/named-acls/{id}/apply body {resource} | Apply; returns {"entriesApplied": 3} |
bash
erp api post /api/v1/named-acls/4/apply --tenant 2 --body '{"resource":"dms.cabinet:12"}'Errors (HTTP 400 unless noted). "Named ACL name is required"; A named ACL called "Standard cabinet" already exists; "accessorType must be one of [role, user]"; "accessorKey (role name or actor) is required"; "action is required"; "effect must be one of [allow, deny]"; No role named "Facilities Staff" (when adding an entry or applying after the role was renamed or deleted); "resource is required"; "No named ACL 9 for tenant 2" (404). Templates are neither versioned nor audited; deleting one does not revoke grants it created.
Temporary Permissions
A temporary permission is an allow or deny for one person, valid only between two instants. Use it for cover during leave or a one-off task.
Where to find it. Security > Roles & Permissions > Temporary Permissions. Page key temporary-permissions.
Fields and options.
| Field | Type or allowed values | Default | Required | Description |
|---|---|---|---|---|
| Actor | Text | empty | Yes | The person's username. Helper text "e.g. alice". |
| Resource | Select: employee, org_unit, job_classification, position | employee | Yes | Studio list; the API accepts any resource string. |
| Action | Select, same 20 values as Permission Sets | create | Yes | |
| Effect | Select: allow, deny | allow | Yes | |
| Scope | Org unit | (Global - no scope) | No | |
| Valid from | Date and time | now | Yes | Local time; sent as an ISO instant. |
| Valid until | Date and time | now plus 24 hours | Yes | Must be after Valid from. |
Grant is disabled while Actor is empty. Grid: Actor (with resource.action), Resource, Action, Effect, Scope ("Global" or "Org unit #12"), Window, Status. History and remove icons per row.
Statuses.
| Status | Condition (computed in the browser from the current time) |
|---|---|
| upcoming | now is before Valid from |
| active | Valid from and Valid until inclusive |
| expired | now is after Valid until |
An expired row stays in the list until removed; it simply stops matching. There is no scheduler. A temporary permission is the first layer of the decision: when an active row matches the person, resource, action and org scope, its decision is final, and a deny wins over an allow among temporary rows.
API. GET /api/v1/temporary-permissions; POST /api/v1/temporary-permissions body {actorSubject, resource, action, effect, orgUnitId, validFrom, validUntil} (returns 201); DELETE /api/v1/temporary-permissions/{id}.
bash
erp api post /api/v1/temporary-permissions --tenant 2 --body '{"actorSubject":"alice","resource":"page:payroll-run","action":"view","effect":"allow","validFrom":"2026-10-06T00:00:00Z","validUntil":"2026-10-13T00:00:00Z"}'Limits and behaviour. Decisions are cached for 60 seconds when the shared cache is enabled; creating or removing a temporary permission does not invalidate that cache, so the start or end can take up to a minute to show. Changes are versioned (entity type temporary_permission) but not written to the Permission Audit Log.
Errors (HTTP 400). "actor is required"; "resource is required"; "action is required"; "effect must be one of [allow, deny]"; "validFrom and validUntil are required"; "validUntil must be after validFrom".
Access Policies
Access policies are attribute-based rules: they name subjects (roles, users, departments and others), protected resources, record scope, conditions, actions, field restrictions and a schedule, and they are versioned and conflict-checked. Use them where a role grant is too coarse, for example "HR managers may view compensation of their own department, with the bank account masked, on weekdays".
Where to find it. Security > Roles & Permissions > Access Policies. Page key access-policies.
Key concepts.
| Term | Definition |
|---|---|
| Policy code | Identifier, letters, digits and _ . -, maximum 100. Stable across versions; fixed once the policy exists. |
| Version | Each published change is a new row with the same code and the version number plus one. Published rows are immutable. |
| Resource code | Free-text code such as HCM.EMPLOYEE.COMPENSATION, or ENTITY.<ENTITY_NAME> to protect every screen and API built on an entity (payroll_run becomes ENTITY.PAYROLL_RUN; hyphens become underscores). |
| Covered resource | A resource that at least one ACTIVE policy names. |
| Default deny | Once a resource is covered, every person not allowed by an ACTIVE policy is denied for it. A resource no policy names is left to the module's existing checks. |
Statuses and lifecycle.
| Status | Meaning | Allowed next steps |
|---|---|---|
| DRAFT | Editable, not evaluated. | Edit, Save draft, Publish (to ACTIVE), Delete |
| ACTIVE | Evaluated. Immutable. | Deactivate (to INACTIVE), Create version (new DRAFT), Archive |
| INACTIVE | Not evaluated. Immutable. | Activate (to ACTIVE), Create version, Archive |
| ARCHIVED | Retired. | None. |
Publishing validates the policy and checks for conflicts, then sets the status to ACTIVE directly. Publishing a new version does not change the status of earlier versions; deactivate or archive the older version yourself. An ACTIVE policy is also a candidate for default deny immediately; recovery from a lock-out is to deactivate the policy, which takes effect within about 10 seconds (the active policy list is cached for 10 seconds).
Fields and options. The editor dialog has eight tabs.
| Tab | Field | Allowed values | Default | Required | Description |
|---|---|---|---|---|---|
| Basic info | Policy code | [A-Za-z0-9_.-], 1 to 100 | empty | Yes | Locked after creation. |
| Policy name | Text | empty | Yes | ||
| Description | Text | empty | No | ||
| Policy type | APPLICATION_ACCESS, MODULE_ACCESS, MENU_ACCESS, RECORD_ACCESS, FIELD_ACCESS, ACTION_ACCESS, ORGANIZATION_ACCESS, EMPLOYEE_DATA_ACCESS, TRANSACTION_ACCESS, REPORT_ACCESS, EXPORT_ACCESS | RECORD_ACCESS | Yes | A label and filter; it does not change evaluation. | |
| Effect | ALLOW, DENY | ALLOW | Yes | ||
| Priority | Integer | 50 | Yes | Higher wins between policies of the same effect. A DENY beats an ALLOW whatever the priority. | |
| Subjects | Type | ROLE, USER, GROUP, POSITION, DESIGNATION, JOB_GRADE, DEPARTMENT, BUSINESS_UNIT, LOCATION, EMPLOYEE_CATEGORY, EMPLOYMENT_TYPE, MANAGER, DYNAMIC_SUBJECT | ROLE | No subjects means everyone ("Everyone" chip). Every type except MANAGER needs a code. | |
| Dynamic subject | CURRENT_USER, CURRENT_USER_DEPARTMENT, CURRENT_USER_BUSINESS_UNIT, CURRENT_USER_LOCATION, CURRENT_USER_DIRECT_REPORTS, CURRENT_USER_ORGANIZATION, CURRENT_USER_COST_CENTER | Compares employee.* with currentUser.*. | |||
| Resources | Resource code | Free text; suggestions HCM.EMPLOYEE.PROFILE, HCM.EMPLOYEE.COMPENSATION, HCM.PAYROLL.PAYSLIP, HCM.PAYROLL.SUMMARY, HCM.LEAVE.REQUESTS, HCM.ATTENDANCE.REGISTER, HCM.RECRUITMENT, HCM.REPORTS, ENTITY.PAYROLL_RUN, ENTITY.EMPLOYEE | No spaces. None means "All resources". | ||
| Access scope | Which records | GLOBAL, ORGANIZATION, LEGAL_ENTITY, BUSINESS_UNIT, DIVISION, DEPARTMENT, TEAM, LOCATION, BRANCH, COST_CENTER, REPORTING_TREE, SELF, DIRECT_REPORTS, CUSTOM_FILTER | GLOBAL | Scopes other than GLOBAL compare the record with the requester, so they apply where one record is read; lists and entity-level checks only match GLOBAL. A warning shows for a GLOBAL ALLOW. | |
| Conditions | Match | AND, OR, NOT | AND | No rules means the policy always applies. | |
| Field, Operator, Value | Operators =, !=, >, <, >=, <=, IN, NOT IN, CONTAINS, STARTS WITH, IS NULL, IS NOT NULL, BETWEEN | = | Placeholder field employee.departmentId. IS NULL and IS NOT NULL take no value; every other operator needs one. | ||
| Actions | Actions | VIEW, CREATE, EDIT, DELETE, APPROVE, REJECT, SUBMIT, CANCEL, EXPORT, IMPORT, DOWNLOAD, PRINT, SHARE, ASSIGN, REASSIGN, DELEGATE | VIEW | Empty means every action. | |
| Restrictions | Restriction | MASK, HIDE, READ_ONLY, NO_EXPORT, NO_DOWNLOAD, NO_PRINT | MASK | MASK, HIDE and READ_ONLY need a field. NO_EXPORT, NO_DOWNLOAD and NO_PRINT turn an otherwise allowed EXPORT, DOWNLOAD or PRINT into a deny. | |
| Masking mode | FULL, PARTIAL, LAST_4, FIRST_4, EMAIL_MASK, PHONE_MASK, CUSTOM | FULL | MASK only. | ||
| Schedule | Effective from, Effective to | YYYY-MM-DD | empty | To must not be before From. | |
| Days of week | MON to SUN | empty (every day) | |||
| Start time, End time | HH:MM | empty | Both or neither. Overnight windows are supported. | ||
| Timezone | IANA zone, for example Asia/Kolkata | UTC | |||
| Allowed network zones | CORPORATE, VPN, TRUSTED_IP | empty (any) | The platform has no trusted network-zone service, so a policy restricted to a zone denies everyone at run time. |
Actions. Row buttons depend on status: Edit (DRAFT) or View, Publish, Activate, Deactivate, Create version, Test, Clone, History, Export, Archive, Delete. Header buttons: Import, New policy, reload.
| Action | Effect | API (base /api/v1/authoring/access-policies) |
|---|---|---|
| New policy, Save draft | Creates a DRAFT (AUTHOR) | POST |
| Save draft (existing) | Optimistic-locked update, body includes expectedRevision | PUT /{id}/draft |
| Save & publish | Save then publish (PUBLISH) | POST /{id}/publish |
| Activate, Deactivate, Archive | Status changes (PUBLISH) | POST /{id}/activate, /deactivate, /archive |
| Create version | New DRAFT with version plus one (AUTHOR) | POST /{id}/new-version |
| Clone | Dialog titled Clone followed by the policy code, field New policy code, helper "Creates a new draft policy with the same content." The clone keeps the source name with " (Clone)" appended and starts at version 1. | POST /{id}/clone body {newCode} |
| Delete | Dialog titled "Delete draft" followed by the code; text "This permanently deletes the draft. Published policies cannot be deleted - archive them instead." (PUBLISH) | DELETE /{id} |
| History | Dialog titled "Version history" followed by the code: Version, Status, Priority, Updated, By | GET /code/{code}/history |
| Export | Downloads access-policy-<code>.json | GET /{id}/export |
| Import | Dialog "Import a policy": paste JSON, Validate (shows a summary, errors and warnings), Import as draft | POST /validate-text, POST /import-text body {json} |
| Test | Dialog "Test - evaluates the tenant's ACTIVE policies (nothing is changed)": Resource (default HCM.EMPLOYEE.PROFILE), Action (default VIEW), Context (JSON) (default {"roleCode": "HR_MANAGER"}). Result shows the decision, matched policy, reason, field restrictions and the evaluation trace. | POST /{id}/evaluate body {resourceCode, action, context} |
Context keys understood by the test and by callers: roleCode, userId, groupCode, positionCode, designationCode, jobGradeCode, departmentId, businessUnitId, locationId, employeeCategory, employmentType, isManager, networkZone, evaluationTime, employee.* (the record) and currentUser.* (the requester). In production calls the platform derives roles, user id and org units itself; a request body cannot claim a role, the time or a network zone.
Evaluation. For a resource and action, the engine reads the ACTIVE policies and skips a policy unless all of the following match: resource, action, effective dates, days, time window and network zone, record scope, subject, then conditions. Among matches, an explicit DENY wins; otherwise the highest-priority ALLOW applies, and if that policy carries NO_EXPORT, NO_DOWNLOAD or NO_PRINT for the requested action the result is DENY; if nothing matches the result is DENY. An ALLOW returns its MASK, HIDE and READ_ONLY restrictions, which the calling module applies to the record.
Validation and warnings. Publishing and import run the validator. Errors block; warnings do not.
| Message | Kind |
|---|---|
| Policy code is required (letters, digits, _ . - only, max 100). | Error |
| Policy name is required. | Error |
| Unknown policy type 'X'. / Effect must be ALLOW or DENY. | Error |
| Invalid subject type 'X'. / Subject of type ROLE needs a code. | Error |
| Invalid resource code 'X' (expected e.g. HCM.EMPLOYEE.COMPENSATION). | Error |
| Unknown access scope 'X'. / Unknown action 'X'. | Error |
| Unknown condition group operator 'X'. / A condition is missing its field. / Unknown condition operator 'X'. / Condition on 'f' needs a value. | Error |
| Unknown restriction type 'X'. / Restriction MASK needs a field name. / Unknown masking mode 'X'. | Error |
| Unknown day of week 'X'. / Schedule startTime 'X' is not HH:MM. / Schedule needs both a start and an end time, or neither. / Unknown timezone 'X'. / Schedule effectiveFrom 'X' is not a valid date (use YYYY-MM-DD). / Effective To is before Effective From. | Error |
| Global ALLOW: grants access to every record in scope. | Warning |
| Global DELETE access. / Global EXPORT access. | Warning |
| Global access to sensitive resource X. (resource code contains COMPENSATION, BANK, SALARY, DOCUMENTS, GOVERNMENT_ID or MEDICAL) | Warning |
The service layer adds: "Cannot publish an invalid policy: ..." (400); "Conflicting policies detected: 1 existing active policy with opposing effect at equal or higher priority" (409, code policy-conflict, detail.conflicts lists code, effect and priority); a published policy cannot be edited ("... published policies are immutable; create a new version instead", 409); "Cannot activate access policy definition 5 in status ACTIVE (requires INACTIVE)" and similar for other transitions (409); stale draft (409 stale-draft, reload and retry); "A new code is required to clone ..."; "An access policy definition with code 'X' already exists for tenant 2"; "only DRAFT policies can be deleted; archive it instead". A conflict exists when another ACTIVE policy has the opposite effect, equal or higher priority, an overlapping resource (a policy with no resources overlaps all) and an overlapping subject (a policy with no subjects overlaps all). The Studio shows the conflict as "Conflicting policies detected: ...: HR_ALLOW (ALLOW, priority 60). Change a priority, subject or resource, or deactivate the other policy."
Where policies are enforced. Through POST /api/v1/access-policies/authorize (body {resourceCode, action, context, record}; the tenant comes from the gateway header) and the generic data-access seam.
| Resource | Where | Notes |
|---|---|---|
HCM.EMPLOYEE.PROFILE | HCM employee directory query and single record, action VIEW | HIDE and MASK rewrite the response. |
HCM.EMPLOYEE.COMPENSATION | HCM compensation reads, payroll compensation comparison | |
HCM.PAYROLL.PAYSLIP | Latest payslip, its explanation, payslip download (DOWNLOAD) | A PDF cannot be masked: allow or deny only. |
HCM.PAYROLL.SUMMARY | Payroll organisation summary, country roll-up | Aggregates have no owner, so only GLOBAL scope matches. |
ENTITY.<NAME> | Every generic entity endpoint (record CRUD, grid query, export, lookup) | Actions READ to VIEW, CREATE, UPDATE to EDIT, DELETE. Only GLOBAL scope without record conditions can match; no field masking; export is treated as a read. The internal system actor is exempt. |
Existing role grants and row scope checks run first; a policy can only deny or restrict further. A failure of the policy service itself lets the request proceed under the existing checks and logs a warning; a policy DENY never fails open. Other modules do not consult policies.
Procedure. Mask the bank account of employees for HR assistants and keep compensation to HR managers.
- New policy: code
HR_COMP_VIEW, nameHR managers view compensation, effectALLOW, priority60. - Subjects:
ROLE/HR_MANAGER. Resources:HCM.EMPLOYEE.COMPENSATION. Access scopeGLOBAL. ActionsVIEW. - Restrictions:
MASK, fieldbankAccount, modeLAST_4. - Test with resource
HCM.EMPLOYEE.COMPENSATION, actionVIEW, context{"roleCode":"HR_MANAGER"}: the result is ALLOW with "MASK bankAccount (LAST_4)". With{"roleCode":"INTERN"}the result is DENY (the resource is covered, the intern is not allowed). - Save & publish. The status becomes ACTIVE and the notice reads
Published HR_COMP_VIEW (v1).
json
{
"code": "HR_COMP_VIEW",
"name": "HR managers view compensation",
"description": "Compensation visible to HR managers with the bank account masked",
"policyType": "RECORD_ACCESS",
"effect": "ALLOW",
"priority": 60,
"subjects": [{ "type": "ROLE", "code": "HR_MANAGER" }],
"resources": [{ "code": "HCM.EMPLOYEE.COMPENSATION" }],
"conditions": { "operator": "AND", "rules": [], "scope": { "type": "GLOBAL" } },
"actions": ["VIEW"],
"restrictions": [{ "type": "MASK", "field": "bankAccount", "mode": "LAST_4" }],
"schedule": { "effectiveFrom": "2026-01-01", "effectiveTo": "", "daysOfWeek": ["MON", "TUE", "WED", "THU", "FRI"], "startTime": "08:00", "endTime": "19:00", "timezone": "Asia/Kolkata", "networkZones": [] }
}bash
erp api post /api/v1/authoring/access-policies --tenant 2 --body @hr_comp_view.json
erp api post /api/v1/authoring/access-policies/5/publish --tenant 2Limits and behaviour. Policies are stored per workspace; there is no decision cache beyond the 10-second policy list. Tests, imports and exports are not audited. The validator checks format, not whether a role or department code exists. The policy type does not alter behaviour.
Related. Permissions - how an access decision is made, Record Permissions.
Effective Permission Viewer
The viewer shows why a person is allowed or denied an action on a resource, and replays the question for a past instant. Administrators use it first when an access result is not what they expected.
Where to find it. Security > Roles & Permissions > Effective Permission Viewer. Page key effective-permission-viewer.
Fields and options.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| Actor | Text | empty | Yes | Helper text e.g. "alice". |
| Resource | Text | empty | Yes | Helper text e.g. "payroll". The exact resource string, for example page:leave-settings or workflow:leave-request. |
| Action | Text | empty | Yes | Helper text e.g. "delete". |
| As of (optional, for Simulate) | Date and time | empty (now) | No | Used by Simulate & save only. |
Actions.
| Action | Effect | API |
|---|---|---|
| Explain now | Runs the resolver at the current instant and shows the result. Nothing is stored. | GET /api/v1/permission-explain?actor=&resource=&action= |
| Simulate & save | Runs the resolver at As of (or now), shows the result and stores it. | POST /api/v1/permission-simulations body {actor, resource, action, asOf}, returns 201 |
| Load (history) | Lists stored simulations, optionally for one actor ("Filter by actor"). | GET /api/v1/permission-simulations?actor= |
Result. A chip shows Allowed or Denied and "Decided by:" followed by one of: "Temporary permission", "User override", "Role", "Default (no matching rule)". The trace grid lists every candidate row of the layer that decided: Layer, Source (the role name, or the actor for temporary and user rows), Effect, Org unit ("tenant-wide" or the id), Applied? ("yes" or "no - scope mismatch"; the latter row exists but its org scope does not reach the person and played no part in the decision). When the trace is empty the viewer states "No temporary permission, user override, or role grant matched at all - the platform-wide fail-open default applied."
History columns: Actor, Resource, Action, As of, Result, Run by, Run at (newest first). Placeholders: "No history loaded yet - click Load, or run a simulation above." and "No simulations recorded yet."
Limits and behaviour. The simulator varies one input: the instant. It replays the person's real current roles and org units, so it can answer "was this temporary grant active last Tuesday" but not "what if I gave this person role X". Role grants and user overrides carry no validity window and are always evaluated as they are now. The explanation is never cached, so it reflects the database, while production checks can lag by up to 60 seconds when the shared cache is on. The endpoints are open to any caller; Simulate records the caller in Run by.
Procedure. Diagnose "Alice cannot open the payroll page".
- Actor
alice, resourcepage:payroll-run, actionview, then Explain now. - Read "Decided by". If it is "Role", the Source column names the role carrying the deny. If a row shows "no - scope mismatch", a role grant exists but at another org unit.
Permission Audit Log
The log lists changes to roles, role grants, permission sets and user overrides: who changed what, with before and after values. It is read-only; to undo a change, restore a version from the History dialog of the item.
Where to find it. Security > Roles & Permissions > Permission Audit Log. Page key permission-audit-timeline.
Fields and options.
| Field | Type or values | Default | Description |
|---|---|---|---|
| Entity type | (any), role, role_permission, permission_set, user_permission | (any) | |
| Entity key (optional) | Text | empty | Helper "Look up one entity's own timeline". When filled, the call uses entity type (default role) and key and ignores Actor. Key formats are in the table under Behaviour shared by all screens. |
| Actor (optional) | Text | empty | Exact match on the person who made the change. |
Nothing is loaded until Load is selected ("No timeline loaded yet - click Load."; "No audit records match."). Columns: When (newest first), Entity (type · key), Operation (create, update, delete), Actor, What changed (one line field: before -> after per changed field). A search box searches the loaded rows.
What is recorded.
| Recorded | Not recorded |
|---|---|
| Role create, update, delete | Field permissions, API permissions, approval limits, record scope rules, temporary permissions, named ACL templates |
| Role grant set and clear (matrix cells on every grid) | Permission set attach and detach (the resulting role grants are not logged as separate events either) |
| Permission set create, update, delete, item add and remove | Access policy changes (see the policy History dialog) |
| User override set and clear | Account and sign-in events (see Audit Logs and Audit Log) |
API. GET /api/v1/permission-audit?entityType=role_permission&actor=alice and GET /api/v1/permission-audit/entity?entityType=role&entityKey=7. Each row carries id, entityType, entityKey, operation, oldValuesJson, newValuesJson, actor, correlationId, occurredAt.
bash
erp api get "/api/v1/permission-audit?entityType=role_permission&actor=alice" --tenant 2Limits and behaviour. The same events invalidate the 60-second decision cache. Reads need no capability.
Related. Effective Permission Viewer, Audit Log.
