Cluster ERP authorises requests with role-based access control: a user holds roles, a role holds permission codes, and a route demands one or more codes. This page explains the permission catalogue in modules/auth/permissions, roles in modules/auth/roles, user-role assignment in modules/auth/users, the guard in modules/auth/guards.py, the family splits in server/permission_families.py, and the REQUIRED_PERMISSION pattern on EventBus contracts. Attribute policies that refine a permission are in ABAC policies; field and row limits are in field and row access.
In short¶
- A permission code has the form
<resource>.<action>. At every start-up, codes forcreate,read,update, anddeleteare seeded for each entity and settings tab in module metadata, plus a fixed list of extra codes (src/cluster_erp/modules/auth/permissions/services.py:44-136,src/cluster_erp/server/app.py:634-858). - One system role with code
adminis ensured at every start-up and re-granted every live permission (src/cluster_erp/modules/auth/roles/services.py:213-276). - A route declares its demand with
require_permission("<code>"); the guard authenticates the caller, checks that the caller holds every code, then evaluates attribute policies (src/cluster_erp/modules/auth/guards.py:205-240). - Granting is capped: a caller may only add a role or a permission whose codes the caller already holds, unless the caller holds a system administrator role (
src/cluster_erp/modules/auth/users/services.py:169-221,src/cluster_erp/modules/auth/roles/services.py:122-163). - A user's permission set is the union over live user-role links and live grants of live permissions (
src/cluster_erp/modules/auth/users/services.py:310-332).
How it works¶
Data model¶
| Entity | Table | Key fields | Source |
|---|---|---|---|
AuthPermission |
auth_permissions |
code (unique among live rows), resource, action, module, translations |
src/cluster_erp/modules/auth/permissions/models.py:15-55 |
AuthRole |
auth_roles |
code (unique among live rows), name, translations, is_system, is_active |
src/cluster_erp/modules/auth/roles/models.py:17-65 |
AuthRolePermission |
auth_role_permissions |
role_id, permission_id (unique pair among live rows) |
src/cluster_erp/modules/auth/roles/models.py:68-97 |
AuthUser |
auth_users |
username, email (each unique among live rows), password_hash, is_active, employee_id |
src/cluster_erp/modules/auth/users/models.py:17-91 |
AuthUserRole |
auth_user_roles |
user_id, role_id (unique pair among live rows) |
src/cluster_erp/modules/auth/users/models.py:94-123 |
Grants and assignments are never deleted. Removing a role from a user or a permission from a role sets deleted_at; adding it again revives the same row (src/cluster_erp/modules/auth/users/services.py:161-181, src/cluster_erp/modules/auth/roles/services.py:115-134).
Seeding the catalogue¶
_seed_permissions_from_metadata collects, for each loaded module, the entity keys of its metadata and the tab keys of its settings sections (src/cluster_erp/server/app.py:639-652). It adds a list of extra codes for lifecycle actions such as approve, install, or read_all (src/cluster_erp/server/app.py:654-848). seed_from_metadata creates any missing code and re-files an existing code under the module that declares its entity today (src/cluster_erp/modules/auth/permissions/services.py:44-136). After that, ensure_system_admin grants every live permission to the admin role, reviving revoked grants, and consolidates a legacy system_admin role into it (src/cluster_erp/modules/auth/roles/services.py:174-276). copy_family_grants then runs (see the section Family splits).
Counts at this commit, made by reading the code:
| Fact | Value | How counted |
|---|---|---|
| CRUD actions seeded per entity or tab | 4 | the tuple actions in src/cluster_erp/modules/auth/permissions/services.py:54 |
| Extra codes in the seed list | 153 | tuples in extra_codes, src/cluster_erp/server/app.py:654-848 |
| Roles created by code | 1 (admin, is_system=True) |
src/cluster_erp/modules/auth/roles/services.py:226-236 |
| Role codes that bypass grant ceilings and approval-step roles | 2 (admin, system_admin) |
SUPERADMIN_ROLE_CODES, src/cluster_erp/modules/auth/users/services.py:32 |
require_permission calls with a literal code |
1,295 calls, 608 distinct codes, in 126 files | grep for require_permission(" over src/cluster_erp |
The total number of codes in a database depends on the loaded modules and their metadata; it is not fixed in code.
The permission check¶
The flowchart shows what require_permission does for one request.
flowchart TD
req["guarded request"] --> user["get_current_user"]
user --> codes["auth_cache.permission_codes"]
codes --> loop{"for each required code"}
loop --> held{"code held?"}
held -->|no| deny["permission_denied 403"]
held -->|yes| abac["check_abac with resource None"]
abac -->|false| deny
abac -->|true| loop
loop -->|all passed| route["route handler"]
route --> post["enforce_abac after load, wired codes only"]In prose:
get_current_userauthenticates the caller (src/cluster_erp/modules/auth/guards.py:212). See authentication and sessions.- The caller's codes come from the short-lived cache (
src/cluster_erp/modules/auth/guards.py:213). - For each required code, a missing code raises
permission_denied, HTTP 403 with codeAUTH_PERMISSION_DENIED(src/cluster_erp/modules/auth/guards.py:222-224,src/cluster_erp/core/errors.py:388-394). check_abacevaluates the code's attribute policies without an entity; codes listed inabac_strict_permission_codesare denied when they have no policy (src/cluster_erp/modules/auth/guards.py:215-236).- Some services call
enforce_abacagain after loading the entity (src/cluster_erp/modules/auth/guards.py:284-303). See ABAC policies.
App entitlement is a separate router dependency, require_app, on business modules; see app store licensing and entitlements.
Assigning roles and grants¶
| Route | Permission | Effect | Source |
|---|---|---|---|
GET /api/v1/auth/permissions/ |
permission.read |
List the catalogue, filtered by module or resource | src/cluster_erp/modules/auth/permissions/routes.py:14-23 |
PUT /api/v1/auth/roles/{role_id}/permissions |
permission.assign |
Replace a role's grants, subject to the ceiling | src/cluster_erp/modules/auth/roles/routes.py:135-150 |
PUT /api/v1/auth/users/{user_id} |
user.update |
Update a user, including role_ids, subject to the ceiling |
src/cluster_erp/modules/auth/users/routes.py:124-137 |
POST /api/v1/auth/users/{user_id}/reset-password |
user.reset_password |
Reset another user's password; the actor must hold every code of the target | src/cluster_erp/modules/auth/users/routes.py:177-194, src/cluster_erp/modules/auth/users/services.py:234-253 |
DELETE /api/v1/auth/roles/{role_id} |
role.delete |
Deactivate a role; system roles are refused | src/cluster_erp/modules/auth/roles/routes.py:95-108 |
The ceiling raises privilege_escalation with code AUTH_PRIVILEGE_ESCALATION and lists the missing codes (src/cluster_erp/core/errors.py:397-405).
Family splits¶
When a group of routes moves from a shared code family to its own, FAMILY_SPLITS names the parent and the new families: sales_order with 8 child families and fleet_asset with 23 (src/cluster_erp/server/permission_families.py:25-64). At start-up, copy_family_grants grants each role that holds a parent code the matching child codes, once; ACTION_GRANTS does the same for lifecycle codes such as shipment.dispatch (src/cluster_erp/server/permission_families.py:67-118).
Permission vocabulary on contracts¶
A route that triggers work in another module through the EventBus reads the needed code from the contract class instead of writing another module's code. CreateBomForProduct.REQUIRED_PERMISSION and CreateProductionOrderForProject.REQUIRED_PERMISSION are the two contracts with this attribute (src/cluster_erp/core/contracts.py:1341-1346, src/cluster_erp/core/contracts.py:1361-1365). Their callers are src/cluster_erp/modules/sales_orders/order_lines/routes.py:114 and src/cluster_erp/modules/projects/items/routes.py:94-98.
Why it is built this way¶
- The project's written engineering rules forbid direct module-to-module calls;
REQUIRED_PERMISSIONkeeps another module's permission string out of the caller (src/cluster_erp/core/contracts.py:1341-1344). - The code's module owner is re-filed at seed time because a code filed under an older owner showed the wrong module to an administrator composing a role (
src/cluster_erp/modules/auth/permissions/services.py:125-133). - The grant ceiling exists so that a holder of
permission.assignoruser.updatecannot raise their own or another user's privileges (src/cluster_erp/modules/auth/roles/services.py:122-125,src/cluster_erp/modules/auth/users/services.py:169-172). - The
adminrole is re-granted all codes at boot because it must hold every active permission after start-up (src/cluster_erp/modules/auth/roles/services.py:251-256).
Rules and boundaries¶
- Other modules import only
modules/auth/guards.pyfrom auth:require_permission,get_current_user,permission_codes_for,actor_allowed, androle_ids_for(src/cluster_erp/modules/auth/guards.py:22-27,src/cluster_erp/modules/auth/guards.py:170-202). - A code is global. The guard checks the code, never the module (
src/cluster_erp/modules/auth/permissions/services.py:126). - A route that authenticates but demands no code calls
get_current_userdirectly, for example the approval inbox and the user's own UI state (src/cluster_erp/modules/auth/approvals/routes.py:35-53,src/cluster_erp/modules/auth/ui_state/routes.py:15-39).
Interaction with other parts¶
- The
authmodule answersGetUserPermissionsfor other modules (src/cluster_erp/modules/auth/module.py:82,src/cluster_erp/modules/auth/module.py:130-144). - Metadata fields and actions can name a permission code; see metadata model.
- App-level role assignments (which roles may use which app) are a separate layer; see app store licensing and entitlements.
Limits and open points¶
- The number of permission codes in a running database is not determined from code; it depends on loaded modules (
src/cluster_erp/server/app.py:642-652). - Whether the web app offers a grant editor beyond the generic role form is not determined from the auth module metadata;
ROLE_METAis the only role screen declared there (src/cluster_erp/modules/auth/module.py:281-283).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| permission code | AuthPermission.code |
A string of the form resource.action that a route demands. | izin kodu | src/cluster_erp/modules/auth/permissions/models.py:27 |
| role | AuthRole |
A named set of permission codes that is assigned to users. | Rol (webapp/i18n/tr.json key resource.role) |
src/cluster_erp/modules/auth/roles/models.py:17 |
| system role | AuthRole.is_system |
A role that cannot be deleted; admin is the one created by code. |
sistem rolü | src/cluster_erp/modules/auth/roles/models.py:46 |
| privilege ceiling | privilege_escalation |
The rule that a caller may grant only codes the caller already holds. | yetki tavanı | src/cluster_erp/core/errors.py:397 |
| permission family split | FAMILY_SPLITS |
A parent code family whose routes moved to their own child families. | izin ailesi ayrımı | src/cluster_erp/server/permission_families.py:25 |
Related pages¶
- Identity, access, tenancy, privacy, and licensing
- ABAC policies
- Field and row access
- Auth module
- Contracts reference
Verified against commit 92f8051 on 2026-11-13.