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

Explanation

ABAC policies

How attribute-based policies in modules/auth/policies refine a permission code with rules over user, resource, and environment attributes.

An attribute-based access control policy (AuthAbacPolicy, an "ABAC policy") adds conditions to one permission code. A user who holds the code must also satisfy every active policy of that code. Policies are records that an administrator edits in the auth settings; the evaluator lives in modules/auth/guards.py and the records in modules/auth/policies/. Roles and permission codes themselves are explained in roles and permissions.

In short

  • A policy belongs to one permission_code and stores a condition of the form {"logic": "all" or "any", "rules": [...]} (src/cluster_erp/modules/auth/policies/models.py:12-55, src/cluster_erp/modules/auth/policies/schemas.py:76-90).
  • A rule has a subject attribute path, an operator, and a target that is a literal or another attribute path (src/cluster_erp/modules/auth/guards.py:325-334).
  • Ten operators exist: eq, neq, in, not_in, gt, gte, lt, lte, contains, between (src/cluster_erp/modules/auth/policies/schemas.py:11-13, src/cluster_erp/modules/auth/guards.py:394-438).
  • All active policies of a code must pass; each policy combines its own rules by its logic (src/cluster_erp/modules/auth/guards.py:259-281, src/cluster_erp/modules/auth/guards.py:306-322).
  • With no policy, a code is allowed, unless it is listed in the setting abac_strict_permission_codes (src/cluster_erp/modules/auth/guards.py:215-236, src/cluster_erp/config/settings.py:339).

How it works

Attributes

_resolve_attribute splits a path at the first dot into a scope and a field (src/cluster_erp/modules/auth/guards.py:337-369):

Scope Fields Source
user Only the allow-listed names id, username, email, first_name, last_name, employee_id, party_id, is_active, department, clearance, title, location; any other name resolves to nothing src/cluster_erp/modules/auth/guards.py:41-61, src/cluster_erp/modules/auth/guards.py:351-356
resource Any key of the attribute dictionary that the caller passes for a loaded entity src/cluster_erp/modules/auth/guards.py:357-358
env current_time (HH:MM:SS, UTC) and current_date (YYYY-MM-DD, UTC) src/cluster_erp/modules/auth/guards.py:359-367

The names department, clearance, title, and location are not columns of AuthUser; the comment calls them forward slots for subject attributes from human resources, and they resolve to nothing today (src/cluster_erp/modules/auth/guards.py:41-45, src/cluster_erp/modules/auth/users/models.py:17-91).

Operators

Operator Meaning Missing or malformed input Source
eq, neq String equality or inequality False src/cluster_erp/modules/auth/guards.py:395-402
in, not_in Membership in a list target False when the target is not a list src/cluster_erp/modules/auth/guards.py:403-412
gt, gte, lt, lte Ordering of numbers, then ISO times, then ISO dates False when the pair cannot be compared src/cluster_erp/modules/auth/guards.py:372-391, src/cluster_erp/modules/auth/guards.py:413-424
contains Substring of a string subject False for non-strings src/cluster_erp/modules/auth/guards.py:425-428
between Inclusive range given as a two-item list False otherwise src/cluster_erp/modules/auth/guards.py:429-436
any other value none False src/cluster_erp/modules/auth/guards.py:438

Where policies are evaluated

The flowchart shows the two evaluation points.

flowchart TD
 guard["require_permission"] --> route_eval["check_abac, resource None"]
 route_eval --> skip["rules on resource.* skipped"]
 skip --> userenv["user.* and env.* rules decided"]
 userenv --> handler["route handler loads entity"]
 handler --> wired{"code in RESOURCE_ENFORCED_CODES?"}
 wired -->|yes| post["enforce_abac with entity attributes"]
 wired -->|no| done["no second evaluation"]
 post --> done

In prose:

  1. The route guard calls check_abac with resource=None. Rules that name a resource.* attribute are left out at this point; user.* and env.* rules are decided (src/cluster_erp/modules/auth/guards.py:225-236, src/cluster_erp/modules/auth/guards.py:306-322).
  2. After it loads the entity, a service calls enforce_abac with the entity's attributes, so resource.* rules are decided too (src/cluster_erp/modules/auth/guards.py:284-303).
  3. Only codes in RESOURCE_ENFORCED_CODES are wired to this second step. At this commit the set holds one code, intranet_official_delivery.read, and its caller is src/cluster_erp/modules/intranet/delivery/routes.py:38 (src/cluster_erp/modules/auth/guards.py:29-39).
  4. PolicyService refuses to store a resource.* rule for any other code, with HTTP 422 and code ABAC_RESOURCE_RULES_UNSUPPORTED, on create and on update (src/cluster_erp/modules/auth/policies/services.py:64-88, src/cluster_erp/modules/auth/policies/services.py:90-131).

Storing a policy

The policy form edits a logic selector and a list of rule rows. _assemble_condition folds them into condition and _clean_rules drops incomplete rows: a missing subject, an unknown operator, an empty target, or a list operator whose target cannot become a non-empty list (src/cluster_erp/modules/auth/policies/schemas.py:44-90). A plain string target for in, not_in, or between is turned into a list, either from a JSON array or from comma-separated values (src/cluster_erp/modules/auth/policies/schemas.py:20-41). An update merges the builder fields into the stored condition, so a partial update does not erase the other half (src/cluster_erp/modules/auth/policies/services.py:97-131).

Illustrative request body built from PolicyCreate (src/cluster_erp/modules/auth/policies/schemas.py:93-104):

{
 "code": "<policy_code>",
 "permission_code": "<resource>.<action>",
 "name": "<policy name>",
 "condition_logic": "all",
 "condition_rules": [
 {"subject": "env.current_time", "operator": "between", "target": ["09:00", "18:00"]}
 ],
 "is_active": true
}

Routes

Route Permission Source
GET /api/v1/auth/policies/ abac_policy.read src/cluster_erp/modules/auth/policies/routes.py:18-27
POST /api/v1/auth/policies/ abac_policy.create src/cluster_erp/modules/auth/policies/routes.py:30-38
GET /api/v1/auth/policies/{policy_id} abac_policy.read src/cluster_erp/modules/auth/policies/routes.py:41-51
PUT /api/v1/auth/policies/{policy_id} abac_policy.update src/cluster_erp/modules/auth/policies/routes.py:54-65
DELETE /api/v1/auth/policies/{policy_id} abac_policy.delete src/cluster_erp/modules/auth/policies/routes.py:68-76
POST /api/v1/auth/policies/bulk-delete abac_policy.delete src/cluster_erp/modules/auth/policies/routes.py:79-87

Deleting sets deleted_at (src/cluster_erp/modules/auth/policies/services.py:133-139). The web screen is the ABAC Policy entity (abac_policy) in the auth settings tabs (src/cluster_erp/modules/auth/module.py:266-275, src/cluster_erp/modules/auth/policies/metadata.py:35).

Why it is built this way

  • Resource rules are skipped at the route guard because no entity is loaded there; evaluating them against an empty dictionary would deny the whole endpoint (src/cluster_erp/modules/auth/guards.py:254-257).
  • A resource rule is refused for unwired codes because the policy would look configured while enforcing nothing (src/cluster_erp/modules/auth/guards.py:29-34, src/cluster_erp/modules/auth/policies/services.py:68-71).
  • The user attribute allow-list keeps the condition language away from sensitive columns such as the password hash (src/cluster_erp/modules/auth/guards.py:41-45).
  • Incomplete rule rows are dropped because a blank rule would silently deny under all, or grant under not_in (src/cluster_erp/modules/auth/policies/schemas.py:45-50).

Rules and boundaries

  • Policies are cached per database with the permission codes and dropped on any policy write; see the section Permission memory in authentication and sessions (src/cluster_erp/modules/auth/auth_cache.py:113-135).
  • The cache keeps a detached PolicyRule snapshot, never an ORM row (src/cluster_erp/modules/auth/auth_cache.py:28-38).
  • Tests that pin the evaluator: tests/test_abac_evaluator.py (17 tests), tests/test_abac_enforcement.py (9), tests/test_abac_policy_condition.py (5), counted with grep -c "def test_". The e2e suite is web-client/_e2e_abac_policy_crud.mjs.

Interaction with other parts

Limits and open points

  • Environment attributes are UTC only; a time zone per tenant or per user is not determined from code (src/cluster_erp/modules/auth/guards.py:362-366).
  • The source of department, clearance, title, and location is not determined from code; they are not columns of AuthUser.

Terms introduced

Term Code identifier Definition Turkish term Source
ABAC policy AuthAbacPolicy A record of rule conditions that a holder of one permission code must also satisfy. ABAC politikası src/cluster_erp/modules/auth/policies/models.py:12
policy rule _evaluate_rule One subject, operator, and target comparison inside a policy condition. politika kuralı src/cluster_erp/modules/auth/guards.py:325
strict permission code abac_strict_permission_codes A code that is denied when no policy exists for it. katı izin kodu src/cluster_erp/config/settings.py:339
resource-enforced code RESOURCE_ENFORCED_CODES A code whose service evaluates resource rules after loading the entity. kaynakta uygulanan kod src/cluster_erp/modules/auth/guards.py:35

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

Verified against commit 92f8051 · 2026-11-13