python-authz 0.5.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-authz
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: A framework-agnostic ReBAC/RBAC/ABAC authorization engine built on one Zanzibar-style relationship graph
5
5
  Author: Anupam Gupta
6
6
  Author-email: Anupam Gupta <anupam@trois.in>
@@ -53,13 +53,17 @@ kept in agreement with each other.
53
53
 
54
54
  ## Status
55
55
 
56
- Early release, version 0.5:
56
+ Release, version 0.7.0:
57
57
 
58
- - the core engine, async SQLite and PostgreSQL storage backends, and the
59
- optional FastAPI `Depends()` integration (`authz.integrations.fastapi`,
60
- installed via `python-authz[fastapi]`)
61
- - a reference sample app (`examples/sample_app/`) and OAuth2/JWT auth
62
- integration recipes (`examples/auth_integrations/`)
58
+ - **Singleton & Cardinality Constraints**: Enforce subject limits on relations (e.g. at most 1 `org_admin`) via `singleton_relations` and `max_subjects` on `Namespace`, raising `CardinalityViolationError` on violations while maintaining idempotent rewrites.
59
+ - **Declarative Role Hierarchies & Strict Upward Overrides**: `build_role_hierarchy(roles_high_to_low, extra_relations=...)` creates multi-tier role hierarchies with transitive downward inheritance and strict upward override relations (`override_<role>`).
60
+ - **Unified ReBAC + RBAC + ABAC**: Zanzibar-style relationship graphs with dynamic runtime ABAC context.
61
+ - **Audit Trail & Policy Explanation**: `Client.explain()` returning `EvaluationDecision` with traversed paths, matched tuples, condition evaluations, and human-readable reasons.
62
+ - **Batch Evaluation & Caching**: `Client.check_batch()` evaluating sequences of `CheckItem`s with per-batch ephemeral read caching.
63
+ - **Introspection**: `Client.list_permissions()` and `Client.list_objects()` for resource and subject discovery.
64
+ - **Declarative Built-in ABAC Predicates**: `builtin_conditions()` and `PythonCallableEvaluator.with_builtins()` (`max_amount`, `min_amount`, `equals_attr`, `in_set`, `jurisdiction_match`, `not_same_subject`).
65
+ - **Enhanced FastAPI Integration**: `requires()` with `get_context`, path/query/header resource parameter resolution, and `explain_access()` storing decisions on `request.state.authz_decision`.
66
+ - Async SQLite and PostgreSQL storage backends.
63
67
 
64
68
  ## Roadmap
65
69
 
@@ -126,6 +130,37 @@ It covers schema vs. data cycles, which rewrite rule (`Union`,
126
130
  situation, and when a depth-limit error means your data is legitimately
127
131
  deep versus something bypassed the guards.
128
132
 
133
+ ## Role Hierarchies & Cardinality Constraints
134
+
135
+ v0.7.0 adds first-class support for ordered role tiers with upward override relations and singleton limits:
136
+
137
+ ```python
138
+ from authz import Namespace, build_role_hierarchy, RelationTuple, CardinalityViolationError
139
+
140
+ # Build 5-tier role relations with downward inheritance and override_<role> relations
141
+ roles = build_role_hierarchy([
142
+ "org_admin",
143
+ "org_manager",
144
+ "tpa_coordinator",
145
+ "org_user",
146
+ "diagnostic_billing_staff",
147
+ ])
148
+
149
+ # Enforce singleton constraint: an organization can have at most ONE org_admin
150
+ org_ns = Namespace("org", roles, singleton_relations={"org_admin"})
151
+ client.register_schema(org_ns)
152
+
153
+ # Alice is assigned org_admin -> inherits org_manager, tpa_coordinator, etc.
154
+ await client.write([RelationTuple("org", "acme", "org_admin", "user:alice")])
155
+ assert await client.check("user:alice", "org_user", "org:acme") is True
156
+
157
+ # Bob cannot also be assigned org_admin on the same org
158
+ try:
159
+ await client.write([RelationTuple("org", "acme", "org_admin", "user:bob")])
160
+ except CardinalityViolationError as e:
161
+ print(f"Singleton violation: {e}")
162
+ ```
163
+
129
164
  ## FastAPI Integration Example
130
165
 
131
166
  A route needs one thing from an authorization library: block the request
@@ -26,13 +26,17 @@ kept in agreement with each other.
26
26
 
27
27
  ## Status
28
28
 
29
- Early release, version 0.5:
30
-
31
- - the core engine, async SQLite and PostgreSQL storage backends, and the
32
- optional FastAPI `Depends()` integration (`authz.integrations.fastapi`,
33
- installed via `python-authz[fastapi]`)
34
- - a reference sample app (`examples/sample_app/`) and OAuth2/JWT auth
35
- integration recipes (`examples/auth_integrations/`)
29
+ Release, version 0.7.0:
30
+
31
+ - **Singleton & Cardinality Constraints**: Enforce subject limits on relations (e.g. at most 1 `org_admin`) via `singleton_relations` and `max_subjects` on `Namespace`, raising `CardinalityViolationError` on violations while maintaining idempotent rewrites.
32
+ - **Declarative Role Hierarchies & Strict Upward Overrides**: `build_role_hierarchy(roles_high_to_low, extra_relations=...)` creates multi-tier role hierarchies with transitive downward inheritance and strict upward override relations (`override_<role>`).
33
+ - **Unified ReBAC + RBAC + ABAC**: Zanzibar-style relationship graphs with dynamic runtime ABAC context.
34
+ - **Audit Trail & Policy Explanation**: `Client.explain()` returning `EvaluationDecision` with traversed paths, matched tuples, condition evaluations, and human-readable reasons.
35
+ - **Batch Evaluation & Caching**: `Client.check_batch()` evaluating sequences of `CheckItem`s with per-batch ephemeral read caching.
36
+ - **Introspection**: `Client.list_permissions()` and `Client.list_objects()` for resource and subject discovery.
37
+ - **Declarative Built-in ABAC Predicates**: `builtin_conditions()` and `PythonCallableEvaluator.with_builtins()` (`max_amount`, `min_amount`, `equals_attr`, `in_set`, `jurisdiction_match`, `not_same_subject`).
38
+ - **Enhanced FastAPI Integration**: `requires()` with `get_context`, path/query/header resource parameter resolution, and `explain_access()` storing decisions on `request.state.authz_decision`.
39
+ - Async SQLite and PostgreSQL storage backends.
36
40
 
37
41
  ## Roadmap
38
42
 
@@ -99,6 +103,37 @@ It covers schema vs. data cycles, which rewrite rule (`Union`,
99
103
  situation, and when a depth-limit error means your data is legitimately
100
104
  deep versus something bypassed the guards.
101
105
 
106
+ ## Role Hierarchies & Cardinality Constraints
107
+
108
+ v0.7.0 adds first-class support for ordered role tiers with upward override relations and singleton limits:
109
+
110
+ ```python
111
+ from authz import Namespace, build_role_hierarchy, RelationTuple, CardinalityViolationError
112
+
113
+ # Build 5-tier role relations with downward inheritance and override_<role> relations
114
+ roles = build_role_hierarchy([
115
+ "org_admin",
116
+ "org_manager",
117
+ "tpa_coordinator",
118
+ "org_user",
119
+ "diagnostic_billing_staff",
120
+ ])
121
+
122
+ # Enforce singleton constraint: an organization can have at most ONE org_admin
123
+ org_ns = Namespace("org", roles, singleton_relations={"org_admin"})
124
+ client.register_schema(org_ns)
125
+
126
+ # Alice is assigned org_admin -> inherits org_manager, tpa_coordinator, etc.
127
+ await client.write([RelationTuple("org", "acme", "org_admin", "user:alice")])
128
+ assert await client.check("user:alice", "org_user", "org:acme") is True
129
+
130
+ # Bob cannot also be assigned org_admin on the same org
131
+ try:
132
+ await client.write([RelationTuple("org", "acme", "org_admin", "user:bob")])
133
+ except CardinalityViolationError as e:
134
+ print(f"Singleton violation: {e}")
135
+ ```
136
+
102
137
  ## FastAPI Integration Example
103
138
 
104
139
  A route needs one thing from an authorization library: block the request
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-authz"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "A framework-agnostic ReBAC/RBAC/ABAC authorization engine built on one Zanzibar-style relationship graph"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-authz"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "A framework-agnostic ReBAC/RBAC/ABAC authorization engine built on one Zanzibar-style relationship graph"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,7 +1,12 @@
1
- from authz.conditions import ConditionEvaluator, PythonCallableEvaluator
2
- from authz.engine import Client, UsersetTree
1
+ from authz.conditions import (
2
+ ConditionEvaluator,
3
+ PythonCallableEvaluator,
4
+ builtin_conditions,
5
+ )
6
+ from authz.engine import CheckItem, Client, EvaluationDecision, UsersetTree
3
7
  from authz.errors import (
4
8
  AuthError,
9
+ CardinalityViolationError,
5
10
  ConditionNotFound,
6
11
  CycleDetected,
7
12
  DepthLimitExceeded,
@@ -19,6 +24,7 @@ from authz.schema import (
19
24
  RewriteRule,
20
25
  TupleToUserset,
21
26
  Union,
27
+ build_role_hierarchy,
22
28
  )
23
29
  from authz.storage.base import Storage, Token
24
30
  from authz.storage.postgres import PostgresStorage
@@ -27,6 +33,8 @@ from authz.tuples import Condition, ObjectRef, RelationTuple, SubjectRef
27
33
 
28
34
  __all__ = [
29
35
  "AuthError",
36
+ "CardinalityViolationError",
37
+ "CheckItem",
30
38
  "Client",
31
39
  "Condition",
32
40
  "ConditionEvaluator",
@@ -34,6 +42,7 @@ __all__ = [
34
42
  "CycleDetected",
35
43
  "DepthLimitExceeded",
36
44
  "Direct",
45
+ "EvaluationDecision",
37
46
  "Exclusion",
38
47
  "From",
39
48
  "Intersection",
@@ -54,6 +63,8 @@ __all__ = [
54
63
  "Union",
55
64
  "UsersetTree",
56
65
  "WouldCreateCycle",
66
+ "build_role_hierarchy",
67
+ "builtin_conditions",
57
68
  ]
58
69
 
59
70
 
@@ -0,0 +1,113 @@
1
+ import inspect
2
+ from typing import Any, Awaitable, Callable, Mapping, Protocol, Union
3
+
4
+ from authz.errors import ConditionNotFound
5
+
6
+ ConditionFn = Callable[..., Union[bool, Awaitable[bool]]]
7
+
8
+
9
+ class ConditionEvaluator(Protocol):
10
+ def is_registered(self, name: str) -> bool: ...
11
+
12
+ async def evaluate(
13
+ self, name: str, subject: str, resource: str, context: Mapping[str, Any]
14
+ ) -> bool: ...
15
+
16
+
17
+ class PythonCallableEvaluator:
18
+ def __init__(self) -> None:
19
+ self._conditions: dict[str, ConditionFn] = {}
20
+
21
+ @classmethod
22
+ def with_builtins(
23
+ cls, extra: Mapping[str, ConditionFn] | None = None
24
+ ) -> "PythonCallableEvaluator":
25
+ evaluator = cls()
26
+ for name, fn in builtin_conditions().items():
27
+ evaluator.register(name, fn)
28
+ if extra:
29
+ for name, fn in extra.items():
30
+ evaluator.register(name, fn)
31
+ return evaluator
32
+
33
+ def register(self, name: str, fn: ConditionFn) -> None:
34
+ self._conditions[name] = fn
35
+
36
+ def is_registered(self, name: str) -> bool:
37
+ return name in self._conditions
38
+
39
+ async def evaluate(
40
+ self, name: str, subject: str, resource: str, context: Mapping[str, Any]
41
+ ) -> bool:
42
+ try:
43
+ fn = self._conditions[name]
44
+ except KeyError:
45
+ raise ConditionNotFound(name) from None
46
+ result = fn(subject=subject, resource=resource, **context)
47
+ if inspect.isawaitable(result):
48
+ result = await result
49
+ return bool(result)
50
+
51
+
52
+ def _max_amount(*, subject: str, resource: str, **ctx: Any) -> bool:
53
+ try:
54
+ amount = float(ctx.get("amount", 0))
55
+ except (ValueError, TypeError):
56
+ amount = 0.0
57
+ try:
58
+ max_amount = float(ctx.get("max_amount", float("inf")))
59
+ except (ValueError, TypeError):
60
+ max_amount = float("inf")
61
+ return amount <= max_amount
62
+
63
+
64
+ def _min_amount(*, subject: str, resource: str, **ctx: Any) -> bool:
65
+ try:
66
+ amount = float(ctx.get("amount", 0))
67
+ except (ValueError, TypeError):
68
+ amount = 0.0
69
+ try:
70
+ min_amount = float(ctx.get("min_amount", 0))
71
+ except (ValueError, TypeError):
72
+ min_amount = 0.0
73
+ return amount >= min_amount
74
+
75
+
76
+ def _equals_attr(*, subject: str, resource: str, **ctx: Any) -> bool:
77
+ attr_name = str(ctx.get("attribute", ""))
78
+ return ctx.get(attr_name) == ctx.get("expected")
79
+
80
+
81
+ def _in_set(*, subject: str, resource: str, **ctx: Any) -> bool:
82
+ attr_name = str(ctx.get("attribute", ""))
83
+ allowed = ctx.get("allowed_values")
84
+ if allowed is None:
85
+ allowed = ()
86
+ return ctx.get(attr_name) in allowed
87
+
88
+
89
+ def _jurisdiction_match(*, subject: str, resource: str, **ctx: Any) -> bool:
90
+ jur = ctx.get("jurisdiction")
91
+ if "allowed_jurisdictions" in ctx:
92
+ allowed = ctx.get("allowed_jurisdictions")
93
+ if allowed is None:
94
+ allowed = ()
95
+ return jur in allowed
96
+ if "required_jurisdiction" in ctx:
97
+ return jur == ctx.get("required_jurisdiction")
98
+ return True
99
+
100
+
101
+ def _not_same_subject(*, subject: str, resource: str, **ctx: Any) -> bool:
102
+ return subject != ctx.get("excluded_subject")
103
+
104
+
105
+ def builtin_conditions() -> dict[str, ConditionFn]:
106
+ return {
107
+ "max_amount": _max_amount,
108
+ "min_amount": _min_amount,
109
+ "equals_attr": _equals_attr,
110
+ "in_set": _in_set,
111
+ "jurisdiction_match": _jurisdiction_match,
112
+ "not_same_subject": _not_same_subject,
113
+ }