aixtools 0.2.5__tar.gz → 0.2.7__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 aixtools might be problematic. Click here for more details.
- {aixtools-0.2.5 → aixtools-0.2.7}/PKG-INFO +1 -1
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/_version.py +3 -3
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/utils.py +36 -13
- aixtools-0.2.7/aixtools/auth/auth.py +149 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/config.py +3 -2
- aixtools-0.2.5/aixtools/auth/auth.py +0 -70
- {aixtools-0.2.5 → aixtools-0.2.7}/README.md +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/config.toml +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/bn.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/en-US.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/gu.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/he-IL.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/hi.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/ja.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/kn.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/ml.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/mr.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/nl.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/ta.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/te.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/.chainlit/translations/zh-CN.json +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/app.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/card.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/remote_agent_connection.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/utils.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/agents/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/agents/agent.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/agents/agent_batch.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/agents/print_nodes.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/agents/prompt.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/app.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/auth/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/chainlit.md +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/compliance/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/compliance/private_data.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/context.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/db/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/db/database.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/db/vector_db.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/evals/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/evals/__main__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/evals/dataset.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/evals/discovery.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/evals/run_evals.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/google/client.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/app.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/display.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/export.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/filters.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/log_utils.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/log_view/node_summary.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logfilters/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logfilters/context_filter.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/log_objects.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/logging_config.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/mcp_log_models.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/mcp_logger.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/model_patch_logging.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/logging/open_telemetry.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/mcp/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/mcp/client.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/mcp/example_client.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/mcp/example_server.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/mcp/fast_mcp_log.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/mcp/faulty_mcp.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/model_patch/model_patch.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/server/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/server/app_mounter.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/server/path.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/server/utils.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/testing/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/testing/aix_test_model.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/testing/mock_tool.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/testing/model_patch_cache.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/tools/doctor/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/tools/doctor/mcp_tool_doctor.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/tools/doctor/tool_doctor.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/tools/doctor/tool_recommendation.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/chainlit/cl_agent_show.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/chainlit/cl_utils.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/config_util.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/enum_with_description.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/files.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/persisted_dict.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/utils/utils.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/vault/__init__.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools/vault/vault.py +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/aixtools.egg-info/SOURCES.txt +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/pyproject.toml +0 -0
- {aixtools-0.2.5 → aixtools-0.2.7}/setup.cfg +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
31
|
+
__version__ = version = '0.2.7'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gb47ecb535'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Utilities for handling A2A SDK agent cards and connections."""
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
|
|
3
5
|
import httpx
|
|
@@ -8,13 +10,34 @@ from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH, PREV_AGENT_CARD_WELL_KNOWN_PAT
|
|
|
8
10
|
|
|
9
11
|
from aixtools.a2a.google_sdk.remote_agent_connection import RemoteAgentConnection
|
|
10
12
|
from aixtools.context import DEFAULT_SESSION_ID, DEFAULT_USER_ID, SessionIdTuple
|
|
13
|
+
from aixtools.logging.logging_config import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
DEFAULT_A2A_TIMEOUT = 60.0
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
class AgentCardLoadFailedError(Exception):
|
|
14
21
|
pass
|
|
15
22
|
|
|
16
23
|
|
|
24
|
+
async def get_agent_card(client: httpx.AsyncClient, address: str) -> AgentCard:
|
|
25
|
+
"""Retrieve the agent card from the given agent address."""
|
|
26
|
+
for card_path in [AGENT_CARD_WELL_KNOWN_PATH, PREV_AGENT_CARD_WELL_KNOWN_PATH]:
|
|
27
|
+
try:
|
|
28
|
+
card_resolver = A2ACardResolver(client, address, card_path)
|
|
29
|
+
card = await card_resolver.get_agent_card()
|
|
30
|
+
card.url = address
|
|
31
|
+
return card
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.warning(f"Error retrieving agent card from {address} at path {card_path}: {e}")
|
|
34
|
+
|
|
35
|
+
raise AgentCardLoadFailedError(f"Failed to load agent card from {address}")
|
|
36
|
+
|
|
37
|
+
|
|
17
38
|
class _AgentCardResolver:
|
|
39
|
+
"""Helper class to resolve and manage agent cards and their connections."""
|
|
40
|
+
|
|
18
41
|
def __init__(self, client: httpx.AsyncClient):
|
|
19
42
|
self._httpx_client = client
|
|
20
43
|
self._a2a_client_factory = ClientFactory(ClientConfig(httpx_client=self._httpx_client))
|
|
@@ -25,17 +48,13 @@ class _AgentCardResolver:
|
|
|
25
48
|
self.clients[card.name] = remote_connection
|
|
26
49
|
|
|
27
50
|
async def retrieve_card(self, address: str):
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
except Exception as e:
|
|
36
|
-
print(f"Error retrieving agent card from {address} at path {card_path}: {e}")
|
|
37
|
-
|
|
38
|
-
raise AgentCardLoadFailedError(f"Failed to load agent card from {address}")
|
|
51
|
+
try:
|
|
52
|
+
card = await get_agent_card(self._httpx_client, address)
|
|
53
|
+
self.register_agent_card(card)
|
|
54
|
+
return
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Error retrieving agent card from {address}: {e}")
|
|
57
|
+
return
|
|
39
58
|
|
|
40
59
|
async def get_a2a_clients(self, agent_hosts: list[str]) -> dict[str, RemoteAgentConnection]:
|
|
41
60
|
async with asyncio.TaskGroup() as task_group:
|
|
@@ -45,15 +64,19 @@ class _AgentCardResolver:
|
|
|
45
64
|
return self.clients
|
|
46
65
|
|
|
47
66
|
|
|
48
|
-
async def get_a2a_clients(
|
|
67
|
+
async def get_a2a_clients(
|
|
68
|
+
ctx: SessionIdTuple, agent_hosts: list[str], *, timeout: float = DEFAULT_A2A_TIMEOUT
|
|
69
|
+
) -> dict[str, RemoteAgentConnection]:
|
|
70
|
+
"""Get A2A clients for all agents defined in the configuration."""
|
|
49
71
|
headers = {
|
|
50
72
|
"user-id": ctx[0],
|
|
51
73
|
"session-id": ctx[1],
|
|
52
74
|
}
|
|
53
|
-
httpx_client = httpx.AsyncClient(headers=headers, timeout=
|
|
75
|
+
httpx_client = httpx.AsyncClient(headers=headers, timeout=timeout, follow_redirects=True)
|
|
54
76
|
return await _AgentCardResolver(httpx_client).get_a2a_clients(agent_hosts)
|
|
55
77
|
|
|
56
78
|
|
|
57
79
|
def get_session_id_tuple(context: RequestContext) -> SessionIdTuple:
|
|
80
|
+
"""Get the user_id, session_id tuple from the request context."""
|
|
58
81
|
headers = context.call_context.state.get("headers", {})
|
|
59
82
|
return headers.get("user-id", DEFAULT_USER_ID), headers.get("session-id", DEFAULT_SESSION_ID)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module that manages OAuth2 functions for authentication
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import jwt
|
|
9
|
+
from fastapi import HTTPException
|
|
10
|
+
from jwt import ExpiredSignatureError, InvalidAudienceError, InvalidIssuerError, InvalidSignatureError, PyJWKClient
|
|
11
|
+
|
|
12
|
+
from aixtools.utils import config
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthTokenErrorCode(str, enum.Enum):
|
|
18
|
+
"""Enum for error codes returned by the AuthTokenError exception."""
|
|
19
|
+
|
|
20
|
+
TOKEN_EXPIRED = "Token expired"
|
|
21
|
+
INVALID_AUDIENCE = "Token not for expected audience"
|
|
22
|
+
INVALID_ISSUER = "Token not for expected issuer"
|
|
23
|
+
INVALID_SIGNATURE = "Token signature error"
|
|
24
|
+
INVALID_TOKEN = "Invalid token"
|
|
25
|
+
JWT_ERROR = "Generic JWT error"
|
|
26
|
+
MISSING_GROUPS_ERROR = "Missing authorized groups"
|
|
27
|
+
INVALID_TOKEN_SCOPE = "Token scope does not match configured scope"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthTokenError(Exception):
|
|
31
|
+
"""Exception raised for authentication token errors."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, error_code: AuthTokenErrorCode, msg: str = None):
|
|
34
|
+
self.error_code = error_code
|
|
35
|
+
error_msg = error_code.value if msg is None else msg
|
|
36
|
+
super().__init__(error_msg)
|
|
37
|
+
|
|
38
|
+
def to_http_exception(self, required_scope: str = None, realm: str = "MCP") -> HTTPException:
|
|
39
|
+
"""
|
|
40
|
+
Returns an HTTPException with 401 status for all AuthTokenErrorCode,
|
|
41
|
+
including MCP JSON body and WWW-Authenticate header.
|
|
42
|
+
"""
|
|
43
|
+
status_code = 401
|
|
44
|
+
www_error = (
|
|
45
|
+
"insufficient_scope" if self.error_code == AuthTokenErrorCode.INVALID_TOKEN_SCOPE else "invalid_token"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
header_value = f'Bearer realm="{realm}", error="{www_error}", error_description="{self.error_code.value}"'
|
|
49
|
+
if self.error_code == AuthTokenErrorCode.INVALID_TOKEN_SCOPE and required_scope:
|
|
50
|
+
header_value += f', scope="{required_scope}"'
|
|
51
|
+
|
|
52
|
+
detail = {"error": {"code": self.error_code.name, "message": self.error_code.value}}
|
|
53
|
+
if self.error_code == AuthTokenErrorCode.INVALID_TOKEN_SCOPE and required_scope:
|
|
54
|
+
detail["error"]["required_scope"] = required_scope
|
|
55
|
+
|
|
56
|
+
return HTTPException(status_code=status_code, detail=detail, headers={"WWW-Authenticate": header_value})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AccessTokenVerifier:
|
|
60
|
+
"""
|
|
61
|
+
Verifies Microsoft SSO JWT token against the configured Tenant ID, Audience, API ID and Issuer URL.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
tenant_id = config.APP_TENANT_ID
|
|
66
|
+
self.api_id = config.APP_API_ID
|
|
67
|
+
self.issuer_url = f"https://sts.windows.net/{tenant_id}/"
|
|
68
|
+
|
|
69
|
+
self.authorized_groups = set(config.APP_AUTHORIZED_GROUPS.split(",")) if config.APP_AUTHORIZED_GROUPS else set()
|
|
70
|
+
if not self.authorized_groups:
|
|
71
|
+
logger.warning("No authorized groups configured")
|
|
72
|
+
|
|
73
|
+
# Azure AD endpoints
|
|
74
|
+
jwks_url = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
|
|
75
|
+
self.jwks_client = PyJWKClient(
|
|
76
|
+
uri=jwks_url,
|
|
77
|
+
# cache keys url response to reduce SSO server network calls,
|
|
78
|
+
# as public keys are not expected to change frequently
|
|
79
|
+
cache_jwk_set=True,
|
|
80
|
+
# cache resolved public keys
|
|
81
|
+
cache_keys=True,
|
|
82
|
+
# cache url response for 10 hours
|
|
83
|
+
lifespan=36000,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.info("Using JWKS: %s", jwks_url)
|
|
87
|
+
|
|
88
|
+
def verify(self, token: str) -> dict:
|
|
89
|
+
"""
|
|
90
|
+
Verifies The JWT access token and returns decoded claims as a dictionary if the token is
|
|
91
|
+
valid, otherwise raises an AuthTokenError
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
|
|
95
|
+
logger.info("Verifying JWT token")
|
|
96
|
+
claims = jwt.decode(
|
|
97
|
+
token,
|
|
98
|
+
signing_key.key,
|
|
99
|
+
algorithms=["RS256"],
|
|
100
|
+
audience=self.api_id,
|
|
101
|
+
issuer=self.issuer_url,
|
|
102
|
+
# ensure audience verification is carried out
|
|
103
|
+
options={"verify_aud": True},
|
|
104
|
+
)
|
|
105
|
+
logger.info("Verified JWT token")
|
|
106
|
+
return claims
|
|
107
|
+
|
|
108
|
+
except ExpiredSignatureError as e:
|
|
109
|
+
raise AuthTokenError(AuthTokenErrorCode.TOKEN_EXPIRED) from e
|
|
110
|
+
except InvalidAudienceError as e:
|
|
111
|
+
raise AuthTokenError(AuthTokenErrorCode.INVALID_AUDIENCE) from e
|
|
112
|
+
except InvalidIssuerError as e:
|
|
113
|
+
raise AuthTokenError(AuthTokenErrorCode.INVALID_ISSUER) from e
|
|
114
|
+
except InvalidSignatureError as e:
|
|
115
|
+
raise AuthTokenError(AuthTokenErrorCode.INVALID_SIGNATURE) from e
|
|
116
|
+
except jwt.exceptions.PyJWTError as e:
|
|
117
|
+
raise AuthTokenError(AuthTokenErrorCode.JWT_ERROR) from e
|
|
118
|
+
|
|
119
|
+
def authorize_claims(self, claims: dict, expected_scope: str):
|
|
120
|
+
"""
|
|
121
|
+
Authorize claims based on token scope, expected scope and authorized groups
|
|
122
|
+
claims: decoded JWT claims
|
|
123
|
+
expected_scope: expected scope for the token
|
|
124
|
+
Raises AuthTokenError if authorization fails.
|
|
125
|
+
"""
|
|
126
|
+
logger.info("Checking JWT token claims")
|
|
127
|
+
if expected_scope:
|
|
128
|
+
token_scopes = claims.get("scp", "").split()
|
|
129
|
+
if expected_scope not in token_scopes:
|
|
130
|
+
logger.error("Expected token scope: %s, got: %s", expected_scope, token_scopes)
|
|
131
|
+
raise AuthTokenError(
|
|
132
|
+
AuthTokenErrorCode.INVALID_TOKEN_SCOPE,
|
|
133
|
+
f"Expected token scope: {expected_scope}, got: {token_scopes}",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not self.authorized_groups:
|
|
137
|
+
logger.info("Authorized JWT token, no authorized groups configured")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
groups = claims.get("groups", [])
|
|
141
|
+
if self.authorized_groups & set(groups):
|
|
142
|
+
logger.info("Authorized JWT token, against %s", groups)
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
logger.error("Could not find any group in JWT token, matching: %s", self.authorized_groups)
|
|
146
|
+
raise AuthTokenError(
|
|
147
|
+
AuthTokenErrorCode.MISSING_GROUPS_ERROR,
|
|
148
|
+
f"Could not find any group in JWT token, matching: {self.authorized_groups}",
|
|
149
|
+
)
|
|
@@ -135,5 +135,6 @@ APP_CLIENT_ID = get_variable_env("APP_CLIENT_ID")
|
|
|
135
135
|
# used for token audience check
|
|
136
136
|
APP_API_ID = get_variable_env("APP_API_ID")
|
|
137
137
|
APP_TENANT_ID = get_variable_env("APP_TENANT_ID")
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
|
|
139
|
+
# used for token authorization check
|
|
140
|
+
APP_AUTHORIZED_GROUPS = get_variable_env("APP_AUTHORIZED_GROUPS", allow_empty=True)
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module that manages OAuth2 functions for authentication
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
import jwt
|
|
8
|
-
from jwt import ExpiredSignatureError, InvalidAudienceError, InvalidIssuerError, PyJWKClient
|
|
9
|
-
|
|
10
|
-
from aixtools.utils import config
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class AuthTokenError(Exception):
|
|
16
|
-
"""Exception raised for authentication token errors."""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# pylint: disable=too-few-public-methods
|
|
20
|
-
class AccessTokenVerifier:
|
|
21
|
-
"""
|
|
22
|
-
Verifies Microsoft SSO JWT token against the configured Tenant ID, Audience, API ID and Issuer URL.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
def __init__(self):
|
|
26
|
-
tenant_id = config.APP_TENANT_ID
|
|
27
|
-
self.api_id = config.APP_API_ID
|
|
28
|
-
self.issuer_url = config.APP_ISSUER_URL
|
|
29
|
-
# Azure AD endpoints
|
|
30
|
-
jwks_url = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
|
|
31
|
-
self.jwks_client = PyJWKClient(
|
|
32
|
-
uri=jwks_url,
|
|
33
|
-
# cache keys url response to reduce SSO server network calls,
|
|
34
|
-
# as public keys are not expected to change frequently
|
|
35
|
-
cache_jwk_set=True,
|
|
36
|
-
# cache resolved public keys
|
|
37
|
-
cache_keys=True,
|
|
38
|
-
# cache url response for 10 hours
|
|
39
|
-
lifespan=36000,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
logger.info("Using JWKS: %s", jwks_url)
|
|
43
|
-
|
|
44
|
-
def verify(self, token: str) -> dict:
|
|
45
|
-
"""
|
|
46
|
-
Verifies The JWT access token and returns decoded claims as a dictionary if the token is
|
|
47
|
-
valid, otherwise raises an AuthTokenError
|
|
48
|
-
"""
|
|
49
|
-
try:
|
|
50
|
-
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
|
|
51
|
-
|
|
52
|
-
claims = jwt.decode(
|
|
53
|
-
token,
|
|
54
|
-
signing_key.key,
|
|
55
|
-
algorithms=["RS256"],
|
|
56
|
-
audience=self.api_id,
|
|
57
|
-
issuer=self.issuer_url,
|
|
58
|
-
# ensure audience verification is carried out
|
|
59
|
-
options={"verify_aud": True},
|
|
60
|
-
)
|
|
61
|
-
return claims
|
|
62
|
-
|
|
63
|
-
except ExpiredSignatureError as e:
|
|
64
|
-
raise AuthTokenError("Token expired") from e
|
|
65
|
-
except InvalidAudienceError as e:
|
|
66
|
-
raise AuthTokenError(f"Token not for expected audience: {e}") from e
|
|
67
|
-
except InvalidIssuerError as e:
|
|
68
|
-
raise AuthTokenError(f"Token not for expected issuer: {e}") from e
|
|
69
|
-
except jwt.exceptions.PyJWTError as e:
|
|
70
|
-
raise AuthTokenError(f"Invalid token: {e}") from e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aixtools-0.2.5 → aixtools-0.2.7}/aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|