Skip to content

Commit 3b4dcf2

Browse files
committed
Refactor agent authorization to use delegation chain from JWT
Replaces custom JWT parsing with a utility function to extract user_id and delegation chain from the JWT. Updates the agent authorization logic to use the extracted actors for original_callers and improves error handling and logging. Also updates the IdentityClient interface to accept original_callers and adds license headers to jwt.py.
1 parent 981fe54 commit 3b4dcf2

File tree

3 files changed

+56
-62
lines changed

3 files changed

+56
-62
lines changed

veadk/integrations/ve_identity/identity_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,7 @@ def check_permission(
695695
principal: Dict[str, str],
696696
operation: Dict[str, str],
697697
resource: Dict[str, str],
698+
original_callers: Optional[List[Dict[str, str]]] = None,
698699
namespace: str = "default",
699700
) -> bool:
700701
"""Check if the principal has permission to perform the operation on the resource.
@@ -703,6 +704,7 @@ def check_permission(
703704
principal: Principal information, e.g., {"Type": "User", "Id": "user123"}
704705
operation: Operation to check, e.g., {"Type": "Action", "Id": "invoke"}
705706
resource: Resource information, e.g., {"Type": "Agent", "Id": "agent456"}
707+
original_callers: Optional list of original callers.
706708
namespace: Namespace of the resource. Defaults to "default".
707709
708710
Returns:
@@ -721,6 +723,7 @@ def check_permission(
721723
operation=operation,
722724
principal=principal,
723725
resource=resource,
726+
original_callers=original_callers,
724727
)
725728

726729
response: volcenginesdkid.CheckPermissionResponse = (

veadk/tools/builtin_tools/agent_authorization.py

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from veadk.integrations.ve_identity.identity_client import IdentityClient
2424
from veadk.integrations.ve_identity.token_manager import get_workload_token
2525
from veadk.utils.logger import get_logger
26+
from veadk.utils.jwt import extract_delegation_chain_from_jwt
2627

2728
logger = get_logger(__name__)
2829

@@ -31,85 +32,61 @@
3132
identity_client = IdentityClient(region=region)
3233

3334

34-
def _strip_bearer_prefix(token: str) -> str:
35-
"""Remove 'Bearer ' prefix from token if present.
36-
Args:
37-
token: Token string that may contain "Bearer " prefix
38-
Returns:
39-
Token without "Bearer " prefix
40-
"""
41-
return token[7:] if token.lower().startswith("bearer ") else token
42-
43-
44-
def _extract_role_id_from_jwt(token: str) -> Optional[str]:
45-
"""Extract role_id (sub field) from JWT token.
46-
Args:
47-
token: JWT token string (with or without "Bearer " prefix)
48-
Returns:
49-
Role ID from sub field, or None if parsing fails
50-
"""
51-
try:
52-
# Remove "Bearer " prefix if present
53-
token = _strip_bearer_prefix(token)
54-
55-
# JWT token has 3 parts separated by dots: header.payload.signature
56-
parts = token.split(".")
57-
if len(parts) != 3:
58-
logger.error("Invalid JWT format: expected 3 parts")
59-
return None
60-
61-
# Decode payload (second part)
62-
payload_part = parts[1]
63-
64-
# Add padding for base64url decoding (JWT doesn't use padding)
65-
missing_padding = len(payload_part) % 4
66-
if missing_padding:
67-
payload_part += "=" * (4 - missing_padding)
68-
69-
# Decode base64 and parse JSON
70-
decoded_bytes = base64.urlsafe_b64decode(payload_part)
71-
payload = json.loads(decoded_bytes.decode("utf-8"))
72-
73-
# Extract sub field as role_id
74-
return payload.get("act").get("sub")
75-
76-
except (ValueError, json.JSONDecodeError) as e:
77-
logger.error(f"Failed to parse JWT token: {e}")
78-
return None
79-
except Exception as e:
80-
logger.error(f"Unexpected error parsing JWT: {e}")
81-
return None
82-
83-
8435
async def check_agent_authorization(
8536
callback_context: CallbackContext,
8637
) -> Optional[types.Content]:
87-
"""Check if the agent is authorized to run using VeIdentity."""
88-
user_id = callback_context._invocation_context.user_id
89-
38+
"""Check if the agent is authorized to run using Agent Identity."""
9039
try:
9140
workload_token = await get_workload_token(
9241
tool_context=callback_context, identity_client=identity_client
9342
)
9443

95-
# Parse role_id from workload_token
96-
role_id = _extract_role_id_from_jwt(workload_token)
44+
# Parse user_id and actors from workload_token
45+
user_id, actors = extract_delegation_chain_from_jwt(workload_token)
9746

98-
principal = {"Type": "User", "Id": user_id}
99-
operation = {"Type": "Action", "Id": "invoke"}
100-
resource = {"Type": "Agent", "Id": role_id}
47+
if not user_id:
48+
logger.warning("Failed to extract user_id from JWT token")
49+
return types.Content(
50+
parts=[types.Part(text="Failed to verify agent authorization.")],
51+
role="model",
52+
)
53+
54+
if len(actors) == 0:
55+
logger.warning("Failed to extract actors from JWT token")
56+
return types.Content(
57+
parts=[types.Part(text="Failed to verify agent authorization.")],
58+
role="model",
59+
)
60+
61+
# The first actor in the chain is the agent itself
62+
role_id = actors[0]
63+
64+
principal = {"Type": "user", "Id": user_id}
65+
operation = {"Type": "action", "Id": "invoke"}
66+
resource = {"Type": "agent", "Id": role_id}
67+
original_callers = [{"Type": "agent", "Id": actor} for actor in actors[1:]]
10168

10269
allowed = identity_client.check_permission(
103-
principal=principal, operation=operation, resource=resource
70+
principal=principal,
71+
operation=operation,
72+
resource=resource,
73+
original_callers=original_callers,
10474
)
10575

10676
if allowed:
107-
logger.info("Agent is authorized to run.")
77+
logger.info(f"Agent {role_id} is authorized to run by user {user_id}.")
10878
return None
10979
else:
110-
logger.warning("Agent is not authorized to run.")
80+
logger.warning(
81+
f"Agent {role_id} is not authorized to run by user {user_id}."
82+
)
11183
return types.Content(
112-
parts=[types.Part(text="Agent is not authorized to run.")], role="model"
84+
parts=[
85+
types.Part(
86+
text=f"Agent {role_id} is not authorized to run by user {user_id}."
87+
)
88+
],
89+
role="model",
11390
)
11491

11592
except Exception as e:

veadk/utils/jwt.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
115
import base64
216
import json
317
from typing import Optional

0 commit comments

Comments
 (0)