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_codeand stores aconditionof 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
subjectattribute path, anoperator, and atargetthat 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 --> doneIn prose:
- The route guard calls
check_abacwithresource=None. Rules that name aresource.*attribute are left out at this point;user.*andenv.*rules are decided (src/cluster_erp/modules/auth/guards.py:225-236,src/cluster_erp/modules/auth/guards.py:306-322). - After it loads the entity, a service calls
enforce_abacwith the entity's attributes, soresource.*rules are decided too (src/cluster_erp/modules/auth/guards.py:284-303). - Only codes in
RESOURCE_ENFORCED_CODESare wired to this second step. At this commit the set holds one code,intranet_official_delivery.read, and its caller issrc/cluster_erp/modules/intranet/delivery/routes.py:38(src/cluster_erp/modules/auth/guards.py:29-39). PolicyServicerefuses to store aresource.*rule for any other code, with HTTP 422 and codeABAC_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 undernot_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
PolicyRulesnapshot, 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 withgrep -c "def test_". The e2e suite isweb-client/_e2e_abac_policy_crud.mjs.
Interaction with other parts¶
- The intranet official-delivery route is the one post-load caller (
src/cluster_erp/modules/intranet/delivery/routes.py:38). See intranet access, audience, and retention. - Row-level scoping for personal records is a separate mechanism; see field and row access.
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, andlocationis not determined from code; they are not columns ofAuthUser.
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 |
Related pages¶
- Identity, access, tenancy, privacy, and licensing
- Roles and permissions
- Field and row access
- Auth module
Verified against commit 92f8051 on 2026-11-13.