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.
- {python_authz-0.5.0 → python_authz-0.7.0}/PKG-INFO +42 -7
- {python_authz-0.5.0 → python_authz-0.7.0}/README.md +42 -7
- {python_authz-0.5.0 → python_authz-0.7.0}/pyproject.toml +1 -1
- {python_authz-0.5.0 → python_authz-0.7.0}/pyproject.toml.orig +1 -1
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/__init__.py +13 -2
- python_authz-0.7.0/src/authz/conditions.py +113 -0
- python_authz-0.7.0/src/authz/engine.py +662 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/errors.py +25 -0
- python_authz-0.7.0/src/authz/integrations/fastapi.py +86 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/schema.py +51 -2
- python_authz-0.5.0/src/authz/conditions.py +0 -37
- python_authz-0.5.0/src/authz/engine.py +0 -297
- python_authz-0.5.0/src/authz/integrations/fastapi.py +0 -37
- {python_authz-0.5.0 → python_authz-0.7.0}/LICENSE +0 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/integrations/__init__.py +0 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/storage/__init__.py +0 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/storage/base.py +0 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/storage/postgres.py +0 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/storage/sqlite.py +0 -0
- {python_authz-0.5.0 → python_authz-0.7.0}/src/authz/tuples.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-authz
|
|
3
|
-
Version: 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
|
-
|
|
56
|
+
Release, version 0.7.0:
|
|
57
57
|
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
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,7 +1,12 @@
|
|
|
1
|
-
from authz.conditions import
|
|
2
|
-
|
|
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
|
+
}
|