tba-protocol-authzen 0.4.0.dev2__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.
- tba_protocol_authzen-0.4.0.dev2/PKG-INFO +14 -0
- tba_protocol_authzen-0.4.0.dev2/pyproject.toml +17 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/__init__.py +66 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/bindings.py +79 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/claims.py +10 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/compatibility.py +38 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/errors.py +20 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/features.py +25 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/migrations.py +28 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/protocol.py +103 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/py.typed +1 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/schemas.py +191 -0
- tba_protocol_authzen-0.4.0.dev2/src/tba_protocol_authzen/versions.py +33 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tba-protocol-authzen
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
|
+
Summary: OpenID AuthZEN Authorization API protocol package.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10,<3.15
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: tba-capability-policy-evaluation (==0.4.0.dev2)
|
|
14
|
+
Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tba-protocol-authzen"
|
|
3
|
+
version = "0.4.0.dev2"
|
|
4
|
+
description = "OpenID AuthZEN Authorization API protocol package."
|
|
5
|
+
requires-python = ">=3.10,<3.15"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"tba-contracts-identity==0.4.0.dev2",
|
|
9
|
+
"tba-capability-policy-evaluation==0.4.0.dev2",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["poetry-core>=1.0.0"]
|
|
14
|
+
build-backend = "poetry.core.masonry.api"
|
|
15
|
+
|
|
16
|
+
[tool.poetry]
|
|
17
|
+
packages = [{ include = "tba_protocol_authzen", from = "src" }]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Versioned OpenID AuthZEN Authorization API ownership."""
|
|
2
|
+
|
|
3
|
+
from tba_contracts_identity.protocol_processing import (
|
|
4
|
+
build_protocol_capability_report,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .bindings import CAPABILITY_REQUIREMENTS
|
|
8
|
+
from .claims import AUTHZEN_CLAIM_CLASSES
|
|
9
|
+
from .compatibility import (
|
|
10
|
+
COMPATIBILITY_PATHS,
|
|
11
|
+
AuthzenCompatibility,
|
|
12
|
+
compatibility,
|
|
13
|
+
)
|
|
14
|
+
from .errors import (
|
|
15
|
+
AuthzenOperationUnavailableError,
|
|
16
|
+
AuthzenProtocolError,
|
|
17
|
+
AuthzenSchemaError,
|
|
18
|
+
)
|
|
19
|
+
from .features import FEATURES_BY_VERSION, supports
|
|
20
|
+
from .migrations import migrate_evaluation
|
|
21
|
+
from .protocol import AuthzenProtocol
|
|
22
|
+
from .schemas import (
|
|
23
|
+
AuthzenPdpMetadata,
|
|
24
|
+
parse_evaluation_request,
|
|
25
|
+
parse_search_request,
|
|
26
|
+
serialize_evaluation_result,
|
|
27
|
+
serialize_search_result,
|
|
28
|
+
)
|
|
29
|
+
from .versions import AuthzenVersion, CURRENT_VERSION, VERSION_HISTORY, select_version
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def capability_report() -> dict[str, object]:
|
|
33
|
+
return build_protocol_capability_report(
|
|
34
|
+
protocol="authzen",
|
|
35
|
+
revision=CURRENT_VERSION.identifier,
|
|
36
|
+
features=tuple(FEATURES_BY_VERSION[CURRENT_VERSION.identifier]),
|
|
37
|
+
evidence_links=("tests/unit/test_versioned_oid4vc_authzen_protocols.py",),
|
|
38
|
+
extra_requirements=CAPABILITY_REQUIREMENTS,
|
|
39
|
+
include_default_artifact_requirements=False,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AUTHZEN_CLAIM_CLASSES",
|
|
45
|
+
"CAPABILITY_REQUIREMENTS",
|
|
46
|
+
"COMPATIBILITY_PATHS",
|
|
47
|
+
"CURRENT_VERSION",
|
|
48
|
+
"FEATURES_BY_VERSION",
|
|
49
|
+
"VERSION_HISTORY",
|
|
50
|
+
"AuthzenCompatibility",
|
|
51
|
+
"AuthzenOperationUnavailableError",
|
|
52
|
+
"AuthzenPdpMetadata",
|
|
53
|
+
"AuthzenProtocol",
|
|
54
|
+
"AuthzenProtocolError",
|
|
55
|
+
"AuthzenSchemaError",
|
|
56
|
+
"AuthzenVersion",
|
|
57
|
+
"capability_report",
|
|
58
|
+
"compatibility",
|
|
59
|
+
"migrate_evaluation",
|
|
60
|
+
"parse_evaluation_request",
|
|
61
|
+
"parse_search_request",
|
|
62
|
+
"select_version",
|
|
63
|
+
"serialize_evaluation_result",
|
|
64
|
+
"serialize_search_result",
|
|
65
|
+
"supports",
|
|
66
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Authorization API 1.0 operations mapped to reportable capabilities."""
|
|
2
|
+
|
|
3
|
+
from tba_contracts_identity.capabilities import ProtocolCapabilityRequirement
|
|
4
|
+
|
|
5
|
+
from .versions import CURRENT_VERSION
|
|
6
|
+
|
|
7
|
+
CAPABILITY_REQUIREMENTS = (
|
|
8
|
+
ProtocolCapabilityRequirement(
|
|
9
|
+
"authzen",
|
|
10
|
+
CURRENT_VERSION.identifier,
|
|
11
|
+
"access-evaluation",
|
|
12
|
+
"/access/v1/evaluation",
|
|
13
|
+
"policy.evaluation",
|
|
14
|
+
"evaluate",
|
|
15
|
+
"policy-evaluation",
|
|
16
|
+
),
|
|
17
|
+
ProtocolCapabilityRequirement(
|
|
18
|
+
"authzen",
|
|
19
|
+
CURRENT_VERSION.identifier,
|
|
20
|
+
"access-evaluations",
|
|
21
|
+
"/access/v1/evaluations",
|
|
22
|
+
"policy.evaluation",
|
|
23
|
+
"evaluate_many",
|
|
24
|
+
"policy-evaluations",
|
|
25
|
+
False,
|
|
26
|
+
),
|
|
27
|
+
ProtocolCapabilityRequirement(
|
|
28
|
+
"authzen",
|
|
29
|
+
CURRENT_VERSION.identifier,
|
|
30
|
+
"search-subject",
|
|
31
|
+
"/access/v1/search/subject",
|
|
32
|
+
"policy.evaluation",
|
|
33
|
+
"search_entities",
|
|
34
|
+
"policy-subjects",
|
|
35
|
+
False,
|
|
36
|
+
),
|
|
37
|
+
ProtocolCapabilityRequirement(
|
|
38
|
+
"authzen",
|
|
39
|
+
CURRENT_VERSION.identifier,
|
|
40
|
+
"search-resource",
|
|
41
|
+
"/access/v1/search/resource",
|
|
42
|
+
"policy.evaluation",
|
|
43
|
+
"search_entities",
|
|
44
|
+
"policy-resources",
|
|
45
|
+
False,
|
|
46
|
+
),
|
|
47
|
+
ProtocolCapabilityRequirement(
|
|
48
|
+
"authzen",
|
|
49
|
+
CURRENT_VERSION.identifier,
|
|
50
|
+
"search-action",
|
|
51
|
+
"/access/v1/search/action",
|
|
52
|
+
"policy.evaluation",
|
|
53
|
+
"search_entities",
|
|
54
|
+
"policy-actions",
|
|
55
|
+
False,
|
|
56
|
+
),
|
|
57
|
+
ProtocolCapabilityRequirement(
|
|
58
|
+
"authzen",
|
|
59
|
+
CURRENT_VERSION.identifier,
|
|
60
|
+
"pdp-configuration",
|
|
61
|
+
"/.well-known/authzen-configuration",
|
|
62
|
+
"policy.evaluation",
|
|
63
|
+
"describe",
|
|
64
|
+
"policy-service-description",
|
|
65
|
+
False,
|
|
66
|
+
),
|
|
67
|
+
ProtocolCapabilityRequirement(
|
|
68
|
+
"authzen",
|
|
69
|
+
CURRENT_VERSION.identifier,
|
|
70
|
+
"signed-metadata",
|
|
71
|
+
"signed_metadata",
|
|
72
|
+
"artifact.processing",
|
|
73
|
+
"validate",
|
|
74
|
+
"verified-authzen-metadata",
|
|
75
|
+
False,
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = ["CAPABILITY_REQUIREMENTS"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""AuthZEN claim-set ownership declaration.
|
|
2
|
+
|
|
3
|
+
Authorization API 1.0 exchanges policy entities and decisions, not a JWT-style
|
|
4
|
+
registered claim set. Signed PDP metadata is a JWT carrier whose generic claims
|
|
5
|
+
remain owned by the JWT protocol package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
AUTHZEN_CLAIM_CLASSES: tuple[type[object], ...] = ()
|
|
9
|
+
|
|
10
|
+
__all__ = ["AUTHZEN_CLAIM_CLASSES"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Authorization API revision compatibility paths."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .versions import CURRENT_VERSION, VERSION_HISTORY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class AuthzenCompatibility:
|
|
10
|
+
source: str
|
|
11
|
+
target: str
|
|
12
|
+
compatible: bool
|
|
13
|
+
lossless: bool
|
|
14
|
+
migration_required: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
COMPATIBILITY_PATHS = tuple(
|
|
18
|
+
AuthzenCompatibility(
|
|
19
|
+
version.identifier,
|
|
20
|
+
CURRENT_VERSION.identifier,
|
|
21
|
+
True,
|
|
22
|
+
True,
|
|
23
|
+
version.identifier != CURRENT_VERSION.identifier,
|
|
24
|
+
)
|
|
25
|
+
for version in VERSION_HISTORY
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compatibility(
|
|
30
|
+
source: str, target: str = CURRENT_VERSION.identifier
|
|
31
|
+
) -> AuthzenCompatibility:
|
|
32
|
+
for path in COMPATIBILITY_PATHS:
|
|
33
|
+
if path.source == source and path.target == target:
|
|
34
|
+
return path
|
|
35
|
+
return AuthzenCompatibility(source, target, False, False, False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["COMPATIBILITY_PATHS", "AuthzenCompatibility", "compatibility"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Authorization API 1.0 mapping errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthzenProtocolError(ValueError):
|
|
5
|
+
"""Base error for invalid AuthZEN protocol material."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthzenSchemaError(AuthzenProtocolError):
|
|
9
|
+
"""Raised when an AuthZEN entity or message has the wrong shape."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthzenOperationUnavailableError(AuthzenProtocolError):
|
|
13
|
+
"""Raised when an optional PDP operation is not configured."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AuthzenOperationUnavailableError",
|
|
18
|
+
"AuthzenProtocolError",
|
|
19
|
+
"AuthzenSchemaError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
_EVALUATION = frozenset({"access-evaluation"})
|
|
2
|
+
_BATCH = _EVALUATION | {"access-evaluations"}
|
|
3
|
+
_SEARCH = _BATCH | {"search-subject", "search-resource", "search-action"}
|
|
4
|
+
_DISCOVERY = _SEARCH | {"configuration", "capabilities", "signed-metadata"}
|
|
5
|
+
|
|
6
|
+
FEATURES_BY_VERSION = {
|
|
7
|
+
"draft-00": _EVALUATION,
|
|
8
|
+
"draft-01": _EVALUATION,
|
|
9
|
+
"implementers-draft-1": _EVALUATION,
|
|
10
|
+
"draft-02": _BATCH,
|
|
11
|
+
"draft-03": _SEARCH,
|
|
12
|
+
"draft-04": _DISCOVERY,
|
|
13
|
+
"draft-05": _DISCOVERY,
|
|
14
|
+
"1.0": _DISCOVERY,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def supports(feature: str, version: str = "1.0") -> bool:
|
|
19
|
+
try:
|
|
20
|
+
return feature in FEATURES_BY_VERSION[version]
|
|
21
|
+
except KeyError as exc:
|
|
22
|
+
raise ValueError(f"unsupported AuthZEN version: {version}") from exc
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["FEATURES_BY_VERSION", "supports"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def migrate_evaluation(
|
|
5
|
+
payload: Mapping[str, object], from_version: str, to_version: str = "1.0"
|
|
6
|
+
) -> dict[str, object]:
|
|
7
|
+
result = dict(payload)
|
|
8
|
+
if from_version == to_version:
|
|
9
|
+
return result
|
|
10
|
+
if (
|
|
11
|
+
from_version
|
|
12
|
+
in {
|
|
13
|
+
"draft-00",
|
|
14
|
+
"draft-01",
|
|
15
|
+
"implementers-draft-1",
|
|
16
|
+
"draft-02",
|
|
17
|
+
"draft-03",
|
|
18
|
+
"draft-04",
|
|
19
|
+
"draft-05",
|
|
20
|
+
}
|
|
21
|
+
and to_version == "1.0"
|
|
22
|
+
):
|
|
23
|
+
result.setdefault("context", {})
|
|
24
|
+
return result
|
|
25
|
+
raise ValueError(f"no AuthZEN migration path from {from_version} to {to_version}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["migrate_evaluation"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Authorization API 1.0 mapping to neutral policy operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from tba_contracts_identity.policy import (
|
|
9
|
+
PolicyEntitySearchRequest,
|
|
10
|
+
PolicyEntitySearchResult,
|
|
11
|
+
PolicyEntitySearchTarget,
|
|
12
|
+
PolicyEvaluationRequest,
|
|
13
|
+
PolicyEvaluationResult,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .errors import AuthzenOperationUnavailableError, AuthzenSchemaError
|
|
17
|
+
from .schemas import (
|
|
18
|
+
parse_evaluation_request,
|
|
19
|
+
parse_search_request,
|
|
20
|
+
serialize_evaluation_result,
|
|
21
|
+
serialize_search_result,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _PolicyOperations(Protocol):
|
|
26
|
+
def evaluate(self, request: PolicyEvaluationRequest) -> PolicyEvaluationResult: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _merged_evaluations(
|
|
30
|
+
payload: Mapping[str, object],
|
|
31
|
+
) -> tuple[Mapping[str, object], ...]:
|
|
32
|
+
evaluations = payload.get("evaluations")
|
|
33
|
+
if evaluations is None or evaluations == []:
|
|
34
|
+
return (payload,)
|
|
35
|
+
if not isinstance(evaluations, Sequence) or isinstance(evaluations, (str, bytes)):
|
|
36
|
+
raise AuthzenSchemaError("AuthZEN evaluations must be an array")
|
|
37
|
+
defaults = {
|
|
38
|
+
name: payload[name]
|
|
39
|
+
for name in ("subject", "action", "resource", "context")
|
|
40
|
+
if name in payload
|
|
41
|
+
}
|
|
42
|
+
merged = []
|
|
43
|
+
for item in evaluations:
|
|
44
|
+
if not isinstance(item, Mapping):
|
|
45
|
+
raise AuthzenSchemaError("each AuthZEN evaluation must be an object")
|
|
46
|
+
merged.append({**defaults, **item})
|
|
47
|
+
return tuple(merged)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AuthzenProtocol:
|
|
51
|
+
version = "1.0"
|
|
52
|
+
|
|
53
|
+
def __init__(self, capability: _PolicyOperations):
|
|
54
|
+
self._capability = capability
|
|
55
|
+
|
|
56
|
+
def access_evaluation(self, payload: Mapping[str, object]) -> dict[str, object]:
|
|
57
|
+
return serialize_evaluation_result(
|
|
58
|
+
self._capability.evaluate(parse_evaluation_request(payload))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def access_evaluations(self, payload: Mapping[str, object]) -> dict[str, object]:
|
|
62
|
+
merged = _merged_evaluations(payload)
|
|
63
|
+
if len(merged) == 1 and not payload.get("evaluations"):
|
|
64
|
+
return self.access_evaluation(merged[0])
|
|
65
|
+
|
|
66
|
+
requests = tuple(parse_evaluation_request(item) for item in merged)
|
|
67
|
+
evaluate_many = getattr(self._capability, "evaluate_many", None)
|
|
68
|
+
results = (
|
|
69
|
+
evaluate_many(requests)
|
|
70
|
+
if callable(evaluate_many)
|
|
71
|
+
else tuple(self._capability.evaluate(request) for request in requests)
|
|
72
|
+
)
|
|
73
|
+
return {
|
|
74
|
+
"evaluations": [serialize_evaluation_result(result) for result in results]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def _search(
|
|
78
|
+
self,
|
|
79
|
+
payload: Mapping[str, object],
|
|
80
|
+
target: PolicyEntitySearchTarget,
|
|
81
|
+
) -> dict[str, object]:
|
|
82
|
+
search = getattr(self._capability, "search_entities", None)
|
|
83
|
+
if not callable(search):
|
|
84
|
+
raise AuthzenOperationUnavailableError(
|
|
85
|
+
f"AuthZEN {target.value} search is not configured"
|
|
86
|
+
)
|
|
87
|
+
request: PolicyEntitySearchRequest = parse_search_request(payload, target)
|
|
88
|
+
result = search(request)
|
|
89
|
+
if not isinstance(result, PolicyEntitySearchResult):
|
|
90
|
+
raise TypeError("policy search must return PolicyEntitySearchResult")
|
|
91
|
+
return serialize_search_result(result)
|
|
92
|
+
|
|
93
|
+
def search_subject(self, payload: Mapping[str, object]) -> dict[str, object]:
|
|
94
|
+
return self._search(payload, PolicyEntitySearchTarget.SUBJECT)
|
|
95
|
+
|
|
96
|
+
def search_resource(self, payload: Mapping[str, object]) -> dict[str, object]:
|
|
97
|
+
return self._search(payload, PolicyEntitySearchTarget.RESOURCE)
|
|
98
|
+
|
|
99
|
+
def search_action(self, payload: Mapping[str, object]) -> dict[str, object]:
|
|
100
|
+
return self._search(payload, PolicyEntitySearchTarget.ACTION)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["AuthzenProtocol"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Authorization API 1.0 wire-schema normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from tba_contracts_identity.policy import (
|
|
9
|
+
PolicyEntity,
|
|
10
|
+
PolicyEntityChain,
|
|
11
|
+
PolicyEntitySearchRequest,
|
|
12
|
+
PolicyEntitySearchResult,
|
|
13
|
+
PolicyEntitySearchTarget,
|
|
14
|
+
PolicyEvaluationRequest,
|
|
15
|
+
PolicyEvaluationResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .errors import AuthzenSchemaError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class AuthzenPdpMetadata:
|
|
23
|
+
policy_decision_point: str
|
|
24
|
+
access_evaluation_endpoint: str
|
|
25
|
+
access_evaluations_endpoint: str | None = None
|
|
26
|
+
search_subject_endpoint: str | None = None
|
|
27
|
+
search_resource_endpoint: str | None = None
|
|
28
|
+
search_action_endpoint: str | None = None
|
|
29
|
+
capabilities: tuple[str, ...] = ()
|
|
30
|
+
signed_metadata: str | None = None
|
|
31
|
+
|
|
32
|
+
def to_payload(self) -> dict[str, object]:
|
|
33
|
+
payload: dict[str, object] = {
|
|
34
|
+
"policy_decision_point": self.policy_decision_point,
|
|
35
|
+
"access_evaluation_endpoint": self.access_evaluation_endpoint,
|
|
36
|
+
}
|
|
37
|
+
for name in (
|
|
38
|
+
"access_evaluations_endpoint",
|
|
39
|
+
"search_subject_endpoint",
|
|
40
|
+
"search_resource_endpoint",
|
|
41
|
+
"search_action_endpoint",
|
|
42
|
+
"signed_metadata",
|
|
43
|
+
):
|
|
44
|
+
if (value := getattr(self, name)) is not None:
|
|
45
|
+
payload[name] = value
|
|
46
|
+
if self.capabilities:
|
|
47
|
+
payload["capabilities"] = list(self.capabilities)
|
|
48
|
+
return payload
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _object(value: object, name: str) -> Mapping[str, object]:
|
|
52
|
+
if not isinstance(value, Mapping):
|
|
53
|
+
raise AuthzenSchemaError(f"AuthZEN {name} must be an object")
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _properties(value: Mapping[str, object], name: str) -> Mapping[str, object]:
|
|
58
|
+
properties = value.get("properties", {})
|
|
59
|
+
if not isinstance(properties, Mapping):
|
|
60
|
+
raise AuthzenSchemaError(f"AuthZEN {name} properties must be an object")
|
|
61
|
+
return dict(properties)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _entity(
|
|
65
|
+
value: object,
|
|
66
|
+
name: str,
|
|
67
|
+
*,
|
|
68
|
+
identifier_required: bool = True,
|
|
69
|
+
) -> PolicyEntity:
|
|
70
|
+
item = _object(value, name)
|
|
71
|
+
if name == "action":
|
|
72
|
+
identifier = item.get("name")
|
|
73
|
+
if identifier_required and not isinstance(identifier, str):
|
|
74
|
+
raise AuthzenSchemaError("AuthZEN action requires name")
|
|
75
|
+
return PolicyEntity(
|
|
76
|
+
"action",
|
|
77
|
+
identifier if isinstance(identifier, str) else "",
|
|
78
|
+
_properties(item, name),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
entity_type = item.get("type")
|
|
82
|
+
identifier = item.get("id")
|
|
83
|
+
if not isinstance(entity_type, str):
|
|
84
|
+
raise AuthzenSchemaError(f"AuthZEN {name} requires type")
|
|
85
|
+
if identifier_required and not isinstance(identifier, str):
|
|
86
|
+
raise AuthzenSchemaError(f"AuthZEN {name} requires id")
|
|
87
|
+
return PolicyEntity(
|
|
88
|
+
entity_type,
|
|
89
|
+
identifier if isinstance(identifier, str) else "",
|
|
90
|
+
_properties(item, name),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parse_evaluation_request(
|
|
95
|
+
payload: Mapping[str, object],
|
|
96
|
+
) -> PolicyEvaluationRequest:
|
|
97
|
+
context = payload.get("context", {})
|
|
98
|
+
if not isinstance(context, Mapping):
|
|
99
|
+
raise AuthzenSchemaError("AuthZEN context must be an object")
|
|
100
|
+
return PolicyEvaluationRequest(
|
|
101
|
+
PolicyEntityChain((_entity(payload.get("subject"), "subject"),)),
|
|
102
|
+
PolicyEntityChain((_entity(payload.get("action"), "action"),)),
|
|
103
|
+
PolicyEntityChain((_entity(payload.get("resource"), "resource"),)),
|
|
104
|
+
dict(context),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_search_request(
|
|
109
|
+
payload: Mapping[str, object],
|
|
110
|
+
target: PolicyEntitySearchTarget,
|
|
111
|
+
) -> PolicyEntitySearchRequest:
|
|
112
|
+
context = payload.get("context", {})
|
|
113
|
+
if not isinstance(context, Mapping):
|
|
114
|
+
raise AuthzenSchemaError("AuthZEN context must be an object")
|
|
115
|
+
page = payload.get("page", {})
|
|
116
|
+
if not isinstance(page, Mapping):
|
|
117
|
+
raise AuthzenSchemaError("AuthZEN page must be an object")
|
|
118
|
+
token = page.get("token")
|
|
119
|
+
if token is not None and not isinstance(token, str):
|
|
120
|
+
raise AuthzenSchemaError("AuthZEN page token must be a string")
|
|
121
|
+
|
|
122
|
+
subject = _entity(
|
|
123
|
+
payload.get("subject"),
|
|
124
|
+
"subject",
|
|
125
|
+
identifier_required=target is not PolicyEntitySearchTarget.SUBJECT,
|
|
126
|
+
)
|
|
127
|
+
resource = _entity(
|
|
128
|
+
payload.get("resource"),
|
|
129
|
+
"resource",
|
|
130
|
+
identifier_required=target is not PolicyEntitySearchTarget.RESOURCE,
|
|
131
|
+
)
|
|
132
|
+
action = (
|
|
133
|
+
None
|
|
134
|
+
if target is PolicyEntitySearchTarget.ACTION
|
|
135
|
+
else _entity(payload.get("action"), "action")
|
|
136
|
+
)
|
|
137
|
+
return PolicyEntitySearchRequest(
|
|
138
|
+
target=target,
|
|
139
|
+
subject=subject,
|
|
140
|
+
action=action,
|
|
141
|
+
resource=resource,
|
|
142
|
+
context=dict(context),
|
|
143
|
+
page_token=token,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def serialize_evaluation_result(result: PolicyEvaluationResult) -> dict[str, object]:
|
|
148
|
+
payload: dict[str, object] = {"decision": result.decision.allowed}
|
|
149
|
+
context: dict[str, object] = {}
|
|
150
|
+
if result.decision.reason:
|
|
151
|
+
context["reason"] = result.decision.reason
|
|
152
|
+
if result.obligations:
|
|
153
|
+
context["obligations"] = list(result.obligations)
|
|
154
|
+
if result.advice:
|
|
155
|
+
context["advice"] = list(result.advice)
|
|
156
|
+
if context:
|
|
157
|
+
payload["context"] = context
|
|
158
|
+
return payload
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def serialize_search_result(result: PolicyEntitySearchResult) -> dict[str, object]:
|
|
162
|
+
entities = []
|
|
163
|
+
for entity in result.entities:
|
|
164
|
+
item: dict[str, object]
|
|
165
|
+
if entity.entity_type == "action":
|
|
166
|
+
item = {"name": entity.identifier}
|
|
167
|
+
else:
|
|
168
|
+
item = {"type": entity.entity_type, "id": entity.identifier}
|
|
169
|
+
if entity.properties:
|
|
170
|
+
item["properties"] = dict(entity.properties)
|
|
171
|
+
entities.append(item)
|
|
172
|
+
payload: dict[str, object] = {"results": entities}
|
|
173
|
+
page: dict[str, object] = {}
|
|
174
|
+
if result.next_page_token is not None:
|
|
175
|
+
page["next_token"] = result.next_page_token
|
|
176
|
+
if result.total is not None:
|
|
177
|
+
page["total"] = result.total
|
|
178
|
+
if page:
|
|
179
|
+
payload["page"] = page
|
|
180
|
+
if result.context:
|
|
181
|
+
payload["context"] = dict(result.context)
|
|
182
|
+
return payload
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
__all__ = [
|
|
186
|
+
"AuthzenPdpMetadata",
|
|
187
|
+
"parse_evaluation_request",
|
|
188
|
+
"parse_search_request",
|
|
189
|
+
"serialize_evaluation_result",
|
|
190
|
+
"serialize_search_result",
|
|
191
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True, slots=True)
|
|
5
|
+
class AuthzenVersion:
|
|
6
|
+
identifier: str
|
|
7
|
+
status: str
|
|
8
|
+
published: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VERSION_HISTORY = (
|
|
12
|
+
AuthzenVersion("draft-00", "superseded-draft", "2024-08-14"),
|
|
13
|
+
AuthzenVersion("draft-01", "superseded-draft", "2024-09-06"),
|
|
14
|
+
AuthzenVersion(
|
|
15
|
+
"implementers-draft-1", "superseded-implementers-draft", "2024-11-15"
|
|
16
|
+
),
|
|
17
|
+
AuthzenVersion("draft-02", "superseded-draft", "2025-01-23"),
|
|
18
|
+
AuthzenVersion("draft-03", "superseded-draft", "2025-03-18"),
|
|
19
|
+
AuthzenVersion("draft-04", "superseded-draft", "2025-10-06"),
|
|
20
|
+
AuthzenVersion("draft-05", "proposed-final-draft", "2025-11-08"),
|
|
21
|
+
AuthzenVersion("1.0", "final", "2026-01-11"),
|
|
22
|
+
)
|
|
23
|
+
CURRENT_VERSION = VERSION_HISTORY[-1]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def select_version(identifier: str = "1.0") -> AuthzenVersion:
|
|
27
|
+
for version in VERSION_HISTORY:
|
|
28
|
+
if version.identifier == identifier:
|
|
29
|
+
return version
|
|
30
|
+
raise ValueError(f"unsupported AuthZEN version: {identifier}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["AuthzenVersion", "CURRENT_VERSION", "VERSION_HISTORY", "select_version"]
|