python-authz 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
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.
- authz/__init__.py +22 -4
- authz/conditions.py +94 -48
- authz/engine.py +330 -208
- authz/errors.py +31 -0
- authz/integrations/fastapi.py +107 -36
- authz/py.typed +0 -0
- authz/schema.py +63 -11
- authz/storage/base.py +5 -0
- authz/storage/postgres.py +81 -35
- authz/storage/sqlite.py +86 -42
- authz/tuples.py +43 -1
- {python_authz-0.6.0.dist-info → python_authz-0.8.0.dist-info}/METADATA +60 -19
- python_authz-0.8.0.dist-info/RECORD +17 -0
- {python_authz-0.6.0.dist-info → python_authz-0.8.0.dist-info}/WHEEL +1 -1
- python_authz-0.6.0.dist-info/RECORD +0 -17
- python_authz-0.6.0.dist-info/entry_points.txt +0 -3
- {python_authz-0.6.0.dist-info → python_authz-0.8.0.dist-info}/licenses/LICENSE +0 -0
authz/__init__.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
1
3
|
from authz.conditions import (
|
|
2
4
|
ConditionEvaluator,
|
|
3
5
|
PythonCallableEvaluator,
|
|
@@ -6,9 +8,11 @@ from authz.conditions import (
|
|
|
6
8
|
from authz.engine import CheckItem, Client, EvaluationDecision, UsersetTree
|
|
7
9
|
from authz.errors import (
|
|
8
10
|
AuthError,
|
|
11
|
+
CardinalityViolationError,
|
|
9
12
|
ConditionNotFound,
|
|
10
13
|
CycleDetected,
|
|
11
14
|
DepthLimitExceeded,
|
|
15
|
+
EvaluationBudgetExceeded,
|
|
12
16
|
NamespaceNotFound,
|
|
13
17
|
RelationNotFound,
|
|
14
18
|
SchemaCycleDetected,
|
|
@@ -23,14 +27,18 @@ from authz.schema import (
|
|
|
23
27
|
RewriteRule,
|
|
24
28
|
TupleToUserset,
|
|
25
29
|
Union,
|
|
30
|
+
build_role_hierarchy,
|
|
26
31
|
)
|
|
27
32
|
from authz.storage.base import Storage, Token
|
|
28
|
-
from authz.storage.postgres import PostgresStorage
|
|
29
33
|
from authz.storage.sqlite import SqliteStorage
|
|
30
34
|
from authz.tuples import Condition, ObjectRef, RelationTuple, SubjectRef
|
|
31
35
|
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from authz.storage.postgres import PostgresStorage
|
|
38
|
+
|
|
32
39
|
__all__ = [
|
|
33
40
|
"AuthError",
|
|
41
|
+
"CardinalityViolationError",
|
|
34
42
|
"CheckItem",
|
|
35
43
|
"Client",
|
|
36
44
|
"Condition",
|
|
@@ -39,6 +47,7 @@ __all__ = [
|
|
|
39
47
|
"CycleDetected",
|
|
40
48
|
"DepthLimitExceeded",
|
|
41
49
|
"Direct",
|
|
50
|
+
"EvaluationBudgetExceeded",
|
|
42
51
|
"EvaluationDecision",
|
|
43
52
|
"Exclusion",
|
|
44
53
|
"From",
|
|
@@ -46,7 +55,6 @@ __all__ = [
|
|
|
46
55
|
"Namespace",
|
|
47
56
|
"NamespaceNotFound",
|
|
48
57
|
"ObjectRef",
|
|
49
|
-
"PostgresStorage",
|
|
50
58
|
"PythonCallableEvaluator",
|
|
51
59
|
"RelationNotFound",
|
|
52
60
|
"RelationTuple",
|
|
@@ -60,9 +68,19 @@ __all__ = [
|
|
|
60
68
|
"Union",
|
|
61
69
|
"UsersetTree",
|
|
62
70
|
"WouldCreateCycle",
|
|
71
|
+
"build_role_hierarchy",
|
|
63
72
|
"builtin_conditions",
|
|
64
73
|
]
|
|
65
74
|
|
|
66
75
|
|
|
67
|
-
def
|
|
68
|
-
|
|
76
|
+
def __getattr__(name: str) -> Any:
|
|
77
|
+
if name == "PostgresStorage":
|
|
78
|
+
try:
|
|
79
|
+
from authz.storage.postgres import PostgresStorage
|
|
80
|
+
except ImportError as exc:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"PostgresStorage requires asyncpg; install it with "
|
|
83
|
+
"'pip install python-authz[postgres]'"
|
|
84
|
+
) from exc
|
|
85
|
+
return PostgresStorage
|
|
86
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
authz/conditions.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import math
|
|
2
3
|
from typing import Any, Awaitable, Callable, Mapping, Protocol, Union
|
|
3
4
|
|
|
4
5
|
from authz.errors import ConditionNotFound
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
ConditionResult = Union[bool, None]
|
|
8
|
+
ConditionFn = Callable[..., Union[ConditionResult, Awaitable[ConditionResult]]]
|
|
9
|
+
|
|
10
|
+
_RESERVED_KEYS = frozenset({"subject", "resource"})
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
class ConditionEvaluator(Protocol):
|
|
@@ -11,7 +15,7 @@ class ConditionEvaluator(Protocol):
|
|
|
11
15
|
|
|
12
16
|
async def evaluate(
|
|
13
17
|
self, name: str, subject: str, resource: str, context: Mapping[str, Any]
|
|
14
|
-
) -> bool: ...
|
|
18
|
+
) -> bool | None: ... # None: required context missing or invalid
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class PythonCallableEvaluator:
|
|
@@ -38,68 +42,110 @@ class PythonCallableEvaluator:
|
|
|
38
42
|
|
|
39
43
|
async def evaluate(
|
|
40
44
|
self, name: str, subject: str, resource: str, context: Mapping[str, Any]
|
|
41
|
-
) -> bool:
|
|
45
|
+
) -> bool | None:
|
|
42
46
|
try:
|
|
43
47
|
fn = self._conditions[name]
|
|
44
48
|
except KeyError:
|
|
45
49
|
raise ConditionNotFound(name) from None
|
|
50
|
+
for key in context:
|
|
51
|
+
if not isinstance(key, str):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"condition {name!r}: context keys must be strings, got {key!r}"
|
|
54
|
+
)
|
|
55
|
+
if key in _RESERVED_KEYS:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"condition {name!r}: context key {key!r} is reserved"
|
|
58
|
+
)
|
|
46
59
|
result = fn(subject=subject, resource=resource, **context)
|
|
47
60
|
if inspect.isawaitable(result):
|
|
48
61
|
result = await result
|
|
49
|
-
|
|
62
|
+
if result is None or isinstance(result, bool):
|
|
63
|
+
return result
|
|
64
|
+
raise TypeError(
|
|
65
|
+
f"condition {name!r} must return True, False or None, "
|
|
66
|
+
f"got {type(result).__name__}"
|
|
67
|
+
)
|
|
50
68
|
|
|
51
69
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
except (ValueError, TypeError):
|
|
56
|
-
amount = 0.0
|
|
70
|
+
def _number(value: Any) -> float | None:
|
|
71
|
+
if value is None or isinstance(value, bool):
|
|
72
|
+
return None
|
|
57
73
|
try:
|
|
58
|
-
|
|
59
|
-
except (ValueError, TypeError):
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
|
|
74
|
+
number = float(value)
|
|
75
|
+
except (ValueError, TypeError, OverflowError):
|
|
76
|
+
return None
|
|
77
|
+
return number if math.isfinite(number) else None
|
|
63
78
|
|
|
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
79
|
|
|
80
|
+
def _collection(value: Any) -> Any:
|
|
81
|
+
return value if isinstance(value, (list, tuple, set, frozenset)) else None
|
|
75
82
|
|
|
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
83
|
|
|
84
|
+
def _attribute(ctx: Mapping[str, Any]) -> Any:
|
|
85
|
+
name = ctx.get("attribute")
|
|
86
|
+
if not isinstance(name, str) or not name:
|
|
87
|
+
return None
|
|
88
|
+
return ctx.get(name)
|
|
80
89
|
|
|
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
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
def _member(value: Any, allowed: Any) -> bool | None:
|
|
92
|
+
try:
|
|
93
|
+
return value in allowed
|
|
94
|
+
except TypeError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _max_amount(*, subject: str, resource: str, **ctx: Any) -> bool | None:
|
|
99
|
+
amount, limit = _number(ctx.get("amount")), _number(ctx.get("max_amount"))
|
|
100
|
+
if amount is None or limit is None:
|
|
101
|
+
return None
|
|
102
|
+
return amount <= limit
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _min_amount(*, subject: str, resource: str, **ctx: Any) -> bool | None:
|
|
106
|
+
amount, limit = _number(ctx.get("amount")), _number(ctx.get("min_amount"))
|
|
107
|
+
if amount is None or limit is None:
|
|
108
|
+
return None
|
|
109
|
+
return amount >= limit
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _equals_attr(*, subject: str, resource: str, **ctx: Any) -> bool | None:
|
|
113
|
+
actual, expected = _attribute(ctx), ctx.get("expected")
|
|
114
|
+
if actual is None or expected is None:
|
|
115
|
+
return None
|
|
116
|
+
return actual == expected
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _in_set(*, subject: str, resource: str, **ctx: Any) -> bool | None:
|
|
120
|
+
actual, allowed = _attribute(ctx), _collection(ctx.get("allowed_values"))
|
|
121
|
+
if actual is None or allowed is None:
|
|
122
|
+
return None
|
|
123
|
+
return _member(actual, allowed)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _jurisdiction_match(*, subject: str, resource: str, **ctx: Any) -> bool | None:
|
|
127
|
+
jurisdiction = ctx.get("jurisdiction")
|
|
128
|
+
if jurisdiction is None:
|
|
129
|
+
return None
|
|
130
|
+
allowed, required = ctx.get("allowed_jurisdictions"), ctx.get("required_jurisdiction")
|
|
131
|
+
if allowed is None and required is None:
|
|
132
|
+
return None
|
|
133
|
+
results = []
|
|
134
|
+
if allowed is not None:
|
|
135
|
+
allowed = _collection(allowed)
|
|
136
|
+
results.append(None if allowed is None else _member(jurisdiction, allowed))
|
|
137
|
+
if required is not None:
|
|
138
|
+
results.append(jurisdiction == required)
|
|
139
|
+
if False in results:
|
|
140
|
+
return False
|
|
141
|
+
return None if None in results else True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _not_same_subject(*, subject: str, resource: str, **ctx: Any) -> bool | None:
|
|
145
|
+
excluded = ctx.get("excluded_subject")
|
|
146
|
+
if not isinstance(excluded, str) or not excluded:
|
|
147
|
+
return None
|
|
148
|
+
return subject != excluded
|
|
103
149
|
|
|
104
150
|
|
|
105
151
|
def builtin_conditions() -> dict[str, ConditionFn]:
|