python-authz 0.7.0__tar.gz → 0.8.1__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.7.0
3
+ Version: 0.8.1
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>
@@ -14,15 +14,16 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Topic :: Security
16
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
- Requires-Dist: duckdb>=1.5.5
17
+ Classifier: Typing :: Typed
18
18
  Requires-Dist: aiosqlite>=0.21.0
19
- Requires-Dist: asyncpg>=0.31.0
20
- Requires-Dist: fastapi[all]>=0.141.1 ; extra == 'fastapi'
19
+ Requires-Dist: fastapi>=0.141.1 ; extra == 'fastapi'
20
+ Requires-Dist: asyncpg>=0.31.0 ; extra == 'postgres'
21
21
  Requires-Python: >=3.11
22
- Project-URL: Homepage, https://github.com/mapuna/python-authz
23
- Project-URL: Repository, https://github.com/mapuna/python-authz
24
- Project-URL: Issues, https://github.com/mapuna/python-authz/issues
22
+ Project-URL: Homepage, https://github.com/tensor-forge-ai/python-authz
23
+ Project-URL: Repository, https://github.com/tensor-forge-ai/python-authz
24
+ Project-URL: Issues, https://github.com/tensor-forge-ai/python-authz/issues
25
25
  Provides-Extra: fastapi
26
+ Provides-Extra: postgres
26
27
  Description-Content-Type: text/markdown
27
28
 
28
29
  # authz
@@ -53,17 +54,18 @@ kept in agreement with each other.
53
54
 
54
55
  ## Status
55
56
 
56
- Release, version 0.7.0:
57
+ Release, version 0.8.1. Version 0.8.0 was a security release: [`docs/security.md`](docs/security.md) analyses the library end to end and records what changed, and [`CHANGELOG.md`](CHANGELOG.md) lists the upgrade notes. The library provides:
57
58
 
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.
59
+ - **Unified ReBAC + RBAC + ABAC**: Zanzibar-style relationship graphs with conditions evaluated against stored policy and request context. Stored context takes precedence.
60
+ - **Three-Valued Conditions**: a condition may return `None` when its context is missing or invalid; rewrite rules combine `True`, `False`, and `None` with Kleene logic, and a check allows only on `True`, so missing context never grants access, even under `Exclusion`.
61
+ - **Bounded Evaluation**: per-check memoization, `max_depth`, and an evaluation budget (`max_evaluations`) for `check()` and `expand()`.
62
+ - **Singleton and Cardinality Constraints**: `singleton_relations` and `max_subjects` on `Namespace`, enforced under storage locks so concurrent writers cannot exceed them.
63
+ - **Declarative Role Hierarchies**: `build_role_hierarchy(roles_high_to_low, extra_relations=...)` with transitive downward inheritance and `override_<role>` relations.
64
+ - **Audit and Explanation**: `Client.explain()` returns an `EvaluationDecision` with the visited relations, matched tuples, condition results, and a reason.
65
+ - **Batch Evaluation and Introspection**: `check_batch()`, `list_permissions()`, and `list_objects()`.
66
+ - **Built-in Conditions**: `max_amount`, `min_amount`, `equals_attr`, `in_set`, `jurisdiction_match`, `not_same_subject`, all fail-closed.
67
+ - **FastAPI Integration**: `requires()` and `explain_access()`, with resource placeholders resolved from path parameters unless `resource_sources` enables query parameters or headers.
68
+ - **Storage**: async SQLite and PostgreSQL backends with configurable table names and an option to connect without DDL privileges.
67
69
 
68
70
  ## Roadmap
69
71
 
@@ -73,13 +75,13 @@ Release, version 0.7.0:
73
75
  - JSON-Logic and CEL `ConditionEvaluator` implementations, alongside the
74
76
  existing Python-callable one
75
77
  - Java, Kotlin, Rust, and C++ SDKs
76
- - making `asyncpg`/`duckdb` and `pyjwt`/`httpx2` optional too, the way
77
- `fastapi` already is
78
78
 
79
79
  ## Install
80
80
 
81
81
  ```bash
82
- uv add python-authz
82
+ uv add python-authz # core, with SQLite storage
83
+ uv add "python-authz[postgres]" # adds PostgresStorage (asyncpg)
84
+ uv add "python-authz[fastapi]" # adds authz.integrations.fastapi
83
85
  ```
84
86
 
85
87
  ## Quickstart
@@ -132,7 +134,7 @@ deep versus something bypassed the guards.
132
134
 
133
135
  ## Role Hierarchies & Cardinality Constraints
134
136
 
135
- v0.7.0 adds first-class support for ordered role tiers with upward override relations and singleton limits:
137
+ Version 0.7.0 added first-class support for ordered role tiers with upward override relations and singleton limits:
136
138
 
137
139
  ```python
138
140
  from authz import Namespace, build_role_hierarchy, RelationTuple, CardinalityViolationError
@@ -193,9 +195,15 @@ caller receives a 403 automatically. See
193
195
  example, including how `get_client`/`get_subject` compose with your app's
194
196
  own database and auth setup.
195
197
 
198
+ ## Security
199
+
200
+ Read [`docs/security.md`](docs/security.md) for the threat model, the findings fixed in 0.8.0, and the responsibilities that stay with the application. Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md).
201
+
196
202
  ## Development
197
203
 
198
204
  ```bash
199
- uv sync --extra fastapi
205
+ uv sync --all-extras
200
206
  uv run pytest
201
207
  ```
208
+
209
+ The PostgreSQL tests use the server at `AUTHZ_TEST_POSTGRES_DSN` (a superuser DSN; the tests create and drop their own database), or start one with testcontainers when Docker is available, and are skipped otherwise.
@@ -26,17 +26,18 @@ kept in agreement with each other.
26
26
 
27
27
  ## Status
28
28
 
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.
29
+ Release, version 0.8.1. Version 0.8.0 was a security release: [`docs/security.md`](docs/security.md) analyses the library end to end and records what changed, and [`CHANGELOG.md`](CHANGELOG.md) lists the upgrade notes. The library provides:
30
+
31
+ - **Unified ReBAC + RBAC + ABAC**: Zanzibar-style relationship graphs with conditions evaluated against stored policy and request context. Stored context takes precedence.
32
+ - **Three-Valued Conditions**: a condition may return `None` when its context is missing or invalid; rewrite rules combine `True`, `False`, and `None` with Kleene logic, and a check allows only on `True`, so missing context never grants access, even under `Exclusion`.
33
+ - **Bounded Evaluation**: per-check memoization, `max_depth`, and an evaluation budget (`max_evaluations`) for `check()` and `expand()`.
34
+ - **Singleton and Cardinality Constraints**: `singleton_relations` and `max_subjects` on `Namespace`, enforced under storage locks so concurrent writers cannot exceed them.
35
+ - **Declarative Role Hierarchies**: `build_role_hierarchy(roles_high_to_low, extra_relations=...)` with transitive downward inheritance and `override_<role>` relations.
36
+ - **Audit and Explanation**: `Client.explain()` returns an `EvaluationDecision` with the visited relations, matched tuples, condition results, and a reason.
37
+ - **Batch Evaluation and Introspection**: `check_batch()`, `list_permissions()`, and `list_objects()`.
38
+ - **Built-in Conditions**: `max_amount`, `min_amount`, `equals_attr`, `in_set`, `jurisdiction_match`, `not_same_subject`, all fail-closed.
39
+ - **FastAPI Integration**: `requires()` and `explain_access()`, with resource placeholders resolved from path parameters unless `resource_sources` enables query parameters or headers.
40
+ - **Storage**: async SQLite and PostgreSQL backends with configurable table names and an option to connect without DDL privileges.
40
41
 
41
42
  ## Roadmap
42
43
 
@@ -46,13 +47,13 @@ Release, version 0.7.0:
46
47
  - JSON-Logic and CEL `ConditionEvaluator` implementations, alongside the
47
48
  existing Python-callable one
48
49
  - Java, Kotlin, Rust, and C++ SDKs
49
- - making `asyncpg`/`duckdb` and `pyjwt`/`httpx2` optional too, the way
50
- `fastapi` already is
51
50
 
52
51
  ## Install
53
52
 
54
53
  ```bash
55
- uv add python-authz
54
+ uv add python-authz # core, with SQLite storage
55
+ uv add "python-authz[postgres]" # adds PostgresStorage (asyncpg)
56
+ uv add "python-authz[fastapi]" # adds authz.integrations.fastapi
56
57
  ```
57
58
 
58
59
  ## Quickstart
@@ -105,7 +106,7 @@ deep versus something bypassed the guards.
105
106
 
106
107
  ## Role Hierarchies & Cardinality Constraints
107
108
 
108
- v0.7.0 adds first-class support for ordered role tiers with upward override relations and singleton limits:
109
+ Version 0.7.0 added first-class support for ordered role tiers with upward override relations and singleton limits:
109
110
 
110
111
  ```python
111
112
  from authz import Namespace, build_role_hierarchy, RelationTuple, CardinalityViolationError
@@ -166,9 +167,15 @@ caller receives a 403 automatically. See
166
167
  example, including how `get_client`/`get_subject` compose with your app's
167
168
  own database and auth setup.
168
169
 
170
+ ## Security
171
+
172
+ Read [`docs/security.md`](docs/security.md) for the threat model, the findings fixed in 0.8.0, and the responsibilities that stay with the application. Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md).
173
+
169
174
  ## Development
170
175
 
171
176
  ```bash
172
- uv sync --extra fastapi
177
+ uv sync --all-extras
173
178
  uv run pytest
174
179
  ```
180
+
181
+ The PostgreSQL tests use the server at `AUTHZ_TEST_POSTGRES_DSN` (a superuser DSN; the tests create and drop their own database), or start one with testcontainers when Docker is available, and are skipped otherwise.
@@ -1,16 +1,12 @@
1
1
  [project]
2
2
  name = "python-authz"
3
- version = "0.7.0"
3
+ version = "0.8.1"
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"
7
7
  license-files = ["LICENSE"]
8
8
  requires-python = ">=3.11"
9
- dependencies = [
10
- "duckdb>=1.5.5",
11
- "aiosqlite>=0.21.0",
12
- "asyncpg>=0.31.0",
13
- ]
9
+ dependencies = ["aiosqlite>=0.21.0"]
14
10
  classifiers = [
15
11
  "Development Status :: 4 - Beta",
16
12
  "Intended Audience :: Developers",
@@ -20,6 +16,7 @@ classifiers = [
20
16
  "Programming Language :: Python :: 3.13",
21
17
  "Topic :: Security",
22
18
  "Topic :: Software Development :: Libraries :: Python Modules",
19
+ "Typing :: Typed",
23
20
  ]
24
21
 
25
22
  [[project.authors]]
@@ -27,22 +24,23 @@ name = "Anupam Gupta"
27
24
  email = "anupam@trois.in"
28
25
 
29
26
  [project.urls]
30
- Homepage = "https://github.com/mapuna/python-authz"
31
- Repository = "https://github.com/mapuna/python-authz"
32
- Issues = "https://github.com/mapuna/python-authz/issues"
27
+ Homepage = "https://github.com/tensor-forge-ai/python-authz"
28
+ Repository = "https://github.com/tensor-forge-ai/python-authz"
29
+ Issues = "https://github.com/tensor-forge-ai/python-authz/issues"
33
30
 
34
31
  [project.optional-dependencies]
35
- fastapi = ["fastapi[all]>=0.141.1"]
36
-
37
- [project.scripts]
38
- authz = "authz:main"
32
+ fastapi = ["fastapi>=0.141.1"]
33
+ postgres = ["asyncpg>=0.31.0"]
39
34
 
40
35
  [dependency-groups]
41
36
  dev = [
37
+ "asyncpg>=0.31.0",
38
+ "fastapi>=0.141.1",
42
39
  "httpx2>=2.12.0",
43
40
  "pyjwt>=2.13.0",
44
41
  "pytest>=8.3.0",
45
42
  "pytest-asyncio>=0.24.0",
43
+ "python-multipart>=0.0.20",
46
44
  "testcontainers[postgres]>=4.15.0",
47
45
  ]
48
46
 
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "python-authz"
3
- version = "0.7.0"
3
+ version = "0.8.1"
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"
7
7
  license-files = ["LICENSE"]
8
8
  authors = [{ name = "Anupam Gupta", email = "anupam@trois.in" }]
9
9
  requires-python = ">=3.11"
10
- dependencies = ["duckdb>=1.5.5", "aiosqlite>=0.21.0", "asyncpg>=0.31.0"]
10
+ dependencies = ["aiosqlite>=0.21.0"]
11
11
  classifiers = [
12
12
  "Development Status :: 4 - Beta",
13
13
  "Intended Audience :: Developers",
@@ -17,28 +17,30 @@ classifiers = [
17
17
  "Programming Language :: Python :: 3.13",
18
18
  "Topic :: Security",
19
19
  "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Typing :: Typed",
20
21
  ]
21
22
 
22
23
  [project.urls]
23
- Homepage = "https://github.com/mapuna/python-authz"
24
- Repository = "https://github.com/mapuna/python-authz"
25
- Issues = "https://github.com/mapuna/python-authz/issues"
24
+ Homepage = "https://github.com/tensor-forge-ai/python-authz"
25
+ Repository = "https://github.com/tensor-forge-ai/python-authz"
26
+ Issues = "https://github.com/tensor-forge-ai/python-authz/issues"
26
27
 
27
28
  [project.optional-dependencies]
28
- fastapi = ["fastapi[all]>=0.141.1"]
29
+ fastapi = ["fastapi>=0.141.1"]
30
+ postgres = ["asyncpg>=0.31.0"]
29
31
 
30
32
  [dependency-groups]
31
33
  dev = [
34
+ "asyncpg>=0.31.0",
35
+ "fastapi>=0.141.1",
32
36
  "httpx2>=2.12.0",
33
37
  "pyjwt>=2.13.0",
34
38
  "pytest>=8.3.0",
35
39
  "pytest-asyncio>=0.24.0",
40
+ "python-multipart>=0.0.20",
36
41
  "testcontainers[postgres]>=4.15.0",
37
42
  ]
38
43
 
39
- [project.scripts]
40
- authz = "authz:main"
41
-
42
44
  [tool.uv.build-backend]
43
45
  module-name = "authz"
44
46
 
@@ -1,3 +1,5 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
1
3
  from authz.conditions import (
2
4
  ConditionEvaluator,
3
5
  PythonCallableEvaluator,
@@ -10,6 +12,7 @@ from authz.errors import (
10
12
  ConditionNotFound,
11
13
  CycleDetected,
12
14
  DepthLimitExceeded,
15
+ EvaluationBudgetExceeded,
13
16
  NamespaceNotFound,
14
17
  RelationNotFound,
15
18
  SchemaCycleDetected,
@@ -27,10 +30,12 @@ from authz.schema import (
27
30
  build_role_hierarchy,
28
31
  )
29
32
  from authz.storage.base import Storage, Token
30
- from authz.storage.postgres import PostgresStorage
31
33
  from authz.storage.sqlite import SqliteStorage
32
34
  from authz.tuples import Condition, ObjectRef, RelationTuple, SubjectRef
33
35
 
36
+ if TYPE_CHECKING:
37
+ from authz.storage.postgres import PostgresStorage
38
+
34
39
  __all__ = [
35
40
  "AuthError",
36
41
  "CardinalityViolationError",
@@ -42,6 +47,7 @@ __all__ = [
42
47
  "CycleDetected",
43
48
  "DepthLimitExceeded",
44
49
  "Direct",
50
+ "EvaluationBudgetExceeded",
45
51
  "EvaluationDecision",
46
52
  "Exclusion",
47
53
  "From",
@@ -49,7 +55,6 @@ __all__ = [
49
55
  "Namespace",
50
56
  "NamespaceNotFound",
51
57
  "ObjectRef",
52
- "PostgresStorage",
53
58
  "PythonCallableEvaluator",
54
59
  "RelationNotFound",
55
60
  "RelationTuple",
@@ -68,5 +73,14 @@ __all__ = [
68
73
  ]
69
74
 
70
75
 
71
- def main() -> None:
72
- print("Hello from authz!")
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}")
@@ -0,0 +1,159 @@
1
+ import inspect
2
+ import math
3
+ from typing import Any, Awaitable, Callable, Mapping, Protocol, Union
4
+
5
+ from authz.errors import ConditionNotFound
6
+
7
+ ConditionResult = Union[bool, None]
8
+ ConditionFn = Callable[..., Union[ConditionResult, Awaitable[ConditionResult]]]
9
+
10
+ _RESERVED_KEYS = frozenset({"subject", "resource"})
11
+
12
+
13
+ class ConditionEvaluator(Protocol):
14
+ def is_registered(self, name: str) -> bool: ...
15
+
16
+ async def evaluate(
17
+ self, name: str, subject: str, resource: str, context: Mapping[str, Any]
18
+ ) -> bool | None: ... # None: required context missing or invalid
19
+
20
+
21
+ class PythonCallableEvaluator:
22
+ def __init__(self) -> None:
23
+ self._conditions: dict[str, ConditionFn] = {}
24
+
25
+ @classmethod
26
+ def with_builtins(
27
+ cls, extra: Mapping[str, ConditionFn] | None = None
28
+ ) -> "PythonCallableEvaluator":
29
+ evaluator = cls()
30
+ for name, fn in builtin_conditions().items():
31
+ evaluator.register(name, fn)
32
+ if extra:
33
+ for name, fn in extra.items():
34
+ evaluator.register(name, fn)
35
+ return evaluator
36
+
37
+ def register(self, name: str, fn: ConditionFn) -> None:
38
+ self._conditions[name] = fn
39
+
40
+ def is_registered(self, name: str) -> bool:
41
+ return name in self._conditions
42
+
43
+ async def evaluate(
44
+ self, name: str, subject: str, resource: str, context: Mapping[str, Any]
45
+ ) -> bool | None:
46
+ try:
47
+ fn = self._conditions[name]
48
+ except KeyError:
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
+ )
59
+ result = fn(subject=subject, resource=resource, **context)
60
+ if inspect.isawaitable(result):
61
+ result = await result
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
+ )
68
+
69
+
70
+ def _number(value: Any) -> float | None:
71
+ if value is None or isinstance(value, bool):
72
+ return None
73
+ try:
74
+ number = float(value)
75
+ except (ValueError, TypeError, OverflowError):
76
+ return None
77
+ return number if math.isfinite(number) else None
78
+
79
+
80
+ def _collection(value: Any) -> Any:
81
+ return value if isinstance(value, (list, tuple, set, frozenset)) else None
82
+
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)
89
+
90
+
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
149
+
150
+
151
+ def builtin_conditions() -> dict[str, ConditionFn]:
152
+ return {
153
+ "max_amount": _max_amount,
154
+ "min_amount": _min_amount,
155
+ "equals_attr": _equals_attr,
156
+ "in_set": _in_set,
157
+ "jurisdiction_match": _jurisdiction_match,
158
+ "not_same_subject": _not_same_subject,
159
+ }