apoa 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.
- apoa-0.1.0/PKG-INFO +109 -0
- apoa-0.1.0/README.md +83 -0
- apoa-0.1.0/apoa/__init__.py +144 -0
- apoa-0.1.0/apoa/audit.py +100 -0
- apoa-0.1.0/apoa/authorization.py +122 -0
- apoa-0.1.0/apoa/client.py +131 -0
- apoa-0.1.0/apoa/constraints.py +33 -0
- apoa-0.1.0/apoa/crypto.py +63 -0
- apoa-0.1.0/apoa/delegation.py +245 -0
- apoa-0.1.0/apoa/errors.py +89 -0
- apoa-0.1.0/apoa/py.typed +0 -0
- apoa-0.1.0/apoa/revocation.py +94 -0
- apoa-0.1.0/apoa/scope.py +55 -0
- apoa-0.1.0/apoa/token.py +562 -0
- apoa-0.1.0/apoa/types.py +247 -0
- apoa-0.1.0/apoa/utils.py +51 -0
- apoa-0.1.0/apoa.egg-info/PKG-INFO +109 -0
- apoa-0.1.0/apoa.egg-info/SOURCES.txt +32 -0
- apoa-0.1.0/apoa.egg-info/dependency_links.txt +1 -0
- apoa-0.1.0/apoa.egg-info/requires.txt +4 -0
- apoa-0.1.0/apoa.egg-info/top_level.txt +1 -0
- apoa-0.1.0/pyproject.toml +42 -0
- apoa-0.1.0/setup.cfg +4 -0
- apoa-0.1.0/tests/test_audit.py +62 -0
- apoa-0.1.0/tests/test_authorization.py +123 -0
- apoa-0.1.0/tests/test_client.py +130 -0
- apoa-0.1.0/tests/test_constraints.py +44 -0
- apoa-0.1.0/tests/test_cross_sdk.py +99 -0
- apoa-0.1.0/tests/test_crypto.py +54 -0
- apoa-0.1.0/tests/test_delegation.py +213 -0
- apoa-0.1.0/tests/test_revocation.py +52 -0
- apoa-0.1.0/tests/test_scope.py +70 -0
- apoa-0.1.0/tests/test_token.py +180 -0
- apoa-0.1.0/tests/test_utils.py +43 -0
apoa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apoa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Agentic Power of Attorney (APOA) standard
|
|
5
|
+
Author-email: Agentic POA <agenticpoa@users.noreply.github.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/agenticpoa/apoa
|
|
8
|
+
Project-URL: Documentation, https://github.com/agenticpoa/apoa/tree/main/sdk-python
|
|
9
|
+
Project-URL: Repository, https://github.com/agenticpoa/apoa
|
|
10
|
+
Project-URL: Issues, https://github.com/agenticpoa/apoa/issues
|
|
11
|
+
Keywords: authorization,ai-agents,delegation,jwt,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: PyJWT[crypto]>=2.8.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# APOA Python SDK
|
|
28
|
+
|
|
29
|
+
Python SDK for the [Agentic Power of Attorney (APOA)](https://github.com/agenticpoa/apoa) standard -- authorization infrastructure for AI agents.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install apoa
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from apoa import (
|
|
41
|
+
APOADefinition, Agent, BrowserSessionConfig, Principal,
|
|
42
|
+
Rule, ServiceAuthorization, SigningOptions,
|
|
43
|
+
create_client, generate_key_pair,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Generate keys and create a client
|
|
47
|
+
private_key, public_key = generate_key_pair()
|
|
48
|
+
client = create_client(default_private_key=private_key)
|
|
49
|
+
|
|
50
|
+
# Create a signed authorization token
|
|
51
|
+
token = client.create_token(APOADefinition(
|
|
52
|
+
principal=Principal(id="did:apoa:you"),
|
|
53
|
+
agent=Agent(id="did:apoa:your-agent", name="HomeBot Pro"),
|
|
54
|
+
services=[ServiceAuthorization(
|
|
55
|
+
service="nationwidemortgage.com",
|
|
56
|
+
scopes=["rate_lock:read", "documents:read"],
|
|
57
|
+
constraints={"signing": False},
|
|
58
|
+
access_mode="browser",
|
|
59
|
+
browser_config=BrowserSessionConfig(
|
|
60
|
+
allowed_urls=["https://portal.nationwidemortgage.com/*"],
|
|
61
|
+
credential_vault_ref="1password://vault/mortgage-portal",
|
|
62
|
+
),
|
|
63
|
+
)],
|
|
64
|
+
rules=[Rule(id="no-signing", description="Never sign anything", enforcement="hard")],
|
|
65
|
+
expires="2026-09-01",
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
# Authorize actions
|
|
69
|
+
result = client.authorize(token, "nationwidemortgage.com", "rate_lock:read")
|
|
70
|
+
print(result.authorized) # True
|
|
71
|
+
|
|
72
|
+
result = client.authorize(token, "nationwidemortgage.com", "documents:sign")
|
|
73
|
+
print(result.authorized) # False
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
- **Token lifecycle**: create, sign (Ed25519/ES256), validate, parse
|
|
79
|
+
- **Scope matching**: hierarchical pattern matching (`appointments:*` matches `appointments:read`)
|
|
80
|
+
- **Constraint enforcement**: boolean denial at the SDK level, rich constraints at the protocol level
|
|
81
|
+
- **Authorization**: revocation + scope + constraints + hard/soft rules in one call
|
|
82
|
+
- **Delegation chains**: parent-to-child with cryptographically enforced attenuation
|
|
83
|
+
- **Cascade revocation**: revoke parent, kill all children instantly
|
|
84
|
+
- **Audit trail**: append-only action log per token
|
|
85
|
+
- **Cross-SDK compatibility**: tokens created by the TypeScript SDK validate in Python and vice versa
|
|
86
|
+
|
|
87
|
+
## Cross-SDK Compatibility
|
|
88
|
+
|
|
89
|
+
Tokens are JWTs. A token signed by `@apoa/core` (TypeScript) validates in `apoa` (Python) and vice versa. The serialization layer handles camelCase (JWT payload) to snake_case (Python) mapping automatically.
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
Two usage styles:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Style 1: Client instance (recommended)
|
|
97
|
+
client = create_client(default_private_key=key)
|
|
98
|
+
client.authorize(token, "service.com", "action:read")
|
|
99
|
+
|
|
100
|
+
# Style 2: Standalone imports
|
|
101
|
+
from apoa import authorize, check_scope
|
|
102
|
+
check_scope(token, "service.com", "action:read")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See the [full spec](https://github.com/agenticpoa/apoa/blob/main/SPEC.md) and [TypeScript SDK](https://github.com/agenticpoa/apoa/tree/main/sdk) for more.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
Apache 2.0
|
apoa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# APOA Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Agentic Power of Attorney (APOA)](https://github.com/agenticpoa/apoa) standard -- authorization infrastructure for AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install apoa
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from apoa import (
|
|
15
|
+
APOADefinition, Agent, BrowserSessionConfig, Principal,
|
|
16
|
+
Rule, ServiceAuthorization, SigningOptions,
|
|
17
|
+
create_client, generate_key_pair,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Generate keys and create a client
|
|
21
|
+
private_key, public_key = generate_key_pair()
|
|
22
|
+
client = create_client(default_private_key=private_key)
|
|
23
|
+
|
|
24
|
+
# Create a signed authorization token
|
|
25
|
+
token = client.create_token(APOADefinition(
|
|
26
|
+
principal=Principal(id="did:apoa:you"),
|
|
27
|
+
agent=Agent(id="did:apoa:your-agent", name="HomeBot Pro"),
|
|
28
|
+
services=[ServiceAuthorization(
|
|
29
|
+
service="nationwidemortgage.com",
|
|
30
|
+
scopes=["rate_lock:read", "documents:read"],
|
|
31
|
+
constraints={"signing": False},
|
|
32
|
+
access_mode="browser",
|
|
33
|
+
browser_config=BrowserSessionConfig(
|
|
34
|
+
allowed_urls=["https://portal.nationwidemortgage.com/*"],
|
|
35
|
+
credential_vault_ref="1password://vault/mortgage-portal",
|
|
36
|
+
),
|
|
37
|
+
)],
|
|
38
|
+
rules=[Rule(id="no-signing", description="Never sign anything", enforcement="hard")],
|
|
39
|
+
expires="2026-09-01",
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
# Authorize actions
|
|
43
|
+
result = client.authorize(token, "nationwidemortgage.com", "rate_lock:read")
|
|
44
|
+
print(result.authorized) # True
|
|
45
|
+
|
|
46
|
+
result = client.authorize(token, "nationwidemortgage.com", "documents:sign")
|
|
47
|
+
print(result.authorized) # False
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- **Token lifecycle**: create, sign (Ed25519/ES256), validate, parse
|
|
53
|
+
- **Scope matching**: hierarchical pattern matching (`appointments:*` matches `appointments:read`)
|
|
54
|
+
- **Constraint enforcement**: boolean denial at the SDK level, rich constraints at the protocol level
|
|
55
|
+
- **Authorization**: revocation + scope + constraints + hard/soft rules in one call
|
|
56
|
+
- **Delegation chains**: parent-to-child with cryptographically enforced attenuation
|
|
57
|
+
- **Cascade revocation**: revoke parent, kill all children instantly
|
|
58
|
+
- **Audit trail**: append-only action log per token
|
|
59
|
+
- **Cross-SDK compatibility**: tokens created by the TypeScript SDK validate in Python and vice versa
|
|
60
|
+
|
|
61
|
+
## Cross-SDK Compatibility
|
|
62
|
+
|
|
63
|
+
Tokens are JWTs. A token signed by `@apoa/core` (TypeScript) validates in `apoa` (Python) and vice versa. The serialization layer handles camelCase (JWT payload) to snake_case (Python) mapping automatically.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
Two usage styles:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Style 1: Client instance (recommended)
|
|
71
|
+
client = create_client(default_private_key=key)
|
|
72
|
+
client.authorize(token, "service.com", "action:read")
|
|
73
|
+
|
|
74
|
+
# Style 2: Standalone imports
|
|
75
|
+
from apoa import authorize, check_scope
|
|
76
|
+
check_scope(token, "service.com", "action:read")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See the [full spec](https://github.com/agenticpoa/apoa/blob/main/SPEC.md) and [TypeScript SDK](https://github.com/agenticpoa/apoa/tree/main/sdk) for more.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
Apache 2.0
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""APOA SDK -- Agentic Power of Attorney for Python."""
|
|
2
|
+
|
|
3
|
+
# Client
|
|
4
|
+
from .client import APOAClient, create_client
|
|
5
|
+
|
|
6
|
+
# Token lifecycle
|
|
7
|
+
from .token import create_token, parse_definition, validate_token
|
|
8
|
+
|
|
9
|
+
# Authorization
|
|
10
|
+
from .authorization import authorize
|
|
11
|
+
from .constraints import check_constraint
|
|
12
|
+
from .scope import check_scope, match_scope
|
|
13
|
+
|
|
14
|
+
# Delegation
|
|
15
|
+
from .delegation import delegate, verify_chain
|
|
16
|
+
|
|
17
|
+
# Revocation
|
|
18
|
+
from .revocation import (
|
|
19
|
+
MemoryRevocationStore,
|
|
20
|
+
RevocationStore,
|
|
21
|
+
cascade_revoke,
|
|
22
|
+
is_revoked,
|
|
23
|
+
revoke,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Audit
|
|
27
|
+
from .audit import (
|
|
28
|
+
AuditStore,
|
|
29
|
+
MemoryAuditStore,
|
|
30
|
+
get_audit_trail,
|
|
31
|
+
get_audit_trail_by_service,
|
|
32
|
+
log_action,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Crypto
|
|
36
|
+
from .crypto import generate_key_pair
|
|
37
|
+
|
|
38
|
+
# Time utilities
|
|
39
|
+
from .utils import is_before_not_before, is_expired
|
|
40
|
+
|
|
41
|
+
# Types
|
|
42
|
+
from .types import (
|
|
43
|
+
APIAccessConfig,
|
|
44
|
+
APOADefinition,
|
|
45
|
+
APOAToken,
|
|
46
|
+
Agent,
|
|
47
|
+
AgentProvider,
|
|
48
|
+
AuditEntry,
|
|
49
|
+
AuditQueryOptions,
|
|
50
|
+
AuthorizationResult,
|
|
51
|
+
BrowserSessionConfig,
|
|
52
|
+
ChainVerificationResult,
|
|
53
|
+
DelegationDefinition,
|
|
54
|
+
LegalFramework,
|
|
55
|
+
Principal,
|
|
56
|
+
RevocationRecord,
|
|
57
|
+
Rule,
|
|
58
|
+
RuleViolation,
|
|
59
|
+
ScopeCheckResult,
|
|
60
|
+
ServiceAuthorization,
|
|
61
|
+
SigningOptions,
|
|
62
|
+
ValidationOptions,
|
|
63
|
+
ValidationResult,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Errors
|
|
67
|
+
from .errors import (
|
|
68
|
+
APOAError,
|
|
69
|
+
AttenuationViolationError,
|
|
70
|
+
ChainVerificationError,
|
|
71
|
+
DefinitionValidationError,
|
|
72
|
+
MetadataValidationError,
|
|
73
|
+
RevocationError,
|
|
74
|
+
RuleEnforcementError,
|
|
75
|
+
ScopeViolationError,
|
|
76
|
+
TokenExpiredError,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
# Client
|
|
81
|
+
"APOAClient",
|
|
82
|
+
"create_client",
|
|
83
|
+
# Token lifecycle
|
|
84
|
+
"create_token",
|
|
85
|
+
"validate_token",
|
|
86
|
+
"parse_definition",
|
|
87
|
+
# Authorization
|
|
88
|
+
"authorize",
|
|
89
|
+
"check_scope",
|
|
90
|
+
"match_scope",
|
|
91
|
+
"check_constraint",
|
|
92
|
+
# Delegation
|
|
93
|
+
"delegate",
|
|
94
|
+
"verify_chain",
|
|
95
|
+
# Revocation
|
|
96
|
+
"revoke",
|
|
97
|
+
"is_revoked",
|
|
98
|
+
"cascade_revoke",
|
|
99
|
+
"RevocationStore",
|
|
100
|
+
"MemoryRevocationStore",
|
|
101
|
+
# Audit
|
|
102
|
+
"log_action",
|
|
103
|
+
"get_audit_trail",
|
|
104
|
+
"get_audit_trail_by_service",
|
|
105
|
+
"AuditStore",
|
|
106
|
+
"MemoryAuditStore",
|
|
107
|
+
# Crypto
|
|
108
|
+
"generate_key_pair",
|
|
109
|
+
# Time
|
|
110
|
+
"is_expired",
|
|
111
|
+
"is_before_not_before",
|
|
112
|
+
# Types
|
|
113
|
+
"Principal",
|
|
114
|
+
"Agent",
|
|
115
|
+
"AgentProvider",
|
|
116
|
+
"LegalFramework",
|
|
117
|
+
"BrowserSessionConfig",
|
|
118
|
+
"APIAccessConfig",
|
|
119
|
+
"ServiceAuthorization",
|
|
120
|
+
"Rule",
|
|
121
|
+
"RuleViolation",
|
|
122
|
+
"APOADefinition",
|
|
123
|
+
"APOAToken",
|
|
124
|
+
"ScopeCheckResult",
|
|
125
|
+
"AuthorizationResult",
|
|
126
|
+
"ValidationResult",
|
|
127
|
+
"RevocationRecord",
|
|
128
|
+
"AuditEntry",
|
|
129
|
+
"AuditQueryOptions",
|
|
130
|
+
"DelegationDefinition",
|
|
131
|
+
"ChainVerificationResult",
|
|
132
|
+
"SigningOptions",
|
|
133
|
+
"ValidationOptions",
|
|
134
|
+
# Errors
|
|
135
|
+
"APOAError",
|
|
136
|
+
"TokenExpiredError",
|
|
137
|
+
"ScopeViolationError",
|
|
138
|
+
"AttenuationViolationError",
|
|
139
|
+
"RevocationError",
|
|
140
|
+
"ChainVerificationError",
|
|
141
|
+
"MetadataValidationError",
|
|
142
|
+
"RuleEnforcementError",
|
|
143
|
+
"DefinitionValidationError",
|
|
144
|
+
]
|
apoa-0.1.0/apoa/audit.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Audit store and logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from .types import AuditEntry, AuditQueryOptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuditStore(Protocol):
|
|
12
|
+
def append(self, entry: AuditEntry) -> None: ...
|
|
13
|
+
def query(self, token_id: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]: ...
|
|
14
|
+
def query_by_service(self, service: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemoryAuditStore:
|
|
18
|
+
"""In-memory audit store for dev/testing."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._entries: list[AuditEntry] = []
|
|
22
|
+
|
|
23
|
+
def append(self, entry: AuditEntry) -> None:
|
|
24
|
+
self._entries.append(entry)
|
|
25
|
+
|
|
26
|
+
def query(self, token_id: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
|
|
27
|
+
results = [e for e in self._entries if e.token_id == token_id]
|
|
28
|
+
return _apply_query_options(results, options)
|
|
29
|
+
|
|
30
|
+
def query_by_service(self, service: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
|
|
31
|
+
results = [e for e in self._entries if e.service == service]
|
|
32
|
+
return _apply_query_options(results, options)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _apply_query_options(entries: list[AuditEntry], options: AuditQueryOptions | None) -> list[AuditEntry]:
|
|
36
|
+
if options is None:
|
|
37
|
+
return entries
|
|
38
|
+
|
|
39
|
+
results = entries
|
|
40
|
+
if options.from_time:
|
|
41
|
+
results = [e for e in results if e.timestamp >= options.from_time]
|
|
42
|
+
if options.to_time:
|
|
43
|
+
results = [e for e in results if e.timestamp <= options.to_time]
|
|
44
|
+
if options.action:
|
|
45
|
+
results = [e for e in results if e.action == options.action]
|
|
46
|
+
if options.service:
|
|
47
|
+
results = [e for e in results if e.service == options.service]
|
|
48
|
+
if options.result:
|
|
49
|
+
results = [e for e in results if e.result == options.result]
|
|
50
|
+
if options.offset:
|
|
51
|
+
results = results[options.offset:]
|
|
52
|
+
if options.limit:
|
|
53
|
+
results = results[: options.limit]
|
|
54
|
+
|
|
55
|
+
return results
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def log_action(
|
|
59
|
+
token_id: str,
|
|
60
|
+
action: str,
|
|
61
|
+
service: str,
|
|
62
|
+
result: str,
|
|
63
|
+
store: AuditStore | None = None,
|
|
64
|
+
**details: str | int | float | bool | None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Log an action against a token."""
|
|
67
|
+
if store is None:
|
|
68
|
+
raise ValueError("audit store is required")
|
|
69
|
+
|
|
70
|
+
entry = AuditEntry(
|
|
71
|
+
token_id=token_id,
|
|
72
|
+
timestamp=datetime.now(timezone.utc),
|
|
73
|
+
action=action,
|
|
74
|
+
service=service,
|
|
75
|
+
result=result,
|
|
76
|
+
details=details if details else None,
|
|
77
|
+
)
|
|
78
|
+
store.append(entry)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_audit_trail(
|
|
82
|
+
token_id: str,
|
|
83
|
+
options: AuditQueryOptions | None = None,
|
|
84
|
+
store: AuditStore | None = None,
|
|
85
|
+
) -> list[AuditEntry]:
|
|
86
|
+
"""Get the audit trail for a token."""
|
|
87
|
+
if store is None:
|
|
88
|
+
raise ValueError("audit store is required")
|
|
89
|
+
return store.query(token_id, options)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_audit_trail_by_service(
|
|
93
|
+
service: str,
|
|
94
|
+
options: AuditQueryOptions | None = None,
|
|
95
|
+
store: AuditStore | None = None,
|
|
96
|
+
) -> list[AuditEntry]:
|
|
97
|
+
"""Get the audit trail for a service across all tokens."""
|
|
98
|
+
if store is None:
|
|
99
|
+
raise ValueError("audit store is required")
|
|
100
|
+
return store.query_by_service(service, options)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Full authorization flow: revocation + scope + constraints + rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .scope import check_scope
|
|
9
|
+
from .types import (
|
|
10
|
+
APOAToken,
|
|
11
|
+
AuditEntry,
|
|
12
|
+
AuthorizationResult,
|
|
13
|
+
RuleViolation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def authorize(
|
|
18
|
+
token: APOAToken,
|
|
19
|
+
service: str,
|
|
20
|
+
action: str,
|
|
21
|
+
revocation_store: Any | None = None,
|
|
22
|
+
audit_store: Any | None = None,
|
|
23
|
+
) -> AuthorizationResult:
|
|
24
|
+
"""One-stop authorization check: revocation + scope + constraints + rules.
|
|
25
|
+
|
|
26
|
+
Enforcement order:
|
|
27
|
+
1. Check revocation (is the token still alive?)
|
|
28
|
+
2. Check scope (is the action in the authorized scope set?)
|
|
29
|
+
3. Check constraints (action segment vs constraint key set to false)
|
|
30
|
+
4. Check hard rules (deny if rule key appears in action)
|
|
31
|
+
5. Check soft rules (log violation + invoke callback + continue)
|
|
32
|
+
"""
|
|
33
|
+
# 1. Check revocation
|
|
34
|
+
if revocation_store is not None:
|
|
35
|
+
record = revocation_store.check(token.jti)
|
|
36
|
+
if record:
|
|
37
|
+
return AuthorizationResult(
|
|
38
|
+
authorized=False,
|
|
39
|
+
reason="token has been revoked",
|
|
40
|
+
checks={"revoked": True},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 2. Check scope
|
|
44
|
+
scope_result = check_scope(token, service, action)
|
|
45
|
+
if not scope_result.allowed:
|
|
46
|
+
return AuthorizationResult(
|
|
47
|
+
authorized=False,
|
|
48
|
+
reason=scope_result.reason,
|
|
49
|
+
checks={"revoked": False, "scope_allowed": False},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# 3. Check constraints
|
|
53
|
+
service_auth = next((s for s in token.definition.services if s.service == service), None)
|
|
54
|
+
if service_auth and service_auth.constraints:
|
|
55
|
+
action_segments = action.split(":")
|
|
56
|
+
for key, value in service_auth.constraints.items():
|
|
57
|
+
if value is False and key in action_segments:
|
|
58
|
+
return AuthorizationResult(
|
|
59
|
+
authorized=False,
|
|
60
|
+
reason=f"constraint '{key}' is set to false",
|
|
61
|
+
checks={"revoked": False, "scope_allowed": True, "constraints_passed": False},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 4 & 5. Check rules
|
|
65
|
+
rules = token.definition.rules
|
|
66
|
+
violations: list[RuleViolation] = []
|
|
67
|
+
|
|
68
|
+
if rules:
|
|
69
|
+
# 4. Hard rules
|
|
70
|
+
for rule in rules:
|
|
71
|
+
if rule.enforcement == "hard":
|
|
72
|
+
rule_key = rule.id[3:] if rule.id.startswith("no-") else rule.id
|
|
73
|
+
if rule_key.lower() in action.lower():
|
|
74
|
+
return AuthorizationResult(
|
|
75
|
+
authorized=False,
|
|
76
|
+
reason=f"hard rule '{rule.id}' violated",
|
|
77
|
+
checks={
|
|
78
|
+
"revoked": False,
|
|
79
|
+
"scope_allowed": True,
|
|
80
|
+
"constraints_passed": True,
|
|
81
|
+
"rules_passed": False,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# 5. Soft rules
|
|
86
|
+
for rule in rules:
|
|
87
|
+
if rule.enforcement == "soft":
|
|
88
|
+
violation = RuleViolation(
|
|
89
|
+
rule_id=rule.id,
|
|
90
|
+
token_id=token.jti,
|
|
91
|
+
action=action,
|
|
92
|
+
service=service,
|
|
93
|
+
timestamp=datetime.now(timezone.utc),
|
|
94
|
+
details=rule.description,
|
|
95
|
+
)
|
|
96
|
+
violations.append(violation)
|
|
97
|
+
|
|
98
|
+
# Log to audit store if available
|
|
99
|
+
if audit_store is not None:
|
|
100
|
+
audit_store.append(AuditEntry(
|
|
101
|
+
token_id=token.jti,
|
|
102
|
+
timestamp=violation.timestamp,
|
|
103
|
+
action=action,
|
|
104
|
+
service=service,
|
|
105
|
+
result="escalated",
|
|
106
|
+
details={"ruleId": rule.id, "ruleDescription": rule.description},
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
# Invoke on_violation callback if provided
|
|
110
|
+
if rule.on_violation:
|
|
111
|
+
rule.on_violation(violation)
|
|
112
|
+
|
|
113
|
+
return AuthorizationResult(
|
|
114
|
+
authorized=True,
|
|
115
|
+
checks={
|
|
116
|
+
"revoked": False,
|
|
117
|
+
"scope_allowed": True,
|
|
118
|
+
"constraints_passed": True,
|
|
119
|
+
"rules_passed": True,
|
|
120
|
+
},
|
|
121
|
+
violations=violations if violations else None,
|
|
122
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""APOAClient -- configured facade that wires up stores and keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .audit import MemoryAuditStore, get_audit_trail, get_audit_trail_by_service, log_action
|
|
8
|
+
from .authorization import authorize
|
|
9
|
+
from .constraints import check_constraint
|
|
10
|
+
from .crypto import generate_key_pair
|
|
11
|
+
from .delegation import delegate, verify_chain
|
|
12
|
+
from .revocation import (
|
|
13
|
+
MemoryRevocationStore,
|
|
14
|
+
cascade_revoke,
|
|
15
|
+
is_revoked,
|
|
16
|
+
revoke,
|
|
17
|
+
)
|
|
18
|
+
from .scope import check_scope
|
|
19
|
+
from .token import create_token, parse_definition, validate_token
|
|
20
|
+
from .types import (
|
|
21
|
+
APOADefinition,
|
|
22
|
+
APOAToken,
|
|
23
|
+
AuditEntry,
|
|
24
|
+
AuditQueryOptions,
|
|
25
|
+
AuthorizationResult,
|
|
26
|
+
ChainVerificationResult,
|
|
27
|
+
DelegationDefinition,
|
|
28
|
+
RevocationRecord,
|
|
29
|
+
ScopeCheckResult,
|
|
30
|
+
SigningOptions,
|
|
31
|
+
ValidationOptions,
|
|
32
|
+
ValidationResult,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class APOAClient:
|
|
37
|
+
"""Configured APOA client with wired-up stores and default signing options."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
revocation_store: Any | None = None,
|
|
42
|
+
audit_store: Any | None = None,
|
|
43
|
+
key_resolver: Any | None = None,
|
|
44
|
+
default_private_key: Any | None = None,
|
|
45
|
+
default_algorithm: str = "EdDSA",
|
|
46
|
+
) -> None:
|
|
47
|
+
self._revocation_store = revocation_store or MemoryRevocationStore()
|
|
48
|
+
self._audit_store = audit_store or MemoryAuditStore()
|
|
49
|
+
self._key_resolver = key_resolver
|
|
50
|
+
self._default_private_key = default_private_key
|
|
51
|
+
self._default_algorithm = default_algorithm
|
|
52
|
+
|
|
53
|
+
def _signing_options(self, options: SigningOptions | None = None) -> SigningOptions:
|
|
54
|
+
if options:
|
|
55
|
+
return options
|
|
56
|
+
if self._default_private_key is None:
|
|
57
|
+
raise ValueError("No signing options provided and no default_private_key configured")
|
|
58
|
+
return SigningOptions(private_key=self._default_private_key, algorithm=self._default_algorithm)
|
|
59
|
+
|
|
60
|
+
def create_token(self, definition: APOADefinition, options: SigningOptions | None = None) -> APOAToken:
|
|
61
|
+
return create_token(definition, self._signing_options(options))
|
|
62
|
+
|
|
63
|
+
def validate_token(self, token: str | APOAToken, options: ValidationOptions | None = None) -> ValidationResult:
|
|
64
|
+
opts = options or ValidationOptions()
|
|
65
|
+
# Wire up defaults
|
|
66
|
+
if opts.key_resolver is None and self._key_resolver is not None:
|
|
67
|
+
opts.key_resolver = self._key_resolver
|
|
68
|
+
if opts.revocation_store is None:
|
|
69
|
+
opts.revocation_store = self._revocation_store
|
|
70
|
+
if not opts.check_revocation:
|
|
71
|
+
opts.check_revocation = True
|
|
72
|
+
return validate_token(token, opts)
|
|
73
|
+
|
|
74
|
+
def check_scope(self, token: APOAToken, service: str, action: str) -> ScopeCheckResult:
|
|
75
|
+
return check_scope(token, service, action)
|
|
76
|
+
|
|
77
|
+
def check_constraint(self, token: APOAToken, service: str, constraint: str) -> ScopeCheckResult:
|
|
78
|
+
return check_constraint(token, service, constraint)
|
|
79
|
+
|
|
80
|
+
def authorize(self, token: APOAToken, service: str, action: str) -> AuthorizationResult:
|
|
81
|
+
return authorize(token, service, action, self._revocation_store, self._audit_store)
|
|
82
|
+
|
|
83
|
+
def delegate(self, parent_token: APOAToken, child_def: DelegationDefinition, options: SigningOptions | None = None) -> APOAToken:
|
|
84
|
+
return delegate(parent_token, child_def, self._signing_options(options))
|
|
85
|
+
|
|
86
|
+
def verify_chain(self, chain: list[APOAToken]) -> ChainVerificationResult:
|
|
87
|
+
return verify_chain(chain, self._revocation_store)
|
|
88
|
+
|
|
89
|
+
def revoke(self, token_id: str, revoked_by: str, reason: str | None = None) -> RevocationRecord:
|
|
90
|
+
return revoke(token_id, revoked_by, reason, self._revocation_store)
|
|
91
|
+
|
|
92
|
+
def is_revoked(self, token_id: str) -> bool:
|
|
93
|
+
return is_revoked(token_id, self._revocation_store)
|
|
94
|
+
|
|
95
|
+
def cascade_revoke(self, parent_token_id: str, child_token_ids: list[str], revoked_by: str, reason: str | None = None) -> RevocationRecord:
|
|
96
|
+
return cascade_revoke(parent_token_id, child_token_ids, revoked_by, reason, self._revocation_store)
|
|
97
|
+
|
|
98
|
+
def log_action(self, token_id: str, action: str, service: str, result: str, **details: str | int | float | bool | None) -> None:
|
|
99
|
+
log_action(token_id, action, service, result, self._audit_store, **details)
|
|
100
|
+
|
|
101
|
+
def get_audit_trail(self, token_id: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
|
|
102
|
+
return get_audit_trail(token_id, options, self._audit_store)
|
|
103
|
+
|
|
104
|
+
def get_audit_trail_by_service(self, service: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
|
|
105
|
+
return get_audit_trail_by_service(service, options, self._audit_store)
|
|
106
|
+
|
|
107
|
+
def generate_key_pair(self, algorithm: str | None = None) -> tuple:
|
|
108
|
+
return generate_key_pair(algorithm or self._default_algorithm)
|
|
109
|
+
|
|
110
|
+
def parse_definition(self, input_str: str, format: str = "json") -> APOADefinition:
|
|
111
|
+
return parse_definition(input_str, format)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def create_client(
|
|
115
|
+
revocation_store: Any | None = None,
|
|
116
|
+
audit_store: Any | None = None,
|
|
117
|
+
key_resolver: Any | None = None,
|
|
118
|
+
default_private_key: Any | None = None,
|
|
119
|
+
default_algorithm: str = "EdDSA",
|
|
120
|
+
) -> APOAClient:
|
|
121
|
+
"""Create a configured APOA client.
|
|
122
|
+
|
|
123
|
+
Defaults to MemoryRevocationStore + MemoryAuditStore for zero-config dev/testing.
|
|
124
|
+
"""
|
|
125
|
+
return APOAClient(
|
|
126
|
+
revocation_store=revocation_store,
|
|
127
|
+
audit_store=audit_store,
|
|
128
|
+
key_resolver=key_resolver,
|
|
129
|
+
default_private_key=default_private_key,
|
|
130
|
+
default_algorithm=default_algorithm,
|
|
131
|
+
)
|