Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Identity and access

Explanation

Roles and permissions

How permission codes are seeded from metadata, granted to roles, assigned to users, and checked by require_permission on every guarded route.

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 for create, read, update, and delete are 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 admin is 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:

  1. get_current_user authenticates the caller (src/cluster_erp/modules/auth/guards.py:212). See authentication and sessions.
  2. The caller's codes come from the short-lived cache (src/cluster_erp/modules/auth/guards.py:213).
  3. For each required code, a missing code raises permission_denied, HTTP 403 with code AUTH_PERMISSION_DENIED (src/cluster_erp/modules/auth/guards.py:222-224, src/cluster_erp/core/errors.py:388-394).
  4. check_abac evaluates the code's attribute policies without an entity; codes listed in abac_strict_permission_codes are denied when they have no policy (src/cluster_erp/modules/auth/guards.py:215-236).
  5. Some services call enforce_abac again 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_PERMISSION keeps 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.assign or user.update cannot 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 admin role 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.py from auth: require_permission, get_current_user, permission_codes_for, actor_allowed, and role_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_user directly, 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 auth module answers GetUserPermissions for 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_META is 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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13