Skip to content

Commit e76df3d

Browse files
pandegoDeanChensj
authored andcommitted
fix: use exchanged OAuth credential in ApplicationIntegrationToolset
Merge #4659 ## Summary - update `ApplicationIntegrationToolset.get_tools()` to propagate `AuthConfig.exchanged_auth_credential` to generated `IntegrationConnectorTool` instances - ensure connector tools use exchanged OAuth credentials (including runtime access token) instead of only the raw auth credential - add regression coverage for exchanged credential propagation Fixes #4553 ## Testing plan - `uv run --extra test pytest -q tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py tests/unittests/tools/application_integration_tool/test_integration_connector_tool.py` ## Notes - This is a minimal fix scoped to connector-tool credential wiring only. Co-authored-by: Shangjie Chen <deanchen@google.com> COPYBARA_INTEGRATE_REVIEW=#4659 from pandego:fix/4553-ait-exchanged-auth-credential 1f9c947 PiperOrigin-RevId: 941281800
1 parent 6aad10d commit e76df3d

2 files changed

Lines changed: 128 additions & 13 deletions

File tree

src/google/adk/tools/application_integration_tool/application_integration_toolset.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from __future__ import annotations
1616

1717
import logging
18+
from typing import Any
19+
from typing import cast
1820
from typing import List
1921
from typing import Optional
2022
from typing import Union
@@ -29,6 +31,7 @@
2931
from ...auth.auth_credential import ServiceAccountCredential
3032
from ...auth.auth_schemes import AuthScheme
3133
from ...auth.auth_tool import AuthConfig
34+
from ..base_tool import BaseTool
3235
from ..base_toolset import BaseToolset
3336
from ..base_toolset import ToolPredicate
3437
from ..openapi_tool.auth.auth_helpers import service_account_scheme_credential
@@ -43,7 +46,7 @@
4346

4447

4548
# TODO: Apply a common toolset interface
46-
class ApplicationIntegrationToolset(BaseToolset):
49+
class ApplicationIntegrationToolset(BaseToolset): # type: ignore[misc]
4750
"""ApplicationIntegrationToolset generates tools from a given Application
4851
Integration or Integration Connector resource.
4952
@@ -182,11 +185,15 @@ def __init__(
182185
"Invalid request, Either integration or (connection and"
183186
" (entity_operations or actions)) should be provided."
184187
)
185-
self._openapi_toolset = None
186-
self._tools = []
188+
self._openapi_toolset: Optional[OpenAPIToolset] = None
189+
self._tools: list[IntegrationConnectorTool] = []
187190
self._parse_spec_to_toolset(spec, connection_details)
188191

189-
def _parse_spec_to_toolset(self, spec_dict, connection_details):
192+
def _parse_spec_to_toolset(
193+
self,
194+
spec_dict: dict[str, Any],
195+
connection_details: dict[str, Any],
196+
) -> None:
190197
"""Parses the spec dict to OpenAPI toolset."""
191198
if self._service_account_json:
192199
sa_credential = ServiceAccountCredential.model_validate_json(
@@ -270,21 +277,64 @@ def _parse_spec_to_toolset(self, spec_dict, connection_details):
270277
)
271278
)
272279

280+
def _clone_connector_tool_with_auth_credential(
281+
self,
282+
tool: IntegrationConnectorTool,
283+
auth_credential: AuthCredential,
284+
) -> IntegrationConnectorTool:
285+
return IntegrationConnectorTool(
286+
name=tool.name,
287+
description=tool.description,
288+
connection_name=tool._connection_name,
289+
connection_host=tool._connection_host,
290+
connection_service_name=tool._connection_service_name,
291+
entity=tool._entity,
292+
action=tool._action,
293+
operation=tool._operation,
294+
rest_api_tool=tool._rest_api_tool,
295+
auth_scheme=tool._auth_scheme,
296+
auth_credential=auth_credential,
297+
)
298+
273299
@override
274300
async def get_tools(
275301
self,
276302
readonly_context: Optional[ReadonlyContext] = None,
277-
) -> List[RestApiTool]:
278-
return (
279-
[
280-
tool
281-
for tool in self._tools
282-
if self._is_tool_selected(tool, readonly_context)
283-
]
284-
if self._openapi_toolset is None
285-
else await self._openapi_toolset.get_tools(readonly_context)
303+
) -> List[BaseTool]:
304+
if self._openapi_toolset is not None:
305+
return cast(
306+
List[BaseTool],
307+
await self._openapi_toolset.get_tools(readonly_context),
308+
)
309+
310+
exchanged_auth_credential = (
311+
self._auth_config.exchanged_auth_credential
312+
if self._auth_config
313+
else None
286314
)
287315

316+
selected_tools = [
317+
tool
318+
for tool in self._tools
319+
if self._is_tool_selected(tool, readonly_context)
320+
]
321+
322+
if not exchanged_auth_credential:
323+
return selected_tools
324+
325+
resolved_tools: List[BaseTool] = []
326+
for tool in selected_tools:
327+
if isinstance(tool, IntegrationConnectorTool) and tool._auth_scheme:
328+
resolved_tools.append(
329+
self._clone_connector_tool_with_auth_credential(
330+
tool, exchanged_auth_credential
331+
)
332+
)
333+
else:
334+
resolved_tools.append(tool)
335+
336+
return resolved_tools
337+
288338
@override
289339
async def close(self) -> None:
290340
if self._openapi_toolset:

tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,3 +671,68 @@ async def test_init_with_connection_with_auth_override_disabled_and_custom_auth(
671671
assert (await toolset.get_tools())[0]._operation == "EXECUTE_ACTION"
672672
assert not (await toolset.get_tools())[0]._auth_scheme
673673
assert not (await toolset.get_tools())[0]._auth_credential
674+
675+
676+
@pytest.mark.asyncio
677+
async def test_get_tools_uses_exchanged_auth_credential_when_available(
678+
project,
679+
location,
680+
mock_integration_client,
681+
mock_connections_client,
682+
mock_openapi_action_spec_parser,
683+
connection_details_auth_override_enabled,
684+
):
685+
connection_name = "test-connection"
686+
actions_list = ["create"]
687+
mock_connections_client.return_value.get_connection_details.return_value = (
688+
connection_details_auth_override_enabled
689+
)
690+
691+
oauth2_data_google_cloud = {
692+
"type": "oauth2",
693+
"flows": {
694+
"authorizationCode": {
695+
"authorizationUrl": "https://test-url/o/oauth2/auth",
696+
"tokenUrl": "https://test-url/token",
697+
"scopes": {
698+
"https://test-url/auth/test-scope": "test scope",
699+
},
700+
}
701+
},
702+
}
703+
704+
oauth2_scheme = dict_to_auth_scheme(oauth2_data_google_cloud)
705+
raw_auth_credential = AuthCredential(
706+
auth_type=AuthCredentialTypes.OAUTH2,
707+
oauth2=OAuth2Auth(
708+
client_id="test-client-id",
709+
client_secret="test-client-secret",
710+
),
711+
)
712+
713+
toolset = ApplicationIntegrationToolset(
714+
project,
715+
location,
716+
connection=connection_name,
717+
actions=actions_list,
718+
auth_scheme=oauth2_scheme,
719+
auth_credential=raw_auth_credential,
720+
)
721+
722+
exchanged_auth_credential = AuthCredential(
723+
auth_type=AuthCredentialTypes.OAUTH2,
724+
oauth2=OAuth2Auth(
725+
client_id="test-client-id",
726+
client_secret="test-client-secret",
727+
access_token="exchanged-access-token",
728+
),
729+
)
730+
toolset._auth_config.exchanged_auth_credential = exchanged_auth_credential
731+
732+
original_tool = toolset._tools[0]
733+
tools = await toolset.get_tools()
734+
735+
assert len(tools) == 1
736+
assert tools[0] is not original_tool
737+
assert tools[0]._auth_credential == exchanged_auth_credential
738+
assert original_tool._auth_credential == raw_auth_credential

0 commit comments

Comments
 (0)