tba-protocol-gnap 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_gnap-0.4.0.dev2/PKG-INFO +17 -0
- tba_protocol_gnap-0.4.0.dev2/pyproject.toml +18 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/__init__.py +55 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/bindings.py +57 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/claims.py +28 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/compatibility.py +38 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/errors.py +20 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/features.py +41 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/migrations.py +19 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/protocol.py +43 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/py.typed +0 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/schemas.py +119 -0
- tba_protocol_gnap-0.4.0.dev2/src/tba_protocol_gnap/versions.py +26 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tba-protocol-gnap
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
|
+
Summary: Versioned RFC 9635 Grant Negotiation and Authorization Protocol mapping.
|
|
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-delegated-access-negotiation (==0.4.0.dev2)
|
|
14
|
+
Requires-Dist: tba-concrete-claim-assertions (==0.4.0.dev2)
|
|
15
|
+
Requires-Dist: tba-concrete-claim-subject-identifiers (==0.4.0.dev2)
|
|
16
|
+
Requires-Dist: tba-concrete-claim-updated-at-rfc3339 (==0.4.0.dev2)
|
|
17
|
+
Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tba-protocol-gnap"
|
|
3
|
+
version = "0.4.0.dev2"
|
|
4
|
+
description = "Versioned RFC 9635 Grant Negotiation and Authorization Protocol mapping."
|
|
5
|
+
requires-python = ">=3.10,<3.15"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"tba-concrete-claim-assertions==0.4.0.dev2",
|
|
9
|
+
"tba-concrete-claim-subject-identifiers==0.4.0.dev2",
|
|
10
|
+
"tba-concrete-claim-updated-at-rfc3339==0.4.0.dev2",
|
|
11
|
+
"tba-capability-delegated-access-negotiation==0.4.0.dev2",
|
|
12
|
+
"tba-contracts-identity==0.4.0.dev2",
|
|
13
|
+
]
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["poetry-core>=1.0.0"]
|
|
16
|
+
build-backend = "poetry.core.masonry.api"
|
|
17
|
+
[tool.poetry]
|
|
18
|
+
packages = [{ include = "tba_protocol_gnap", from = "src" }]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Versioned RFC 9635 GNAP protocol 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 GNAP_SUBJECT_CLAIM_CLASSES, compose_gnap_subject_claim_set
|
|
9
|
+
from .compatibility import COMPATIBILITY_PATHS, GnapCompatibility, compatibility
|
|
10
|
+
from .errors import (
|
|
11
|
+
GnapOperationUnavailableError,
|
|
12
|
+
GnapProtocolError,
|
|
13
|
+
GnapSchemaError,
|
|
14
|
+
)
|
|
15
|
+
from .features import FEATURES_BY_VERSION, supports
|
|
16
|
+
from .migrations import migrate_request
|
|
17
|
+
from .protocol import GnapProtocol
|
|
18
|
+
from .schemas import GnapRequest, parse_gnap_request, serialize_gnap_result
|
|
19
|
+
from .versions import CURRENT_VERSION, VERSION_HISTORY, GnapVersion, select_version
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def capability_report() -> dict[str, object]:
|
|
23
|
+
return build_protocol_capability_report(
|
|
24
|
+
protocol="gnap",
|
|
25
|
+
revision=CURRENT_VERSION.identifier,
|
|
26
|
+
features=tuple(FEATURES_BY_VERSION[CURRENT_VERSION.identifier]),
|
|
27
|
+
evidence_links=("tests/unit/test_versioned_gnap_protocol.py",),
|
|
28
|
+
extra_requirements=CAPABILITY_REQUIREMENTS,
|
|
29
|
+
include_default_artifact_requirements=False,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CAPABILITY_REQUIREMENTS",
|
|
35
|
+
"COMPATIBILITY_PATHS",
|
|
36
|
+
"CURRENT_VERSION",
|
|
37
|
+
"FEATURES_BY_VERSION",
|
|
38
|
+
"GNAP_SUBJECT_CLAIM_CLASSES",
|
|
39
|
+
"VERSION_HISTORY",
|
|
40
|
+
"GnapCompatibility",
|
|
41
|
+
"GnapOperationUnavailableError",
|
|
42
|
+
"GnapProtocol",
|
|
43
|
+
"GnapProtocolError",
|
|
44
|
+
"GnapRequest",
|
|
45
|
+
"GnapSchemaError",
|
|
46
|
+
"GnapVersion",
|
|
47
|
+
"capability_report",
|
|
48
|
+
"compatibility",
|
|
49
|
+
"compose_gnap_subject_claim_set",
|
|
50
|
+
"migrate_request",
|
|
51
|
+
"parse_gnap_request",
|
|
52
|
+
"select_version",
|
|
53
|
+
"serialize_gnap_result",
|
|
54
|
+
"supports",
|
|
55
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""RFC 9635 wire 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
|
+
"gnap",
|
|
10
|
+
CURRENT_VERSION.identifier,
|
|
11
|
+
"grant-request",
|
|
12
|
+
"grant endpoint POST",
|
|
13
|
+
"grant.negotiation",
|
|
14
|
+
"request_grant",
|
|
15
|
+
"grant-negotiation-result",
|
|
16
|
+
),
|
|
17
|
+
ProtocolCapabilityRequirement(
|
|
18
|
+
"gnap",
|
|
19
|
+
CURRENT_VERSION.identifier,
|
|
20
|
+
"grant-continuation",
|
|
21
|
+
"continuation endpoint POST/PATCH/DELETE",
|
|
22
|
+
"grant.negotiation",
|
|
23
|
+
"continue_grant",
|
|
24
|
+
"grant-negotiation-result",
|
|
25
|
+
False,
|
|
26
|
+
),
|
|
27
|
+
ProtocolCapabilityRequirement(
|
|
28
|
+
"gnap",
|
|
29
|
+
CURRENT_VERSION.identifier,
|
|
30
|
+
"access-token-rotation",
|
|
31
|
+
"token management rotate",
|
|
32
|
+
"grant.negotiation",
|
|
33
|
+
"rotate_access_token",
|
|
34
|
+
"rotated-access-token",
|
|
35
|
+
False,
|
|
36
|
+
),
|
|
37
|
+
ProtocolCapabilityRequirement(
|
|
38
|
+
"gnap",
|
|
39
|
+
CURRENT_VERSION.identifier,
|
|
40
|
+
"client-key-proof",
|
|
41
|
+
"GNAP request signature",
|
|
42
|
+
"artifact.processing",
|
|
43
|
+
"validate",
|
|
44
|
+
"verified-gnap-key-proof",
|
|
45
|
+
),
|
|
46
|
+
ProtocolCapabilityRequirement(
|
|
47
|
+
"gnap",
|
|
48
|
+
CURRENT_VERSION.identifier,
|
|
49
|
+
"request-replay",
|
|
50
|
+
"GNAP request digest and proof",
|
|
51
|
+
"security.replay-protection",
|
|
52
|
+
"check_and_reserve",
|
|
53
|
+
"reserved-gnap-request",
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = ["CAPABILITY_REQUIREMENTS"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""RFC 9635 subject-information claim composition."""
|
|
2
|
+
|
|
3
|
+
from tba_concrete_claim_assertions import AssertionsClaim
|
|
4
|
+
from tba_concrete_claim_subject_identifiers import SubjectIdentifiersClaim
|
|
5
|
+
from tba_concrete_claim_updated_at_rfc3339 import UpdatedAtRfc3339Claim
|
|
6
|
+
from tba_contracts_identity.claims import ClaimSet
|
|
7
|
+
|
|
8
|
+
from .versions import CURRENT_VERSION
|
|
9
|
+
|
|
10
|
+
GNAP_SUBJECT_CLAIM_CLASSES = (
|
|
11
|
+
SubjectIdentifiersClaim,
|
|
12
|
+
AssertionsClaim,
|
|
13
|
+
UpdatedAtRfc3339Claim,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compose_gnap_subject_claim_set(*claims: object) -> ClaimSet:
|
|
18
|
+
unexpected = [
|
|
19
|
+
claim.name
|
|
20
|
+
for claim in claims
|
|
21
|
+
if not isinstance(claim, GNAP_SUBJECT_CLAIM_CLASSES)
|
|
22
|
+
]
|
|
23
|
+
if unexpected:
|
|
24
|
+
raise ValueError(f"claims are not RFC 9635 subject fields: {unexpected}")
|
|
25
|
+
return ClaimSet(tuple(claims), "gnap-subject", CURRENT_VERSION.identifier)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["GNAP_SUBJECT_CLAIM_CLASSES", "compose_gnap_subject_claim_set"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""GNAP draft-to-RFC 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 GnapCompatibility:
|
|
10
|
+
source: str
|
|
11
|
+
target: str
|
|
12
|
+
compatible: bool
|
|
13
|
+
lossless: bool
|
|
14
|
+
migration_required: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
COMPATIBILITY_PATHS = tuple(
|
|
18
|
+
GnapCompatibility(
|
|
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
|
+
) -> GnapCompatibility:
|
|
32
|
+
for path in COMPATIBILITY_PATHS:
|
|
33
|
+
if path.source == source and path.target == target:
|
|
34
|
+
return path
|
|
35
|
+
return GnapCompatibility(source, target, False, False, False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["COMPATIBILITY_PATHS", "GnapCompatibility", "compatibility"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""RFC 9635 protocol mapping errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GnapProtocolError(ValueError):
|
|
5
|
+
"""Base error for invalid GNAP protocol material."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GnapSchemaError(GnapProtocolError):
|
|
9
|
+
"""Raised when a GNAP request or response has an invalid shape."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GnapOperationUnavailableError(GnapProtocolError):
|
|
13
|
+
"""Raised when an optional grant lifecycle operation is unavailable."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"GnapOperationUnavailableError",
|
|
18
|
+
"GnapProtocolError",
|
|
19
|
+
"GnapSchemaError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
FEATURES_BY_VERSION = {
|
|
2
|
+
"draft-13": frozenset(
|
|
3
|
+
{"access-token-request", "client-instance", "interaction", "continuation"}
|
|
4
|
+
),
|
|
5
|
+
"draft-ietf-gnap-core-protocol-20": frozenset(
|
|
6
|
+
{
|
|
7
|
+
"access-token-request",
|
|
8
|
+
"client-instance",
|
|
9
|
+
"interaction",
|
|
10
|
+
"continuation",
|
|
11
|
+
"subject-information",
|
|
12
|
+
"token-rotation",
|
|
13
|
+
}
|
|
14
|
+
),
|
|
15
|
+
"RFC9635": frozenset(
|
|
16
|
+
{
|
|
17
|
+
"access-token-request",
|
|
18
|
+
"client-instance",
|
|
19
|
+
"interaction",
|
|
20
|
+
"continuation",
|
|
21
|
+
"subject-information",
|
|
22
|
+
"token-rotation",
|
|
23
|
+
"key-proofing",
|
|
24
|
+
"multiple-access-tokens",
|
|
25
|
+
"grant-modification",
|
|
26
|
+
"token-management",
|
|
27
|
+
"authorization-server-discovery",
|
|
28
|
+
"subject-identifiers-rfc9493",
|
|
29
|
+
}
|
|
30
|
+
),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def supports(feature: str, version: str = "RFC9635") -> bool:
|
|
35
|
+
try:
|
|
36
|
+
return feature in FEATURES_BY_VERSION[version]
|
|
37
|
+
except KeyError as exc:
|
|
38
|
+
raise ValueError(f"unsupported GNAP version: {version}") from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["FEATURES_BY_VERSION", "supports"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Any, Mapping
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def migrate_request(
|
|
5
|
+
value: Mapping[str, Any], *, source: str, target: str = "RFC9635"
|
|
6
|
+
) -> dict[str, Any]:
|
|
7
|
+
if source == target:
|
|
8
|
+
return dict(value)
|
|
9
|
+
if (
|
|
10
|
+
source not in {"draft-13", "draft-ietf-gnap-core-protocol-20"}
|
|
11
|
+
or target != "RFC9635"
|
|
12
|
+
):
|
|
13
|
+
raise ValueError(f"unsupported GNAP migration: {source} -> {target}")
|
|
14
|
+
# The supported draft request members retained their RFC 9635 wire names.
|
|
15
|
+
# Migration metadata is deliberately not injected into protocol payloads.
|
|
16
|
+
return dict(value)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["migrate_request"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""RFC 9635 mapping to the neutral grant-negotiation capability."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tba_capability_delegated_access_negotiation import GrantNegotiationCapability
|
|
9
|
+
from tba_contracts_identity.gnap import GrantContinuationRequest
|
|
10
|
+
|
|
11
|
+
from .schemas import GnapRequest, parse_gnap_request, serialize_gnap_result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GnapProtocol:
|
|
15
|
+
version = "RFC9635"
|
|
16
|
+
|
|
17
|
+
def __init__(self, capability: GrantNegotiationCapability):
|
|
18
|
+
self._capability = capability
|
|
19
|
+
|
|
20
|
+
async def grant(self, payload: Mapping[str, Any]) -> dict[str, object]:
|
|
21
|
+
request = parse_gnap_request(payload).to_contract()
|
|
22
|
+
return serialize_gnap_result(await self._capability.request(request))
|
|
23
|
+
|
|
24
|
+
async def continue_grant(
|
|
25
|
+
self,
|
|
26
|
+
continuation_token: str,
|
|
27
|
+
*,
|
|
28
|
+
proof: str | bytes | Mapping[str, object] | None = None,
|
|
29
|
+
) -> dict[str, object]:
|
|
30
|
+
request = GrantContinuationRequest(continuation_token, proof)
|
|
31
|
+
return serialize_gnap_result(await self._capability.continue_request(request))
|
|
32
|
+
|
|
33
|
+
async def rotate_access_token(
|
|
34
|
+
self,
|
|
35
|
+
management_token: str,
|
|
36
|
+
*,
|
|
37
|
+
proof: str | bytes | Mapping[str, object] | None = None,
|
|
38
|
+
) -> dict[str, object]:
|
|
39
|
+
request = GrantContinuationRequest(management_token, proof)
|
|
40
|
+
return serialize_gnap_result(await self._capability.rotate(request))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["GnapProtocol", "GnapRequest", "parse_gnap_request"]
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""RFC 9635 request and response schema normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from tba_contracts_identity.gnap import (
|
|
10
|
+
GrantAccessRequest,
|
|
11
|
+
GrantNegotiationRequest,
|
|
12
|
+
GrantNegotiationResult,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .errors import GnapSchemaError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class GnapRequest:
|
|
20
|
+
access_token: tuple[Mapping[str, Any], ...]
|
|
21
|
+
client: Mapping[str, Any] | str
|
|
22
|
+
interact: Mapping[str, Any] | None = None
|
|
23
|
+
subject: Mapping[str, Any] | None = None
|
|
24
|
+
|
|
25
|
+
def to_contract(self) -> GrantNegotiationRequest:
|
|
26
|
+
requests = tuple(
|
|
27
|
+
GrantAccessRequest(
|
|
28
|
+
tuple(item["access"]),
|
|
29
|
+
label=item.get("label"),
|
|
30
|
+
flags=tuple(item.get("flags", ())),
|
|
31
|
+
)
|
|
32
|
+
for item in self.access_token
|
|
33
|
+
)
|
|
34
|
+
return GrantNegotiationRequest(
|
|
35
|
+
requests,
|
|
36
|
+
self.client,
|
|
37
|
+
dict(self.interact) if self.interact is not None else None,
|
|
38
|
+
dict(self.subject) if self.subject is not None else None,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _access_requests(value: object) -> tuple[Mapping[str, Any], ...]:
|
|
43
|
+
if value is None:
|
|
44
|
+
return ()
|
|
45
|
+
items = (value,) if isinstance(value, Mapping) else value
|
|
46
|
+
if not isinstance(items, Sequence) or isinstance(items, (str, bytes)):
|
|
47
|
+
raise GnapSchemaError("GNAP access_token must be an object or array")
|
|
48
|
+
requests = []
|
|
49
|
+
for item in items:
|
|
50
|
+
if not isinstance(item, Mapping):
|
|
51
|
+
raise GnapSchemaError("GNAP access_token entries must be objects")
|
|
52
|
+
access = item.get("access")
|
|
53
|
+
if (
|
|
54
|
+
not isinstance(access, Sequence)
|
|
55
|
+
or isinstance(access, (str, bytes))
|
|
56
|
+
or not access
|
|
57
|
+
):
|
|
58
|
+
raise GnapSchemaError("GNAP access token request requires access")
|
|
59
|
+
label = item.get("label")
|
|
60
|
+
if label is not None and not isinstance(label, str):
|
|
61
|
+
raise GnapSchemaError("GNAP access token label must be a string")
|
|
62
|
+
flags = item.get("flags", ())
|
|
63
|
+
if (
|
|
64
|
+
not isinstance(flags, Sequence)
|
|
65
|
+
or isinstance(flags, (str, bytes))
|
|
66
|
+
or any(not isinstance(flag, str) for flag in flags)
|
|
67
|
+
or len(set(flags)) != len(flags)
|
|
68
|
+
):
|
|
69
|
+
raise GnapSchemaError("GNAP access token flags must be unique strings")
|
|
70
|
+
requests.append(dict(item))
|
|
71
|
+
labels = [item.get("label") for item in requests if item.get("label") is not None]
|
|
72
|
+
if len(requests) > 1 and len(labels) != len(requests):
|
|
73
|
+
raise GnapSchemaError("multiple GNAP access token requests require labels")
|
|
74
|
+
if len(set(labels)) != len(labels):
|
|
75
|
+
raise GnapSchemaError("GNAP access token labels must be unique")
|
|
76
|
+
return tuple(requests)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_gnap_request(value: Mapping[str, Any]) -> GnapRequest:
|
|
80
|
+
access = _access_requests(value.get("access_token"))
|
|
81
|
+
subject = value.get("subject")
|
|
82
|
+
if subject is not None and not isinstance(subject, Mapping):
|
|
83
|
+
raise GnapSchemaError("GNAP subject must be an object")
|
|
84
|
+
if not access and subject is None:
|
|
85
|
+
raise GnapSchemaError("GNAP request requires access_token or subject")
|
|
86
|
+
client = value.get("client")
|
|
87
|
+
if not isinstance(client, (Mapping, str)):
|
|
88
|
+
raise GnapSchemaError("GNAP request requires a client instance")
|
|
89
|
+
interact = value.get("interact")
|
|
90
|
+
if interact is not None and not isinstance(interact, Mapping):
|
|
91
|
+
raise GnapSchemaError("GNAP interact must be an object")
|
|
92
|
+
return GnapRequest(
|
|
93
|
+
access,
|
|
94
|
+
dict(client) if isinstance(client, Mapping) else client,
|
|
95
|
+
dict(interact) if isinstance(interact, Mapping) else None,
|
|
96
|
+
dict(subject) if isinstance(subject, Mapping) else None,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def serialize_gnap_result(result: GrantNegotiationResult) -> dict[str, object]:
|
|
101
|
+
payload: dict[str, object] = {}
|
|
102
|
+
if result.continuation is not None:
|
|
103
|
+
payload["continue"] = dict(result.continuation)
|
|
104
|
+
if result.access_tokens:
|
|
105
|
+
payload["access_token"] = (
|
|
106
|
+
dict(result.access_tokens[0])
|
|
107
|
+
if len(result.access_tokens) == 1
|
|
108
|
+
else [dict(token) for token in result.access_tokens]
|
|
109
|
+
)
|
|
110
|
+
if result.interaction is not None:
|
|
111
|
+
payload["interact"] = dict(result.interaction)
|
|
112
|
+
if result.subject is not None:
|
|
113
|
+
payload["subject"] = dict(result.subject)
|
|
114
|
+
if result.metadata:
|
|
115
|
+
payload.update(result.metadata)
|
|
116
|
+
return payload
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = ["GnapRequest", "parse_gnap_request", "serialize_gnap_result"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True, slots=True)
|
|
5
|
+
class GnapVersion:
|
|
6
|
+
identifier: str
|
|
7
|
+
status: str
|
|
8
|
+
published: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VERSION_HISTORY = (
|
|
12
|
+
GnapVersion("draft-13", "superseded-draft", "2023-03-13"),
|
|
13
|
+
GnapVersion("draft-ietf-gnap-core-protocol-20", "superseded-draft", "2024-04-24"),
|
|
14
|
+
GnapVersion("RFC9635", "proposed-standard", "2024-10"),
|
|
15
|
+
)
|
|
16
|
+
CURRENT_VERSION = VERSION_HISTORY[-1]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def select_version(identifier: str = "RFC9635") -> GnapVersion:
|
|
20
|
+
for version in VERSION_HISTORY:
|
|
21
|
+
if version.identifier == identifier:
|
|
22
|
+
return version
|
|
23
|
+
raise ValueError(f"unsupported GNAP version: {identifier}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["CURRENT_VERSION", "VERSION_HISTORY", "GnapVersion", "select_version"]
|