agentic-fabriq-sdk 0.1.3__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.
Potentially problematic release.
This version of agentic-fabriq-sdk might be problematic. Click here for more details.
- agentic_fabriq_sdk-0.1.3/PKG-INFO +81 -0
- agentic_fabriq_sdk-0.1.3/README.md +54 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/__init__.py +55 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/auth/__init__.py +31 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/auth/dpop.py +43 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/auth/oauth.py +247 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/auth/token_cache.py +318 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/connectors/__init__.py +23 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/connectors/base.py +231 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/connectors/registry.py +262 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/dx/__init__.py +12 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/dx/decorators.py +40 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/dx/runtime.py +170 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/events.py +699 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/exceptions.py +140 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/fabriq_client.py +198 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/models/__init__.py +47 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/models/audit.py +44 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/models/types.py +242 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/py.typed +0 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/transport/__init__.py +7 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/transport/http.py +366 -0
- agentic_fabriq_sdk-0.1.3/af_sdk/vault.py +500 -0
- agentic_fabriq_sdk-0.1.3/pyproject.toml +36 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-fabriq-sdk
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Fabriq/Agentic Fabric Python SDK: high-level client, DX helpers, auth
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: fabriq,agentic-fabric,sdk,ai,agents
|
|
7
|
+
Author: Agentic Fabric Contributors
|
|
8
|
+
Author-email: contributors@agentic-fabric.org
|
|
9
|
+
Requires-Python: >=3.11,<3.13
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: PyJWT (>=2.8.0)
|
|
18
|
+
Requires-Dist: httpx (>=0.25)
|
|
19
|
+
Requires-Dist: pydantic (>=2.4)
|
|
20
|
+
Requires-Dist: stevedore (>=5.1.0)
|
|
21
|
+
Requires-Dist: typing-extensions
|
|
22
|
+
Project-URL: Documentation, https://docs.agentic-fabric.org
|
|
23
|
+
Project-URL: Homepage, https://github.com/agentic-fabric/agentic-fabric
|
|
24
|
+
Project-URL: Repository, https://github.com/agentic-fabric/agentic-fabric
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Agentic Fabric SDK (Fabriq)
|
|
28
|
+
|
|
29
|
+
`agentic-fabriq-sdk` provides a Python SDK for interacting with Fabriq/Agentic Fabric.
|
|
30
|
+
|
|
31
|
+
- High-level client: `af_sdk.FabriqClient`
|
|
32
|
+
- DX layer: `af_sdk.dx` (`ToolFabric`, `AgentFabric`, `MCPServer`, `Agent`, and `tool`)
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install agentic-fabriq-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from af_sdk.fabriq_client import FabriqClient
|
|
44
|
+
|
|
45
|
+
TOKEN = "..." # Bearer JWT for the Fabriq Gateway
|
|
46
|
+
BASE = "http://localhost:8000"
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
async with FabriqClient(base_url=BASE, auth_token=TOKEN) as af:
|
|
50
|
+
agents = await af.list_agents()
|
|
51
|
+
print(agents)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
DX orchestration:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
|
|
58
|
+
|
|
59
|
+
slack = ToolFabric(provider="slack", base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
|
|
60
|
+
agents = AgentFabric(base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
|
|
61
|
+
|
|
62
|
+
@tool
|
|
63
|
+
def echo(x: str) -> str:
|
|
64
|
+
return x
|
|
65
|
+
|
|
66
|
+
bot = Agent(
|
|
67
|
+
system_prompt="demo",
|
|
68
|
+
tools=[echo],
|
|
69
|
+
agents=agents.get_agents(["summarizer"]),
|
|
70
|
+
base_url="http://localhost:8000",
|
|
71
|
+
access_token=TOKEN,
|
|
72
|
+
tenant_id=TENANT,
|
|
73
|
+
provider_fabrics={"slack": slack},
|
|
74
|
+
)
|
|
75
|
+
print(bot.run("Summarize my Slack messages"))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Apache-2.0
|
|
81
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Agentic Fabric SDK (Fabriq)
|
|
2
|
+
|
|
3
|
+
`agentic-fabriq-sdk` provides a Python SDK for interacting with Fabriq/Agentic Fabric.
|
|
4
|
+
|
|
5
|
+
- High-level client: `af_sdk.FabriqClient`
|
|
6
|
+
- DX layer: `af_sdk.dx` (`ToolFabric`, `AgentFabric`, `MCPServer`, `Agent`, and `tool`)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install agentic-fabriq-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from af_sdk.fabriq_client import FabriqClient
|
|
18
|
+
|
|
19
|
+
TOKEN = "..." # Bearer JWT for the Fabriq Gateway
|
|
20
|
+
BASE = "http://localhost:8000"
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
async with FabriqClient(base_url=BASE, auth_token=TOKEN) as af:
|
|
24
|
+
agents = await af.list_agents()
|
|
25
|
+
print(agents)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
DX orchestration:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
|
|
32
|
+
|
|
33
|
+
slack = ToolFabric(provider="slack", base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
|
|
34
|
+
agents = AgentFabric(base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
|
|
35
|
+
|
|
36
|
+
@tool
|
|
37
|
+
def echo(x: str) -> str:
|
|
38
|
+
return x
|
|
39
|
+
|
|
40
|
+
bot = Agent(
|
|
41
|
+
system_prompt="demo",
|
|
42
|
+
tools=[echo],
|
|
43
|
+
agents=agents.get_agents(["summarizer"]),
|
|
44
|
+
base_url="http://localhost:8000",
|
|
45
|
+
access_token=TOKEN,
|
|
46
|
+
tenant_id=TENANT,
|
|
47
|
+
provider_fabrics={"slack": slack},
|
|
48
|
+
)
|
|
49
|
+
print(bot.run("Summarize my Slack messages"))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
Apache-2.0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agentic Fabric SDK
|
|
3
|
+
|
|
4
|
+
Official Python SDK for building connectors and interacting with Agentic Fabric.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .auth.oauth import oauth_required
|
|
8
|
+
from .connectors.base import AgentConnector, ConnectorContext, ToolConnector
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
AFError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
AuthorizationError,
|
|
13
|
+
ConnectorError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
from .models.types import (
|
|
18
|
+
AgentInvokeRequest,
|
|
19
|
+
AgentInvokeResult,
|
|
20
|
+
ToolInvokeRequest,
|
|
21
|
+
ToolInvokeResult,
|
|
22
|
+
)
|
|
23
|
+
from .transport.http import HTTPClient
|
|
24
|
+
from .fabriq_client import FabriqClient
|
|
25
|
+
from .models.audit import AuditEvent
|
|
26
|
+
|
|
27
|
+
__version__ = "1.0.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"oauth_required",
|
|
31
|
+
"ToolConnector",
|
|
32
|
+
"AgentConnector",
|
|
33
|
+
"ConnectorContext",
|
|
34
|
+
"AFError",
|
|
35
|
+
"AuthenticationError",
|
|
36
|
+
"AuthorizationError",
|
|
37
|
+
"ConnectorError",
|
|
38
|
+
"NotFoundError",
|
|
39
|
+
"ValidationError",
|
|
40
|
+
"AgentInvokeRequest",
|
|
41
|
+
"AgentInvokeResult",
|
|
42
|
+
"ToolInvokeRequest",
|
|
43
|
+
"ToolInvokeResult",
|
|
44
|
+
"HTTPClient",
|
|
45
|
+
"FabriqClient",
|
|
46
|
+
"AuditEvent",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Lazy expose dx submodule under af_sdk.dx
|
|
50
|
+
from importlib import import_module as _import_module # noqa: E402
|
|
51
|
+
|
|
52
|
+
def __getattr__(name):
|
|
53
|
+
if name == "dx":
|
|
54
|
+
return _import_module("af_sdk.dx")
|
|
55
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication and authorization utilities for Agentic Fabric SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .oauth import (
|
|
6
|
+
api_key_required,
|
|
7
|
+
mtls_required,
|
|
8
|
+
no_auth_required,
|
|
9
|
+
oauth_required,
|
|
10
|
+
ScopeValidator,
|
|
11
|
+
TokenValidator,
|
|
12
|
+
)
|
|
13
|
+
from .token_cache import TokenManager, VaultClient
|
|
14
|
+
|
|
15
|
+
# DPoP helper will be provided from af_sdk.auth.dpop
|
|
16
|
+
try:
|
|
17
|
+
from .dpop import create_dpop_proof
|
|
18
|
+
except Exception: # pragma: no cover - optional import if file missing
|
|
19
|
+
create_dpop_proof = None # type: ignore
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"oauth_required",
|
|
23
|
+
"api_key_required",
|
|
24
|
+
"mtls_required",
|
|
25
|
+
"no_auth_required",
|
|
26
|
+
"ScopeValidator",
|
|
27
|
+
"TokenValidator",
|
|
28
|
+
"TokenManager",
|
|
29
|
+
"VaultClient",
|
|
30
|
+
"create_dpop_proof",
|
|
31
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client-side DPoP (Proof of Possession) helper for AF SDK.
|
|
3
|
+
|
|
4
|
+
This provides a simple HMAC-signed JWT for development that matches the
|
|
5
|
+
Gateway's mock PoP verifier semantics. In production, replace with a
|
|
6
|
+
DPoP JWT signed using a private key corresponding to the client cert
|
|
7
|
+
thumbprint per RFC 9449.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Dict, Optional
|
|
14
|
+
|
|
15
|
+
import jwt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_dpop_proof(*, method: str, url: str, thumbprint: str = "dev-thumbprint", lifetime_s: int = 60, secret: Optional[str] = None) -> str:
|
|
19
|
+
"""Create a development DPoP-like JWT for AF mock endpoints.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
method: HTTP method, e.g., "POST".
|
|
23
|
+
url: Full request URL.
|
|
24
|
+
thumbprint: x5t#S256 thumbprint string (dev default).
|
|
25
|
+
lifetime_s: Token lifetime in seconds.
|
|
26
|
+
secret: HMAC secret for signing (dev only). If not provided, uses a default.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A compact JWT string to send in the DPoP header.
|
|
30
|
+
"""
|
|
31
|
+
now = int(time.time())
|
|
32
|
+
payload: Dict = {
|
|
33
|
+
"htm": method.upper(),
|
|
34
|
+
"htu": url,
|
|
35
|
+
"iat": now,
|
|
36
|
+
"exp": now + lifetime_s,
|
|
37
|
+
"cnf": {"x5t#S256": thumbprint},
|
|
38
|
+
"typ": "pop",
|
|
39
|
+
}
|
|
40
|
+
key = secret or "af-dev-pop-secret"
|
|
41
|
+
return jwt.encode(payload, key, algorithm="HS256")
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth authentication decorator and helpers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any, Awaitable, Callable, List, Optional
|
|
8
|
+
|
|
9
|
+
from ..exceptions import AuthenticationError, TokenRefreshError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def oauth_required(*, scopes: List[str], refresh_if_expired: bool = True):
|
|
13
|
+
"""
|
|
14
|
+
Decorator that injects a valid OAuth2 Bearer token for the current user.
|
|
15
|
+
|
|
16
|
+
Automatically:
|
|
17
|
+
1. Pulls access token from TokenManager (refreshes if expired)
|
|
18
|
+
2. Populates `Authorization` header
|
|
19
|
+
3. Enforces that requested scopes ⊆ granted scopes
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
scopes: List of required OAuth scopes
|
|
23
|
+
refresh_if_expired: Whether to attempt token refresh if expired
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
AuthenticationError: If token is invalid or missing
|
|
27
|
+
TokenRefreshError: If token refresh fails
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
31
|
+
@wraps(fn)
|
|
32
|
+
async def wrapper(self, *args, **kwargs):
|
|
33
|
+
ctx = getattr(self, "ctx", None)
|
|
34
|
+
if not ctx:
|
|
35
|
+
raise AuthenticationError("Connector context not available")
|
|
36
|
+
|
|
37
|
+
tool_id = getattr(self, "TOOL_ID", None)
|
|
38
|
+
if not tool_id:
|
|
39
|
+
raise AuthenticationError("TOOL_ID not set in connector")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Get OAuth token from token manager
|
|
43
|
+
token = await ctx.token_manager.get_oauth_token(
|
|
44
|
+
tool_id=tool_id,
|
|
45
|
+
user_id=ctx.user_id,
|
|
46
|
+
scopes=scopes,
|
|
47
|
+
refresh_if_expired=refresh_if_expired,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Inject Authorization header
|
|
51
|
+
headers = kwargs.setdefault("headers", {})
|
|
52
|
+
headers.setdefault("Authorization", f"Bearer {token}")
|
|
53
|
+
|
|
54
|
+
# Log the request (without token)
|
|
55
|
+
ctx.logger.info(
|
|
56
|
+
f"Making OAuth request to {tool_id}",
|
|
57
|
+
extra={"scopes": scopes, "user_id": ctx.user_id},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return await fn(self, *args, **kwargs)
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
ctx.logger.error(f"OAuth authentication failed: {e}")
|
|
64
|
+
if isinstance(e, (AuthenticationError, TokenRefreshError)):
|
|
65
|
+
raise
|
|
66
|
+
raise AuthenticationError(f"OAuth authentication failed: {e}")
|
|
67
|
+
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def api_key_required(*, key_name: str = "api_key", header_name: str = "X-API-Key"):
|
|
74
|
+
"""
|
|
75
|
+
Decorator that injects an API key for the current user.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
key_name: Name of the API key in the vault
|
|
79
|
+
header_name: HTTP header name for the API key
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
AuthenticationError: If API key is missing or invalid
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
86
|
+
@wraps(fn)
|
|
87
|
+
async def wrapper(self, *args, **kwargs):
|
|
88
|
+
ctx = getattr(self, "ctx", None)
|
|
89
|
+
if not ctx:
|
|
90
|
+
raise AuthenticationError("Connector context not available")
|
|
91
|
+
|
|
92
|
+
tool_id = getattr(self, "TOOL_ID", None)
|
|
93
|
+
if not tool_id:
|
|
94
|
+
raise AuthenticationError("TOOL_ID not set in connector")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Get API key from vault
|
|
98
|
+
secret_path = f"af/{ctx.tenant_id}/{ctx.user_id}/api_keys/{tool_id}/{key_name}"
|
|
99
|
+
secret = await ctx.token_manager.vault_client.read_secret(secret_path)
|
|
100
|
+
|
|
101
|
+
if not secret or "value" not in secret:
|
|
102
|
+
raise AuthenticationError(f"API key not found: {key_name}")
|
|
103
|
+
|
|
104
|
+
api_key = secret["value"]
|
|
105
|
+
|
|
106
|
+
# Inject API key header
|
|
107
|
+
headers = kwargs.setdefault("headers", {})
|
|
108
|
+
headers.setdefault(header_name, api_key)
|
|
109
|
+
|
|
110
|
+
# Log the request
|
|
111
|
+
ctx.logger.info(
|
|
112
|
+
f"Making API key request to {tool_id}",
|
|
113
|
+
extra={"key_name": key_name, "user_id": ctx.user_id},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return await fn(self, *args, **kwargs)
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
ctx.logger.error(f"API key authentication failed: {e}")
|
|
120
|
+
if isinstance(e, AuthenticationError):
|
|
121
|
+
raise
|
|
122
|
+
raise AuthenticationError(f"API key authentication failed: {e}")
|
|
123
|
+
|
|
124
|
+
return wrapper
|
|
125
|
+
|
|
126
|
+
return decorator
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def mtls_required(*, cert_path: Optional[str] = None, key_path: Optional[str] = None):
|
|
130
|
+
"""
|
|
131
|
+
Decorator that configures mutual TLS authentication.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
cert_path: Path to client certificate (optional, uses default if not provided)
|
|
135
|
+
key_path: Path to client private key (optional, uses default if not provided)
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
AuthenticationError: If mTLS configuration fails
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
142
|
+
@wraps(fn)
|
|
143
|
+
async def wrapper(self, *args, **kwargs):
|
|
144
|
+
ctx = getattr(self, "ctx", None)
|
|
145
|
+
if not ctx:
|
|
146
|
+
raise AuthenticationError("Connector context not available")
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
# Configure mTLS for the HTTP client
|
|
150
|
+
# This would typically be done at the session level
|
|
151
|
+
# For now, we'll add it to the kwargs
|
|
152
|
+
cert_config = (cert_path, key_path) if cert_path and key_path else None
|
|
153
|
+
kwargs.setdefault("cert", cert_config)
|
|
154
|
+
|
|
155
|
+
ctx.logger.info(
|
|
156
|
+
"Making mTLS request",
|
|
157
|
+
extra={"cert_path": cert_path, "user_id": ctx.user_id},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return await fn(self, *args, **kwargs)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
ctx.logger.error(f"mTLS authentication failed: {e}")
|
|
164
|
+
raise AuthenticationError(f"mTLS authentication failed: {e}")
|
|
165
|
+
|
|
166
|
+
return wrapper
|
|
167
|
+
|
|
168
|
+
return decorator
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def no_auth_required(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
172
|
+
"""
|
|
173
|
+
Decorator that marks a method as not requiring authentication.
|
|
174
|
+
Useful for public endpoints or health checks.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
@wraps(fn)
|
|
178
|
+
async def wrapper(self, *args, **kwargs):
|
|
179
|
+
ctx = getattr(self, "ctx", None)
|
|
180
|
+
if ctx:
|
|
181
|
+
ctx.logger.info("Making unauthenticated request")
|
|
182
|
+
return await fn(self, *args, **kwargs)
|
|
183
|
+
|
|
184
|
+
return wrapper
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ScopeValidator:
|
|
188
|
+
"""Helper class for validating OAuth scopes."""
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def validate_scopes(required_scopes: List[str], granted_scopes: List[str]) -> bool:
|
|
192
|
+
"""
|
|
193
|
+
Validate that all required scopes are granted.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
required_scopes: List of required scopes
|
|
197
|
+
granted_scopes: List of granted scopes
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if all required scopes are granted, False otherwise
|
|
201
|
+
"""
|
|
202
|
+
return set(required_scopes).issubset(set(granted_scopes))
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def missing_scopes(required_scopes: List[str], granted_scopes: List[str]) -> List[str]:
|
|
206
|
+
"""
|
|
207
|
+
Get list of missing scopes.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
required_scopes: List of required scopes
|
|
211
|
+
granted_scopes: List of granted scopes
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of missing scopes
|
|
215
|
+
"""
|
|
216
|
+
return list(set(required_scopes) - set(granted_scopes))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TokenValidator:
|
|
220
|
+
"""Helper class for validating OAuth tokens."""
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def is_expired(expires_at: float) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
Check if a token is expired.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
expires_at: Expiration timestamp
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if token is expired, False otherwise
|
|
232
|
+
"""
|
|
233
|
+
return time.time() >= expires_at
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def expires_soon(expires_at: float, buffer_seconds: int = 300) -> bool:
|
|
237
|
+
"""
|
|
238
|
+
Check if a token expires soon.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
expires_at: Expiration timestamp
|
|
242
|
+
buffer_seconds: Buffer time in seconds
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if token expires within buffer time, False otherwise
|
|
246
|
+
"""
|
|
247
|
+
return time.time() + buffer_seconds >= expires_at
|