arrow-local-uri 0.1.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.
- arrow_local_uri-0.1.0/PKG-INFO +32 -0
- arrow_local_uri-0.1.0/README.md +22 -0
- arrow_local_uri-0.1.0/arrow_local_uri.egg-info/PKG-INFO +32 -0
- arrow_local_uri-0.1.0/arrow_local_uri.egg-info/SOURCES.txt +29 -0
- arrow_local_uri-0.1.0/arrow_local_uri.egg-info/dependency_links.txt +1 -0
- arrow_local_uri-0.1.0/arrow_local_uri.egg-info/requires.txt +1 -0
- arrow_local_uri-0.1.0/arrow_local_uri.egg-info/top_level.txt +1 -0
- arrow_local_uri-0.1.0/arrow_uri/__init__.py +133 -0
- arrow_local_uri-0.1.0/arrow_uri/audit.py +128 -0
- arrow_local_uri-0.1.0/arrow_uri/context.py +87 -0
- arrow_local_uri-0.1.0/arrow_uri/delegation.py +141 -0
- arrow_local_uri-0.1.0/arrow_uri/elevation.py +299 -0
- arrow_local_uri-0.1.0/arrow_uri/elevation_uri.py +547 -0
- arrow_local_uri-0.1.0/arrow_uri/envelope.py +103 -0
- arrow_local_uri-0.1.0/arrow_uri/errors.py +29 -0
- arrow_local_uri-0.1.0/arrow_uri/helper.py +186 -0
- arrow_local_uri-0.1.0/arrow_uri/identity.py +78 -0
- arrow_local_uri-0.1.0/arrow_uri/json_types.py +8 -0
- arrow_local_uri-0.1.0/arrow_uri/keys.py +246 -0
- arrow_local_uri-0.1.0/arrow_uri/models.py +184 -0
- arrow_local_uri-0.1.0/arrow_uri/oauth.py +299 -0
- arrow_local_uri-0.1.0/arrow_uri/protection.py +127 -0
- arrow_local_uri-0.1.0/arrow_uri/revocation.py +48 -0
- arrow_local_uri-0.1.0/arrow_uri/secrets.py +136 -0
- arrow_local_uri-0.1.0/arrow_uri/security.py +457 -0
- arrow_local_uri-0.1.0/arrow_uri/trace.py +110 -0
- arrow_local_uri-0.1.0/arrow_uri/trust.py +144 -0
- arrow_local_uri-0.1.0/arrow_uri/uri.py +322 -0
- arrow_local_uri-0.1.0/arrow_uri/validity.py +215 -0
- arrow_local_uri-0.1.0/pyproject.toml +16 -0
- arrow_local_uri-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arrow-local-uri
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local URI trust, authentication, validity, tracing, and elevation framework.
|
|
5
|
+
Author: Joao Lopes
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pydantic>=2.10
|
|
10
|
+
|
|
11
|
+
# Arrow
|
|
12
|
+
|
|
13
|
+
Arrow is an independent local URI trust boundary for Python applications. It
|
|
14
|
+
provides signed and expiring envelopes, authentication strength, provenance,
|
|
15
|
+
capabilities, validity, replay protection, revocation, delegation, tracing,
|
|
16
|
+
redaction, rate limiting, idempotency, OAuth primitives, and native OS
|
|
17
|
+
privilege elevation.
|
|
18
|
+
|
|
19
|
+
Arrow contains no Lake Terminal storage or session-management logic.
|
|
20
|
+
|
|
21
|
+
<!-- exactus:inventory:start -->
|
|
22
|
+
## Directory contents
|
|
23
|
+
|
|
24
|
+
### Files
|
|
25
|
+
|
|
26
|
+
- [`pyproject.toml`](pyproject.toml) — Provide pyproject toml content.
|
|
27
|
+
|
|
28
|
+
### Subdirectories
|
|
29
|
+
|
|
30
|
+
- [`arrow_uri/`](arrow_uri/README.md) — Arrow public interface.
|
|
31
|
+
|
|
32
|
+
<!-- exactus:inventory:end -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Arrow
|
|
2
|
+
|
|
3
|
+
Arrow is an independent local URI trust boundary for Python applications. It
|
|
4
|
+
provides signed and expiring envelopes, authentication strength, provenance,
|
|
5
|
+
capabilities, validity, replay protection, revocation, delegation, tracing,
|
|
6
|
+
redaction, rate limiting, idempotency, OAuth primitives, and native OS
|
|
7
|
+
privilege elevation.
|
|
8
|
+
|
|
9
|
+
Arrow contains no Lake Terminal storage or session-management logic.
|
|
10
|
+
|
|
11
|
+
<!-- exactus:inventory:start -->
|
|
12
|
+
## Directory contents
|
|
13
|
+
|
|
14
|
+
### Files
|
|
15
|
+
|
|
16
|
+
- [`pyproject.toml`](pyproject.toml) — Provide pyproject toml content.
|
|
17
|
+
|
|
18
|
+
### Subdirectories
|
|
19
|
+
|
|
20
|
+
- [`arrow_uri/`](arrow_uri/README.md) — Arrow public interface.
|
|
21
|
+
|
|
22
|
+
<!-- exactus:inventory:end -->
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arrow-local-uri
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local URI trust, authentication, validity, tracing, and elevation framework.
|
|
5
|
+
Author: Joao Lopes
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pydantic>=2.10
|
|
10
|
+
|
|
11
|
+
# Arrow
|
|
12
|
+
|
|
13
|
+
Arrow is an independent local URI trust boundary for Python applications. It
|
|
14
|
+
provides signed and expiring envelopes, authentication strength, provenance,
|
|
15
|
+
capabilities, validity, replay protection, revocation, delegation, tracing,
|
|
16
|
+
redaction, rate limiting, idempotency, OAuth primitives, and native OS
|
|
17
|
+
privilege elevation.
|
|
18
|
+
|
|
19
|
+
Arrow contains no Lake Terminal storage or session-management logic.
|
|
20
|
+
|
|
21
|
+
<!-- exactus:inventory:start -->
|
|
22
|
+
## Directory contents
|
|
23
|
+
|
|
24
|
+
### Files
|
|
25
|
+
|
|
26
|
+
- [`pyproject.toml`](pyproject.toml) — Provide pyproject toml content.
|
|
27
|
+
|
|
28
|
+
### Subdirectories
|
|
29
|
+
|
|
30
|
+
- [`arrow_uri/`](arrow_uri/README.md) — Arrow public interface.
|
|
31
|
+
|
|
32
|
+
<!-- exactus:inventory:end -->
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
arrow_local_uri.egg-info/PKG-INFO
|
|
4
|
+
arrow_local_uri.egg-info/SOURCES.txt
|
|
5
|
+
arrow_local_uri.egg-info/dependency_links.txt
|
|
6
|
+
arrow_local_uri.egg-info/requires.txt
|
|
7
|
+
arrow_local_uri.egg-info/top_level.txt
|
|
8
|
+
arrow_uri/__init__.py
|
|
9
|
+
arrow_uri/audit.py
|
|
10
|
+
arrow_uri/context.py
|
|
11
|
+
arrow_uri/delegation.py
|
|
12
|
+
arrow_uri/elevation.py
|
|
13
|
+
arrow_uri/elevation_uri.py
|
|
14
|
+
arrow_uri/envelope.py
|
|
15
|
+
arrow_uri/errors.py
|
|
16
|
+
arrow_uri/helper.py
|
|
17
|
+
arrow_uri/identity.py
|
|
18
|
+
arrow_uri/json_types.py
|
|
19
|
+
arrow_uri/keys.py
|
|
20
|
+
arrow_uri/models.py
|
|
21
|
+
arrow_uri/oauth.py
|
|
22
|
+
arrow_uri/protection.py
|
|
23
|
+
arrow_uri/revocation.py
|
|
24
|
+
arrow_uri/secrets.py
|
|
25
|
+
arrow_uri/security.py
|
|
26
|
+
arrow_uri/trace.py
|
|
27
|
+
arrow_uri/trust.py
|
|
28
|
+
arrow_uri/uri.py
|
|
29
|
+
arrow_uri/validity.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pydantic>=2.10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
arrow_uri
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Arrow public interface."""
|
|
2
|
+
|
|
3
|
+
from .audit import AuditEvent, AuditRecord, HashChainAuditLog
|
|
4
|
+
from .context import Deadline, RequestContext, RequestIdentity
|
|
5
|
+
from .delegation import Delegation, DelegationManager, DelegationRequest
|
|
6
|
+
from .elevation import ElevationGrant, PrivilegeBroker
|
|
7
|
+
from .elevation_uri import (
|
|
8
|
+
ElevationEnvelope,
|
|
9
|
+
ElevationURIManager,
|
|
10
|
+
PrivilegedOperation,
|
|
11
|
+
PrivilegedRegistry,
|
|
12
|
+
)
|
|
13
|
+
from .envelope import EnvelopeSigner, SignedEnvelope
|
|
14
|
+
from .helper import HelperGrant, HelperGrantSigner
|
|
15
|
+
from .identity import ExecutablePolicy, PeerIdentityProvider
|
|
16
|
+
from .keys import KeyRing, KeyStatus, SigningKey
|
|
17
|
+
from .models import (
|
|
18
|
+
CallerInfo,
|
|
19
|
+
SecurityContext,
|
|
20
|
+
TraceContext,
|
|
21
|
+
URIRequest,
|
|
22
|
+
URIResponse,
|
|
23
|
+
URIStatus,
|
|
24
|
+
)
|
|
25
|
+
from .oauth import (
|
|
26
|
+
AuthorizationRequest,
|
|
27
|
+
DeviceAuthorization,
|
|
28
|
+
OAuthClient,
|
|
29
|
+
OAuthTransport,
|
|
30
|
+
PKCEPair,
|
|
31
|
+
TokenSet,
|
|
32
|
+
)
|
|
33
|
+
from .protection import IdempotencyStore, RateLimiter, Redactor
|
|
34
|
+
from .revocation import RevocationRegistry
|
|
35
|
+
from .secrets import MemorySecretStore, RefreshTokenVault, SecretStore
|
|
36
|
+
from .security import (
|
|
37
|
+
AuthorizationPolicy,
|
|
38
|
+
NonceStore,
|
|
39
|
+
OAuthToken,
|
|
40
|
+
OAuthTokenValidator,
|
|
41
|
+
SessionClaims,
|
|
42
|
+
SignedSessionManager,
|
|
43
|
+
StaticOAuthValidator,
|
|
44
|
+
)
|
|
45
|
+
from .trace import Span, SpanStatus, Trace
|
|
46
|
+
from .trust import (
|
|
47
|
+
AuthenticationStrength,
|
|
48
|
+
Evidence,
|
|
49
|
+
EvidenceKind,
|
|
50
|
+
Principal,
|
|
51
|
+
TrustLevel,
|
|
52
|
+
)
|
|
53
|
+
from .uri import (
|
|
54
|
+
SignedURIManager,
|
|
55
|
+
SignedURIRequest,
|
|
56
|
+
URIValue,
|
|
57
|
+
URIValues,
|
|
58
|
+
)
|
|
59
|
+
from .validity import (
|
|
60
|
+
ValidityCode,
|
|
61
|
+
ValidityPolicy,
|
|
62
|
+
ValidityResult,
|
|
63
|
+
ValidityWindow,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__: tuple[str, ...] = (
|
|
67
|
+
"AuditEvent",
|
|
68
|
+
"AuditRecord",
|
|
69
|
+
"AuthenticationStrength",
|
|
70
|
+
"AuthorizationPolicy",
|
|
71
|
+
"AuthorizationRequest",
|
|
72
|
+
"CallerInfo",
|
|
73
|
+
"Deadline",
|
|
74
|
+
"Delegation",
|
|
75
|
+
"DelegationManager",
|
|
76
|
+
"DelegationRequest",
|
|
77
|
+
"DeviceAuthorization",
|
|
78
|
+
"ElevationEnvelope",
|
|
79
|
+
"ElevationGrant",
|
|
80
|
+
"ElevationURIManager",
|
|
81
|
+
"EnvelopeSigner",
|
|
82
|
+
"Evidence",
|
|
83
|
+
"EvidenceKind",
|
|
84
|
+
"ExecutablePolicy",
|
|
85
|
+
"HashChainAuditLog",
|
|
86
|
+
"HelperGrant",
|
|
87
|
+
"HelperGrantSigner",
|
|
88
|
+
"IdempotencyStore",
|
|
89
|
+
"KeyRing",
|
|
90
|
+
"KeyStatus",
|
|
91
|
+
"MemorySecretStore",
|
|
92
|
+
"NonceStore",
|
|
93
|
+
"OAuthClient",
|
|
94
|
+
"OAuthToken",
|
|
95
|
+
"OAuthTokenValidator",
|
|
96
|
+
"OAuthTransport",
|
|
97
|
+
"PKCEPair",
|
|
98
|
+
"PeerIdentityProvider",
|
|
99
|
+
"Principal",
|
|
100
|
+
"PrivilegeBroker",
|
|
101
|
+
"PrivilegedOperation",
|
|
102
|
+
"PrivilegedRegistry",
|
|
103
|
+
"RateLimiter",
|
|
104
|
+
"Redactor",
|
|
105
|
+
"RefreshTokenVault",
|
|
106
|
+
"RequestContext",
|
|
107
|
+
"RequestIdentity",
|
|
108
|
+
"RevocationRegistry",
|
|
109
|
+
"SecretStore",
|
|
110
|
+
"SecurityContext",
|
|
111
|
+
"SessionClaims",
|
|
112
|
+
"SignedEnvelope",
|
|
113
|
+
"SignedSessionManager",
|
|
114
|
+
"SignedURIManager",
|
|
115
|
+
"SignedURIRequest",
|
|
116
|
+
"SigningKey",
|
|
117
|
+
"Span",
|
|
118
|
+
"SpanStatus",
|
|
119
|
+
"StaticOAuthValidator",
|
|
120
|
+
"TokenSet",
|
|
121
|
+
"Trace",
|
|
122
|
+
"TraceContext",
|
|
123
|
+
"TrustLevel",
|
|
124
|
+
"URIRequest",
|
|
125
|
+
"URIResponse",
|
|
126
|
+
"URIStatus",
|
|
127
|
+
"URIValue",
|
|
128
|
+
"URIValues",
|
|
129
|
+
"ValidityCode",
|
|
130
|
+
"ValidityPolicy",
|
|
131
|
+
"ValidityResult",
|
|
132
|
+
"ValidityWindow",
|
|
133
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Tamper-evident hash-chained audit records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from json import dumps
|
|
8
|
+
from threading import Lock
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
16
|
+
class AuditEvent:
|
|
17
|
+
"""Fields describing one audited event.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
timestamp: Event timestamp text.
|
|
21
|
+
request_id: Originating request identifier.
|
|
22
|
+
trace_id: Originating trace identifier.
|
|
23
|
+
principal: Acting principal, when known.
|
|
24
|
+
action: Performed action.
|
|
25
|
+
outcome: Event outcome.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
timestamp: str
|
|
30
|
+
request_id: str
|
|
31
|
+
trace_id: str
|
|
32
|
+
principal: str | None
|
|
33
|
+
action: str
|
|
34
|
+
outcome: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
38
|
+
class AuditRecord:
|
|
39
|
+
"""One chained audit record.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
sequence: Monotonic record sequence.
|
|
43
|
+
timestamp: Event timestamp text.
|
|
44
|
+
request_id: Originating request identifier.
|
|
45
|
+
trace_id: Originating trace identifier.
|
|
46
|
+
principal: Acting principal, when known.
|
|
47
|
+
action: Performed action.
|
|
48
|
+
outcome: Event outcome.
|
|
49
|
+
previous_hash: Hash of the preceding record.
|
|
50
|
+
record_hash: Hash of this record.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
sequence: int
|
|
55
|
+
timestamp: str
|
|
56
|
+
request_id: str
|
|
57
|
+
trace_id: str
|
|
58
|
+
principal: str | None
|
|
59
|
+
action: str
|
|
60
|
+
outcome: str
|
|
61
|
+
previous_hash: str
|
|
62
|
+
record_hash: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class HashChainAuditLog:
|
|
66
|
+
"""Append-only audit log with SHA-256 chain integrity."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, path: Path) -> None:
|
|
69
|
+
"""Create a log writing chained records to a JSONL file.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path: Destination JSONL file path.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
self._path: Path = path
|
|
76
|
+
self._lock: Lock = Lock()
|
|
77
|
+
self._sequence: int = 0
|
|
78
|
+
self._previous_hash: str = "0" * 64
|
|
79
|
+
|
|
80
|
+
def append(self, *, event: AuditEvent) -> AuditRecord:
|
|
81
|
+
"""Append and return a chained record.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
event: Audited event fields.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The appended chained record.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
with self._lock:
|
|
91
|
+
sequence: int = self._sequence + 1
|
|
92
|
+
material: str = dumps(
|
|
93
|
+
obj={
|
|
94
|
+
"sequence": sequence,
|
|
95
|
+
"timestamp": event.timestamp,
|
|
96
|
+
"request_id": event.request_id,
|
|
97
|
+
"trace_id": event.trace_id,
|
|
98
|
+
"principal": event.principal,
|
|
99
|
+
"action": event.action,
|
|
100
|
+
"outcome": event.outcome,
|
|
101
|
+
"previous_hash": self._previous_hash,
|
|
102
|
+
},
|
|
103
|
+
separators=(",", ":"),
|
|
104
|
+
sort_keys=True,
|
|
105
|
+
)
|
|
106
|
+
digest: str = sha256(
|
|
107
|
+
data=material.encode(encoding="utf-8"),
|
|
108
|
+
).hexdigest()
|
|
109
|
+
record: AuditRecord = AuditRecord(
|
|
110
|
+
sequence=sequence,
|
|
111
|
+
timestamp=event.timestamp,
|
|
112
|
+
request_id=event.request_id,
|
|
113
|
+
trace_id=event.trace_id,
|
|
114
|
+
principal=event.principal,
|
|
115
|
+
action=event.action,
|
|
116
|
+
outcome=event.outcome,
|
|
117
|
+
previous_hash=self._previous_hash,
|
|
118
|
+
record_hash=digest,
|
|
119
|
+
)
|
|
120
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
with self._path.open(mode="a", encoding="utf-8") as stream:
|
|
122
|
+
_ = stream.write(
|
|
123
|
+
dumps(obj=asdict(obj=record), separators=(",", ":"))
|
|
124
|
+
+ "\n",
|
|
125
|
+
)
|
|
126
|
+
self._sequence = sequence
|
|
127
|
+
self._previous_hash = digest
|
|
128
|
+
return record
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Immutable trusted request context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .models import CallerInfo, SecurityContext
|
|
11
|
+
from .trace import Trace
|
|
12
|
+
from .trust import Principal
|
|
13
|
+
from .validity import ValidityResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
17
|
+
class Deadline:
|
|
18
|
+
"""Absolute request deadline.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
at: Instant at which the deadline elapses.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
at: datetime
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def expired(self) -> bool:
|
|
29
|
+
"""Return whether the deadline has elapsed.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Whether the current time is at or past the deadline.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
return datetime.now(tz=UTC) >= self.at
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
39
|
+
class RequestIdentity:
|
|
40
|
+
"""Stable identity for one invocation.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
request_id: Unique request identifier.
|
|
44
|
+
operation: Requested operation name.
|
|
45
|
+
uri: Source request URI.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
request_id: str
|
|
50
|
+
operation: str
|
|
51
|
+
uri: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
55
|
+
class RequestContext:
|
|
56
|
+
"""Trusted context delivered to application code.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
request: Stable request identity.
|
|
60
|
+
caller: Transport-established caller provenance.
|
|
61
|
+
principal: Consolidated authenticated principal.
|
|
62
|
+
security: Authenticated identity and authorization state.
|
|
63
|
+
validity: Validity evaluation result.
|
|
64
|
+
trace: Trace collector for the request.
|
|
65
|
+
deadline: Optional absolute request deadline.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
request: RequestIdentity
|
|
70
|
+
caller: CallerInfo
|
|
71
|
+
principal: Principal
|
|
72
|
+
security: SecurityContext
|
|
73
|
+
validity: ValidityResult
|
|
74
|
+
trace: Trace
|
|
75
|
+
deadline: Deadline | None = None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def executable(self) -> bool:
|
|
79
|
+
"""Return whether validity and deadline permit execution.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Whether the request may execute right now.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
return self.validity.valid and (
|
|
86
|
+
self.deadline is None or not self.deadline.expired
|
|
87
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Capability delegation for local agents and applications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
|
+
from secrets import token_urlsafe
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .errors import AuthorizationError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .revocation import RevocationRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
17
|
+
class Delegation:
|
|
18
|
+
"""Temporary subset of a principal's capabilities.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
delegation_id: Unique delegation identifier.
|
|
22
|
+
issuer: Issuing principal.
|
|
23
|
+
subject: Delegated subject.
|
|
24
|
+
capabilities: Delegated capability names.
|
|
25
|
+
issued_at: Issue timestamp.
|
|
26
|
+
expires_at: Expiry timestamp.
|
|
27
|
+
depth: Chain depth of this delegation.
|
|
28
|
+
max_depth: Maximum permitted chain depth.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
delegation_id: str
|
|
33
|
+
issuer: str
|
|
34
|
+
subject: str
|
|
35
|
+
capabilities: frozenset[str]
|
|
36
|
+
issued_at: datetime
|
|
37
|
+
expires_at: datetime
|
|
38
|
+
depth: int
|
|
39
|
+
max_depth: int
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def valid(self) -> bool:
|
|
43
|
+
"""Return whether the delegation remains temporally valid.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Whether the expiry timestamp is still in the future.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
return datetime.now(tz=UTC) < self.expires_at
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
53
|
+
class DelegationRequest:
|
|
54
|
+
"""Fields describing one requested delegation.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
issuer: Issuing principal.
|
|
58
|
+
subject: Delegated subject.
|
|
59
|
+
issuer_capabilities: Capabilities the issuer holds.
|
|
60
|
+
capabilities: Capabilities to delegate.
|
|
61
|
+
ttl: Delegation lifetime.
|
|
62
|
+
depth: Chain depth of this delegation.
|
|
63
|
+
max_depth: Maximum permitted chain depth.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
issuer: str
|
|
68
|
+
subject: str
|
|
69
|
+
issuer_capabilities: frozenset[str]
|
|
70
|
+
capabilities: frozenset[str]
|
|
71
|
+
ttl: timedelta
|
|
72
|
+
depth: int = 0
|
|
73
|
+
max_depth: int = 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True, kw_only=True)
|
|
77
|
+
class DelegationManager:
|
|
78
|
+
"""Issue constrained delegation chains.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
revocations: Revocation registry consulted during validation.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
revocations: RevocationRegistry
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def issue(*, request: DelegationRequest) -> Delegation:
|
|
89
|
+
"""Issue a subset-only delegation.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
request: Requested delegation fields.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The issued delegation.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
AuthorizationError: When the request exceeds the issuer's
|
|
99
|
+
capabilities or the permitted chain depth.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
if not request.capabilities <= request.issuer_capabilities:
|
|
103
|
+
msg: str = (
|
|
104
|
+
"Cannot delegate capabilities the issuer does not possess."
|
|
105
|
+
)
|
|
106
|
+
raise AuthorizationError(
|
|
107
|
+
msg,
|
|
108
|
+
)
|
|
109
|
+
if request.depth > request.max_depth:
|
|
110
|
+
msg = "Delegation depth exceeded."
|
|
111
|
+
raise AuthorizationError(msg)
|
|
112
|
+
now: datetime = datetime.now(tz=UTC)
|
|
113
|
+
return Delegation(
|
|
114
|
+
delegation_id=token_urlsafe(nbytes=18),
|
|
115
|
+
issuer=request.issuer,
|
|
116
|
+
subject=request.subject,
|
|
117
|
+
capabilities=request.capabilities,
|
|
118
|
+
issued_at=now,
|
|
119
|
+
expires_at=now + request.ttl,
|
|
120
|
+
depth=request.depth,
|
|
121
|
+
max_depth=request.max_depth,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def validate(self, *, delegation: Delegation) -> None:
|
|
125
|
+
"""Validate expiry and revocation.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
delegation: Delegation to validate.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
AuthorizationError: When the delegation is expired or revoked.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
if not delegation.valid:
|
|
135
|
+
msg: str = "Delegation expired."
|
|
136
|
+
raise AuthorizationError(msg)
|
|
137
|
+
if self.revocations.is_revoked(
|
|
138
|
+
identifier=delegation.delegation_id,
|
|
139
|
+
):
|
|
140
|
+
msg = "Delegation revoked."
|
|
141
|
+
raise AuthorizationError(msg)
|