Skip to content

Commit 61c42db

Browse files
committed
Fix: Support impersonated credential (#2001)
1 parent 6419a2a commit 61c42db

File tree

10 files changed

+238
-8
lines changed

10 files changed

+238
-8
lines changed

src/google/adk/tools/apihub_tool/clients/apihub_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
from abc import ABC
1618
from abc import abstractmethod
1719
import base64
@@ -324,7 +326,9 @@ def _get_access_token(self) -> str:
324326
raise ValueError(f"Invalid service account JSON: {e}") from e
325327
else:
326328
try:
327-
credentials, _ = default_service_credential()
329+
credentials, _ = default_service_credential(
330+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
331+
)
328332
except:
329333
credentials = None
330334

src/google/adk/tools/apihub_tool/clients/secret_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import json
1618
from typing import Optional
1719

@@ -73,7 +75,9 @@ def __init__(
7375
credentials.refresh(request)
7476
else:
7577
try:
76-
credentials, _ = default_service_credential()
78+
credentials, _ = default_service_credential(
79+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
80+
)
7781
except Exception as e:
7882
raise ValueError(
7983
"'service_account_json' or 'auth_token' are both missing, and"

src/google/adk/tools/application_integration_tool/clients/connections_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import json
1618
import time
1719
from typing import Any
@@ -810,7 +812,9 @@ def _get_access_token(self) -> str:
810812
)
811813
else:
812814
try:
813-
credentials, _ = default_service_credential()
815+
credentials, _ = default_service_credential(
816+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
817+
)
814818
except:
815819
credentials = None
816820

src/google/adk/tools/application_integration_tool/clients/integration_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import json
1618
from typing import List
1719
from typing import Optional
@@ -241,7 +243,9 @@ def _get_access_token(self) -> str:
241243
)
242244
else:
243245
try:
244-
credentials, _ = default_service_credential()
246+
credentials, _ = default_service_credential(
247+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
248+
)
245249
except:
246250
credentials = None
247251

src/google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Credential fetcher for Google Service Account."""
1616

17+
from __future__ import annotations
18+
1719
from typing import Optional
1820

1921
import google.auth
@@ -72,7 +74,9 @@ def exchange_credential(
7274

7375
try:
7476
if auth_credential.service_account.use_default_credential:
75-
credentials, _ = google.auth.default()
77+
credentials, _ = google.auth.default(
78+
scopes=["https://www.googleapis.com/auth/cloud-platform"],
79+
)
7680
else:
7781
config = auth_credential.service_account
7882
credentials = service_account.Credentials.from_service_account_info(

tests/unittests/tools/apihub_tool/clients/test_apihub_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ def test_get_access_token_use_default_credential(
297297
client = APIHubClient()
298298
token = client._get_access_token()
299299
assert token == "default_token"
300+
# Verify default_service_credential is called with the correct scopes parameter
301+
mock_default_service_credential.assert_called_once_with(
302+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
303+
)
300304
mock_credential.refresh.assert_called_once()
301305
assert client.credential_cache == mock_credential
302306

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Copyright 2025 Google LLC
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+
15+
"""Unit tests for the SecretManagerClient."""
16+
17+
import json
18+
from unittest.mock import MagicMock
19+
from unittest.mock import patch
20+
21+
from google.adk.tools.apihub_tool.clients.secret_client import SecretManagerClient
22+
import pytest
23+
24+
import google
25+
26+
27+
class TestSecretManagerClient:
28+
"""Tests for the SecretManagerClient class."""
29+
30+
@patch("google.cloud.secretmanager.SecretManagerServiceClient")
31+
@patch(
32+
"google.adk.tools.apihub_tool.clients.secret_client.default_service_credential"
33+
)
34+
def test_init_with_default_credentials(
35+
self, mock_default_service_credential, mock_secret_manager_client
36+
):
37+
"""Test initialization with default credentials."""
38+
# Setup
39+
mock_credentials = MagicMock()
40+
mock_default_service_credential.return_value = (
41+
mock_credentials,
42+
"test-project",
43+
)
44+
45+
# Execute
46+
client = SecretManagerClient()
47+
48+
# Verify
49+
mock_default_service_credential.assert_called_once_with(
50+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
51+
)
52+
mock_secret_manager_client.assert_called_once_with(
53+
credentials=mock_credentials
54+
)
55+
assert client._credentials == mock_credentials
56+
assert client._client == mock_secret_manager_client.return_value
57+
58+
@patch("google.cloud.secretmanager.SecretManagerServiceClient")
59+
@patch("google.oauth2.service_account.Credentials.from_service_account_info")
60+
def test_init_with_service_account_json(
61+
self, mock_from_service_account_info, mock_secret_manager_client
62+
):
63+
"""Test initialization with service account JSON."""
64+
# Setup
65+
mock_credentials = MagicMock()
66+
mock_from_service_account_info.return_value = mock_credentials
67+
service_account_json = json.dumps({
68+
"type": "service_account",
69+
"project_id": "test-project",
70+
"private_key_id": "key-id",
71+
"private_key": "private-key",
72+
"client_email": "test@example.com",
73+
})
74+
75+
# Execute
76+
client = SecretManagerClient(service_account_json=service_account_json)
77+
78+
# Verify
79+
mock_from_service_account_info.assert_called_once_with(
80+
json.loads(service_account_json)
81+
)
82+
mock_secret_manager_client.assert_called_once_with(
83+
credentials=mock_credentials
84+
)
85+
assert client._credentials == mock_credentials
86+
assert client._client == mock_secret_manager_client.return_value
87+
88+
@patch("google.cloud.secretmanager.SecretManagerServiceClient")
89+
def test_init_with_auth_token(self, mock_secret_manager_client):
90+
"""Test initialization with auth token."""
91+
# Setup
92+
auth_token = "test-token"
93+
mock_credentials = MagicMock()
94+
95+
# Mock the entire credentials creation process
96+
with (
97+
patch("google.auth.credentials.Credentials") as mock_credentials_class,
98+
patch("google.auth.transport.requests.Request") as mock_request,
99+
):
100+
# Configure the mock to return our mock_credentials when instantiated
101+
mock_credentials_class.return_value = mock_credentials
102+
103+
# Execute
104+
client = SecretManagerClient(auth_token=auth_token)
105+
106+
# Verify
107+
mock_credentials.refresh.assert_called_once()
108+
mock_secret_manager_client.assert_called_once_with(
109+
credentials=mock_credentials
110+
)
111+
assert client._credentials == mock_credentials
112+
assert client._client == mock_secret_manager_client.return_value
113+
114+
@patch(
115+
"google.adk.tools.apihub_tool.clients.secret_client.default_service_credential"
116+
)
117+
def test_init_with_default_credentials_error(
118+
self, mock_default_service_credential
119+
):
120+
"""Test initialization with default credentials that fails."""
121+
# Setup
122+
mock_default_service_credential.side_effect = Exception("Auth error")
123+
124+
# Execute and verify
125+
with pytest.raises(
126+
ValueError,
127+
match="error occurred while trying to use default credentials",
128+
):
129+
SecretManagerClient()
130+
131+
def test_init_with_invalid_service_account_json(self):
132+
"""Test initialization with invalid service account JSON."""
133+
# Execute and verify
134+
with pytest.raises(ValueError, match="Invalid service account JSON"):
135+
SecretManagerClient(service_account_json="invalid-json")
136+
137+
@patch("google.cloud.secretmanager.SecretManagerServiceClient")
138+
@patch(
139+
"google.adk.tools.apihub_tool.clients.secret_client.default_service_credential"
140+
)
141+
def test_get_secret(
142+
self, mock_default_service_credential, mock_secret_manager_client
143+
):
144+
"""Test getting a secret."""
145+
# Setup
146+
mock_credentials = MagicMock()
147+
mock_default_service_credential.return_value = (
148+
mock_credentials,
149+
"test-project",
150+
)
151+
152+
mock_client = MagicMock()
153+
mock_secret_manager_client.return_value = mock_client
154+
mock_response = MagicMock()
155+
mock_response.payload.data.decode.return_value = "secret-value"
156+
mock_client.access_secret_version.return_value = mock_response
157+
158+
# Execute - use default credentials instead of auth_token
159+
client = SecretManagerClient()
160+
result = client.get_secret(
161+
"projects/test-project/secrets/test-secret/versions/latest"
162+
)
163+
164+
# Verify
165+
assert result == "secret-value"
166+
mock_client.access_secret_version.assert_called_once_with(
167+
name="projects/test-project/secrets/test-secret/versions/latest"
168+
)
169+
mock_response.payload.data.decode.assert_called_once_with("UTF-8")
170+
171+
@patch("google.cloud.secretmanager.SecretManagerServiceClient")
172+
@patch(
173+
"google.adk.tools.apihub_tool.clients.secret_client.default_service_credential"
174+
)
175+
def test_get_secret_error(
176+
self, mock_default_service_credential, mock_secret_manager_client
177+
):
178+
"""Test getting a secret that fails."""
179+
# Setup
180+
mock_credentials = MagicMock()
181+
mock_default_service_credential.return_value = (
182+
mock_credentials,
183+
"test-project",
184+
)
185+
186+
mock_client = MagicMock()
187+
mock_secret_manager_client.return_value = mock_client
188+
mock_client.access_secret_version.side_effect = Exception("Secret error")
189+
190+
# Execute and verify - use default credentials instead of auth_token
191+
client = SecretManagerClient()
192+
with pytest.raises(Exception, match="Secret error"):
193+
client.get_secret(
194+
"projects/test-project/secrets/test-secret/versions/latest"
195+
)

tests/unittests/tools/application_integration_tool/clients/test_connections_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,11 +604,15 @@ def test_get_access_token_with_default_credentials(
604604
mock.patch(
605605
"google.adk.tools.application_integration_tool.clients.connections_client.default_service_credential",
606606
return_value=(mock_credentials, "test_project_id"),
607-
),
607+
) as mock_default_service_credential,
608608
mock.patch.object(mock_credentials, "refresh", return_value=None),
609609
):
610610
token = client._get_access_token()
611611
assert token == "test_token"
612+
# Verify default_service_credential is called with the correct scopes parameter
613+
mock_default_service_credential.assert_called_once_with(
614+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
615+
)
612616

613617
def test_get_access_token_no_valid_credentials(
614618
self, project, location, connection_name

tests/unittests/tools/application_integration_tool/clients/test_integration_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ def test_get_access_token_with_default_credentials(
537537
mock.patch(
538538
"google.adk.tools.application_integration_tool.clients.integration_client.default_service_credential",
539539
return_value=(mock_credentials, "test_project_id"),
540-
),
540+
) as mock_default_service_credential,
541541
mock.patch.object(mock_credentials, "refresh", return_value=None),
542542
):
543543
client = IntegrationClient(
@@ -552,6 +552,10 @@ def test_get_access_token_with_default_credentials(
552552
)
553553
token = client._get_access_token()
554554
assert token == "test_token"
555+
# Verify default_service_credential is called with the correct scopes parameter
556+
mock_default_service_credential.assert_called_once_with(
557+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
558+
)
555559

556560
def test_get_access_token_no_valid_credentials(
557561
self, project, location, integration_name, triggers, connection_name

tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_service_account_exchanger.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ def test_exchange_credential_use_default_credential_success(
125125
assert result.auth_type == AuthCredentialTypes.HTTP
126126
assert result.http.scheme == "bearer"
127127
assert result.http.credentials.token == "mock_access_token"
128-
mock_google_auth_default.assert_called_once()
128+
# Verify google.auth.default is called with the correct scopes parameter
129+
mock_google_auth_default.assert_called_once_with(
130+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
131+
)
129132
mock_credentials.refresh.assert_called_once()
130133

131134

0 commit comments

Comments
 (0)