From 4c3e4c2ca3fa15fe5a7062b15f4a73052bc3cdfe Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 12 May 2025 13:58:35 +0530 Subject: [PATCH 01/14] feat: add toolbox_auth_methods module --- .../src/toolbox_core/toolbox_auth_methods.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py diff --git a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py b/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py new file mode 100644 index 00000000..b44c4fe9 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py @@ -0,0 +1,20 @@ +from functools import partial +from google.auth._default_async import default_async +from google.auth.transport import _aiohttp_requests +import google.auth +from google.auth.transport.requests import AuthorizedSession, Request +from google.auth._credentials_async import Credentials + + +async def aget_google_id_token(): + creds, _ = default_async() + await creds.refresh(_aiohttp_requests.Request()) + creds.before_request = partial(Credentials.before_request, creds) + return creds.id_token + +def get_google_id_token(): + credentials, _ = google.auth.default() + session = AuthorizedSession(credentials) + request = Request(session) + credentials.refresh(request) + return credentials.id_token \ No newline at end of file From 874f2f173c72cfeb672331f9e73ae2bb0b90e252 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 12 May 2025 14:02:18 +0530 Subject: [PATCH 02/14] cleanup --- .../src/toolbox_core/toolbox_auth_methods.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py b/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py index b44c4fe9..f048997c 100644 --- a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py +++ b/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py @@ -1,20 +1,24 @@ from functools import partial + +import google.auth +from google.auth._credentials_async import Credentials from google.auth._default_async import default_async from google.auth.transport import _aiohttp_requests -import google.auth -from google.auth.transport.requests import AuthorizedSession, Request -from google.auth._credentials_async import Credentials +from google.auth.transport.requests import AuthorizedSession, Request async def aget_google_id_token(): creds, _ = default_async() await creds.refresh(_aiohttp_requests.Request()) creds.before_request = partial(Credentials.before_request, creds) - return creds.id_token + token = creds.id_token + return f"Bearer {token}" + def get_google_id_token(): credentials, _ = google.auth.default() - session = AuthorizedSession(credentials) - request = Request(session) - credentials.refresh(request) - return credentials.id_token \ No newline at end of file + session = AuthorizedSession(credentials) + request = Request(session) + credentials.refresh(request) + token = credentials.id_token + return f"Bearer {token}" From 308764a2fbf0ea312c23a5bc3d4b7b6763df5411 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 12 May 2025 14:27:50 +0530 Subject: [PATCH 03/14] feat: Add toolbox auth methods module --- .../src/toolbox_core/toolbox_auth_methods.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py b/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py index f048997c..633a200f 100644 --- a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py +++ b/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py @@ -1,3 +1,31 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The tokens obtained by these functions are formatted as "Bearer" tokens +# and are intended to be passed in the "Authorization" header of HTTP requests. +# +# Example User Experience: +# from toolbox_core import toolbox_auth_methods +# +# auth_token_provider = toolbox_auth_methods.aget_google_id_token +# toolbox = ToolboxClient( +# URL, +# client_headers={"Authorization": auth_token_provider}, +# ) +# tools = await toolbox.load_toolset() + + from functools import partial import google.auth @@ -8,6 +36,16 @@ async def aget_google_id_token(): + """ + Asynchronously fetches a Google ID token. + + The token is formatted as a 'Bearer' token string and is suitable for use + in an HTTP Authorization header. This function uses Application Default + Credentials. + + Returns: + A string in the format "Bearer ". + """ creds, _ = default_async() await creds.refresh(_aiohttp_requests.Request()) creds.before_request = partial(Credentials.before_request, creds) @@ -16,6 +54,16 @@ async def aget_google_id_token(): def get_google_id_token(): + """ + Synchronously fetches a Google ID token. + + The token is formatted as a 'Bearer' token string and is suitable for use + in an HTTP Authorization header. This function uses Application Default + Credentials. + + Returns: + A string in the format "Bearer ". + """ credentials, _ = google.auth.default() session = AuthorizedSession(credentials) request = Request(session) From ee0aa6163e4ce2ad1255bed30d10aa8441bcfbeb Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:08:47 +0530 Subject: [PATCH 04/14] add tests --- packages/toolbox-core/pyproject.toml | 1 + ...oolbox_auth_methods.py => auth_methods.py} | 4 +- .../toolbox-core/tests/test_auth_methods.py | 197 ++++++++++++++++++ 3 files changed, 200 insertions(+), 2 deletions(-) rename packages/toolbox-core/src/toolbox_core/{toolbox_auth_methods.py => auth_methods.py} (95%) create mode 100644 packages/toolbox-core/tests/test_auth_methods.py diff --git a/packages/toolbox-core/pyproject.toml b/packages/toolbox-core/pyproject.toml index 39dad445..6a918b4e 100644 --- a/packages/toolbox-core/pyproject.toml +++ b/packages/toolbox-core/pyproject.toml @@ -48,6 +48,7 @@ test = [ "pytest-aioresponses==0.3.0", "pytest-asyncio==0.26.0", "pytest-cov==6.1.1", + "pytest-mock==3.14.0", "google-cloud-secret-manager==2.23.3", "google-cloud-storage==3.1.0", ] diff --git a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py b/packages/toolbox-core/src/toolbox_core/auth_methods.py similarity index 95% rename from packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py rename to packages/toolbox-core/src/toolbox_core/auth_methods.py index 633a200f..83f15774 100644 --- a/packages/toolbox-core/src/toolbox_core/toolbox_auth_methods.py +++ b/packages/toolbox-core/src/toolbox_core/auth_methods.py @@ -16,9 +16,9 @@ # and are intended to be passed in the "Authorization" header of HTTP requests. # # Example User Experience: -# from toolbox_core import toolbox_auth_methods +# from toolbox_core import auth_methods # -# auth_token_provider = toolbox_auth_methods.aget_google_id_token +# auth_token_provider = auth_methods.aget_google_id_token # toolbox = ToolboxClient( # URL, # client_headers={"Authorization": auth_token_provider}, diff --git a/packages/toolbox-core/tests/test_auth_methods.py b/packages/toolbox-core/tests/test_auth_methods.py new file mode 100644 index 00000000..ad7b9d46 --- /dev/null +++ b/packages/toolbox-core/tests/test_auth_methods.py @@ -0,0 +1,197 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import google.auth.exceptions +import pytest +from unittest.mock import patch, AsyncMock, MagicMock + +from toolbox_core import auth_methods + +@pytest.mark.asyncio +@patch("toolbox_core.auth_methods.partial") +@patch("toolbox_core.auth_methods._aiohttp_requests.Request") +@patch("toolbox_core.auth_methods.Credentials") +@patch("toolbox_core.auth_methods.default_async") +async def test_aget_google_id_token_success( + mock_default_async, + mock_credentials_class, + mock_aiohttp_request_class, + mock_partial, +): + """ + Test aget_google_id_token successfully retrieves and formats a token using pytest. + """ + # Setup mock for default_async() -> (creds, project_id) + mock_creds_instance = AsyncMock() + mock_creds_instance.id_token = "test_async_id_token_123" + mock_default_async.return_value = (mock_creds_instance, "test-project") + + # Setup mock for _aiohttp_requests.Request() + mock_aio_request_instance = MagicMock() + mock_aiohttp_request_class.return_value = mock_aio_request_instance + + # Setup mock for Credentials.before_request (class attribute used in partial) + mock_unbound_before_request = MagicMock() + mock_credentials_class.before_request = mock_unbound_before_request + + # Setup mock for partial() + mock_partial_object = MagicMock() + mock_partial.return_value = mock_partial_object + + token = await auth_methods.aget_google_id_token() + + mock_default_async.assert_called_once_with() + mock_aiohttp_request_class.assert_called_once_with() + mock_creds_instance.refresh.assert_called_once_with(mock_aio_request_instance) + + mock_partial.assert_called_once_with( + mock_unbound_before_request, mock_creds_instance + ) + assert mock_creds_instance.before_request == mock_partial_object + + assert token == "Bearer test_async_id_token_123" + + +@pytest.mark.asyncio +@patch("toolbox_core.auth_methods.default_async") +async def test_aget_google_id_token_default_credentials_error(mock_default_async): + """ + Test aget_google_id_token when default_async raises DefaultCredentialsError. + """ + mock_default_async.side_effect = google.auth.exceptions.DefaultCredentialsError( + "ADC not found" + ) + + with pytest.raises( + google.auth.exceptions.DefaultCredentialsError, match="ADC not found" + ): + await auth_methods.aget_google_id_token() + + mock_default_async.assert_called_once_with() + + +@pytest.mark.asyncio +@patch("toolbox_core.auth_methods._aiohttp_requests.Request") +@patch("toolbox_core.auth_methods.default_async") +async def test_aget_google_id_token_refresh_error( + mock_default_async, + mock_aiohttp_request_class, +): + """ + Test aget_google_id_token when creds.refresh raises RefreshError. + The `partial` call should not happen if refresh fails. + """ + mock_creds_instance = AsyncMock() + mock_creds_instance.refresh.side_effect = google.auth.exceptions.RefreshError( + "Token refresh failed" + ) + mock_default_async.return_value = (mock_creds_instance, "test-project") + + mock_aio_request_instance = MagicMock() + mock_aiohttp_request_class.return_value = mock_aio_request_instance + + with pytest.raises( + google.auth.exceptions.RefreshError, match="Token refresh failed" + ): + await auth_methods.aget_google_id_token() + + mock_default_async.assert_called_once_with() + mock_aiohttp_request_class.assert_called_once_with() + mock_creds_instance.refresh.assert_called_once_with(mock_aio_request_instance) + + +# --- Synchronous Tests --- + +@patch("toolbox_core.auth_methods.Request") +@patch("toolbox_core.auth_methods.AuthorizedSession") +@patch("toolbox_core.auth_methods.google.auth.default") +def test_get_google_id_token_success( + mock_google_auth_default, + mock_authorized_session_class, + mock_request_class, +): + """ + Test get_google_id_token successfully retrieves and formats a token using pytest. + """ + # Setup mock for google.auth.default() -> (credentials, project_id) + mock_creds_instance = MagicMock() + mock_creds_instance.id_token = "test_sync_id_token_456" + mock_google_auth_default.return_value = (mock_creds_instance, "test-project") + + # Setup mock for AuthorizedSession() + mock_session_instance = MagicMock() + mock_authorized_session_class.return_value = mock_session_instance + + # Setup mock for Request() + mock_request_instance = MagicMock() + mock_request_class.return_value = mock_request_instance + + token = auth_methods.get_google_id_token() + + mock_google_auth_default.assert_called_once_with() + mock_authorized_session_class.assert_called_once_with(mock_creds_instance) + mock_request_class.assert_called_once_with(mock_session_instance) + mock_creds_instance.refresh.assert_called_once_with(mock_request_instance) + assert token == "Bearer test_sync_id_token_456" + + +@patch("toolbox_core.auth_methods.google.auth.default") +def test_get_google_id_token_default_credentials_error(mock_google_auth_default): + """ + Test get_google_id_token when google.auth.default raises DefaultCredentialsError. + """ + mock_google_auth_default.side_effect = ( + google.auth.exceptions.DefaultCredentialsError("Sync ADC not found") + ) + + with pytest.raises( + google.auth.exceptions.DefaultCredentialsError, match="Sync ADC not found" + ): + auth_methods.get_google_id_token() + + mock_google_auth_default.assert_called_once_with() + + +@patch("toolbox_core.auth_methods.Request") +@patch("toolbox_core.auth_methods.AuthorizedSession") +@patch("toolbox_core.auth_methods.google.auth.default") +def test_get_google_id_token_refresh_error( + mock_google_auth_default, + mock_authorized_session_class, + mock_request_class, +): + """ + Test get_google_id_token when credentials.refresh raises RefreshError. + """ + mock_creds_instance = MagicMock() + mock_creds_instance.refresh.side_effect = google.auth.exceptions.RefreshError( + "Sync token refresh failed" + ) + mock_google_auth_default.return_value = (mock_creds_instance, "test-project") + + mock_session_instance = MagicMock() + mock_authorized_session_class.return_value = mock_session_instance + + mock_request_instance = MagicMock() + mock_request_class.return_value = mock_request_instance + + with pytest.raises( + google.auth.exceptions.RefreshError, match="Sync token refresh failed" + ): + auth_methods.get_google_id_token() + + mock_google_auth_default.assert_called_once_with() + mock_authorized_session_class.assert_called_once_with(mock_creds_instance) + mock_request_class.assert_called_once_with(mock_session_instance) + mock_creds_instance.refresh.assert_called_once_with(mock_request_instance) \ No newline at end of file From 441ea32c9ca50d2f9c9f148c343fcd5c505c90f7 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:11:17 +0530 Subject: [PATCH 05/14] clean tests --- .../toolbox-core/tests/test_auth_methods.py | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/packages/toolbox-core/tests/test_auth_methods.py b/packages/toolbox-core/tests/test_auth_methods.py index ad7b9d46..0f1e6ba1 100644 --- a/packages/toolbox-core/tests/test_auth_methods.py +++ b/packages/toolbox-core/tests/test_auth_methods.py @@ -12,12 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import google.auth.exceptions -import pytest from unittest.mock import patch, AsyncMock, MagicMock +import pytest +import google.auth.exceptions + from toolbox_core import auth_methods +# Constants for test values +MOCK_ASYNC_ID_TOKEN = "test_async_id_token_123" +MOCK_SYNC_ID_TOKEN = "test_sync_id_token_456" +MOCK_PROJECT_ID = "test-project" + +# Error Messages +ADC_NOT_FOUND_MSG = "ADC not found" +TOKEN_REFRESH_FAILED_MSG = "Token refresh failed" +SYNC_ADC_NOT_FOUND_MSG = "Sync ADC not found" +SYNC_TOKEN_REFRESH_FAILED_MSG = "Sync token refresh failed" + + @pytest.mark.asyncio @patch("toolbox_core.auth_methods.partial") @patch("toolbox_core.auth_methods._aiohttp_requests.Request") @@ -30,22 +43,18 @@ async def test_aget_google_id_token_success( mock_partial, ): """ - Test aget_google_id_token successfully retrieves and formats a token using pytest. + Test aget_google_id_token successfully retrieves and formats a token. """ - # Setup mock for default_async() -> (creds, project_id) mock_creds_instance = AsyncMock() - mock_creds_instance.id_token = "test_async_id_token_123" - mock_default_async.return_value = (mock_creds_instance, "test-project") + mock_creds_instance.id_token = MOCK_ASYNC_ID_TOKEN + mock_default_async.return_value = (mock_creds_instance, MOCK_PROJECT_ID) - # Setup mock for _aiohttp_requests.Request() mock_aio_request_instance = MagicMock() mock_aiohttp_request_class.return_value = mock_aio_request_instance - # Setup mock for Credentials.before_request (class attribute used in partial) mock_unbound_before_request = MagicMock() mock_credentials_class.before_request = mock_unbound_before_request - # Setup mock for partial() mock_partial_object = MagicMock() mock_partial.return_value = mock_partial_object @@ -59,22 +68,21 @@ async def test_aget_google_id_token_success( mock_unbound_before_request, mock_creds_instance ) assert mock_creds_instance.before_request == mock_partial_object - - assert token == "Bearer test_async_id_token_123" + assert token == f"Bearer {MOCK_ASYNC_ID_TOKEN}" @pytest.mark.asyncio @patch("toolbox_core.auth_methods.default_async") async def test_aget_google_id_token_default_credentials_error(mock_default_async): """ - Test aget_google_id_token when default_async raises DefaultCredentialsError. + Test aget_google_id_token handles DefaultCredentialsError. """ mock_default_async.side_effect = google.auth.exceptions.DefaultCredentialsError( - "ADC not found" + ADC_NOT_FOUND_MSG ) with pytest.raises( - google.auth.exceptions.DefaultCredentialsError, match="ADC not found" + google.auth.exceptions.DefaultCredentialsError, match=ADC_NOT_FOUND_MSG ): await auth_methods.aget_google_id_token() @@ -89,20 +97,19 @@ async def test_aget_google_id_token_refresh_error( mock_aiohttp_request_class, ): """ - Test aget_google_id_token when creds.refresh raises RefreshError. - The `partial` call should not happen if refresh fails. + Test aget_google_id_token handles RefreshError. """ mock_creds_instance = AsyncMock() mock_creds_instance.refresh.side_effect = google.auth.exceptions.RefreshError( - "Token refresh failed" + TOKEN_REFRESH_FAILED_MSG ) - mock_default_async.return_value = (mock_creds_instance, "test-project") + mock_default_async.return_value = (mock_creds_instance, MOCK_PROJECT_ID) mock_aio_request_instance = MagicMock() mock_aiohttp_request_class.return_value = mock_aio_request_instance with pytest.raises( - google.auth.exceptions.RefreshError, match="Token refresh failed" + google.auth.exceptions.RefreshError, match=TOKEN_REFRESH_FAILED_MSG ): await auth_methods.aget_google_id_token() @@ -122,18 +129,15 @@ def test_get_google_id_token_success( mock_request_class, ): """ - Test get_google_id_token successfully retrieves and formats a token using pytest. + Test get_google_id_token successfully retrieves and formats a token. """ - # Setup mock for google.auth.default() -> (credentials, project_id) mock_creds_instance = MagicMock() - mock_creds_instance.id_token = "test_sync_id_token_456" - mock_google_auth_default.return_value = (mock_creds_instance, "test-project") + mock_creds_instance.id_token = MOCK_SYNC_ID_TOKEN + mock_google_auth_default.return_value = (mock_creds_instance, MOCK_PROJECT_ID) - # Setup mock for AuthorizedSession() mock_session_instance = MagicMock() mock_authorized_session_class.return_value = mock_session_instance - # Setup mock for Request() mock_request_instance = MagicMock() mock_request_class.return_value = mock_request_instance @@ -143,20 +147,20 @@ def test_get_google_id_token_success( mock_authorized_session_class.assert_called_once_with(mock_creds_instance) mock_request_class.assert_called_once_with(mock_session_instance) mock_creds_instance.refresh.assert_called_once_with(mock_request_instance) - assert token == "Bearer test_sync_id_token_456" + assert token == f"Bearer {MOCK_SYNC_ID_TOKEN}" @patch("toolbox_core.auth_methods.google.auth.default") def test_get_google_id_token_default_credentials_error(mock_google_auth_default): """ - Test get_google_id_token when google.auth.default raises DefaultCredentialsError. + Test get_google_id_token handles DefaultCredentialsError. """ mock_google_auth_default.side_effect = ( - google.auth.exceptions.DefaultCredentialsError("Sync ADC not found") + google.auth.exceptions.DefaultCredentialsError(SYNC_ADC_NOT_FOUND_MSG) ) with pytest.raises( - google.auth.exceptions.DefaultCredentialsError, match="Sync ADC not found" + google.auth.exceptions.DefaultCredentialsError, match=SYNC_ADC_NOT_FOUND_MSG ): auth_methods.get_google_id_token() @@ -172,13 +176,13 @@ def test_get_google_id_token_refresh_error( mock_request_class, ): """ - Test get_google_id_token when credentials.refresh raises RefreshError. + Test get_google_id_token handles RefreshError. """ mock_creds_instance = MagicMock() mock_creds_instance.refresh.side_effect = google.auth.exceptions.RefreshError( - "Sync token refresh failed" + SYNC_TOKEN_REFRESH_FAILED_MSG ) - mock_google_auth_default.return_value = (mock_creds_instance, "test-project") + mock_google_auth_default.return_value = (mock_creds_instance, MOCK_PROJECT_ID) mock_session_instance = MagicMock() mock_authorized_session_class.return_value = mock_session_instance @@ -187,7 +191,7 @@ def test_get_google_id_token_refresh_error( mock_request_class.return_value = mock_request_instance with pytest.raises( - google.auth.exceptions.RefreshError, match="Sync token refresh failed" + google.auth.exceptions.RefreshError, match=SYNC_TOKEN_REFRESH_FAILED_MSG ): auth_methods.get_google_id_token() From fe933854e3cf2015f594c76c2829d6ac9d86513a Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:11:40 +0530 Subject: [PATCH 06/14] lint --- packages/toolbox-core/tests/test_auth_methods.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/tests/test_auth_methods.py b/packages/toolbox-core/tests/test_auth_methods.py index 0f1e6ba1..e316726b 100644 --- a/packages/toolbox-core/tests/test_auth_methods.py +++ b/packages/toolbox-core/tests/test_auth_methods.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch, AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch -import pytest import google.auth.exceptions +import pytest from toolbox_core import auth_methods @@ -120,6 +120,7 @@ async def test_aget_google_id_token_refresh_error( # --- Synchronous Tests --- + @patch("toolbox_core.auth_methods.Request") @patch("toolbox_core.auth_methods.AuthorizedSession") @patch("toolbox_core.auth_methods.google.auth.default") @@ -198,4 +199,4 @@ def test_get_google_id_token_refresh_error( mock_google_auth_default.assert_called_once_with() mock_authorized_session_class.assert_called_once_with(mock_creds_instance) mock_request_class.assert_called_once_with(mock_session_instance) - mock_creds_instance.refresh.assert_called_once_with(mock_request_instance) \ No newline at end of file + mock_creds_instance.refresh.assert_called_once_with(mock_request_instance) From 3a354ced4b656bae930736fdebfff9228ed3ba66 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:43:24 +0530 Subject: [PATCH 07/14] add docs --- packages/toolbox-core/README.md | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 094b30d4..b9e349cc 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -221,6 +221,86 @@ workflow.add_edge("tools", "agent") app = workflow.compile() ``` +## Authenticating to the Toolbox Server + +This section describes how to authenticate the ToolboxClient itself when +connecting to a Toolbox server instance that requires authentication. This is +crucial for securing your Toolbox server endpoint, especially when deployed on +platforms like Cloud Run, GKE, or any environment where unauthenticated access is restricted. + +This client-to-server authentication ensures that the Toolbox server can verify the identity of the client making the request before any tool is loaded or called. It is different from [Authenticating Tools](#authenticating-tools), which deals with providing credentials for specific tools within an already connected Toolbox session. + +### When is Client-to-Server Authentication Needed? +You'll need this type of authentication if your Toolbox server is configured to deny unauthenticated requests. For example: + +- Your Toolbox server is deployed on Cloud Run and configured to "Require authentication." +- Your server is behind an Identity-Aware Proxy (IAP) or a similar authentication layer. +- You have custom authentication middleware on your self-hosted Toolbox server. + +Without proper client authentication in these scenarios, attempts to connect or +make calls (like `load_tool`) will likely fail with `Unauthorized` errors. + +### How it works + +The `ToolboxClient` (and `ToolboxSyncClient`) allows you to specify functions (or coroutines for the async client) that dynamically generate HTTP headers for every request sent to the Toolbox server. The most common use case is to add an Authorization header with a bearer token (e.g., a Google ID token). + +These header-generating functions are called just before each request, ensuring +that fresh credentials or header values can be used. + +### Configuration + +You can configure these dynamic headers in two ways: + +1. **During Client Initialization** + + ```python + from toolbox_core import ToolboxClient + + client = ToolboxClient("toolbox-url", headers={"header1": header1_getter, "header2": header2_getter, ...}) + ``` + +1. **After Client Initialization** + + ```python + from toolbox_core import ToolboxClient + + client = ToolboxClient("toolbox-url") + client.add_headers({"header1": header1_getter, "header2": header2_getter, ...}) + ``` + +### Authenticating with Google Cloud Servers + +For Toolbox servers hosted on Google Cloud (e.g., Cloud Run) and requiring +`Google ID token` authentication, the helper module +[auth_methods](src/toolbox_core/auth_methods.py) provides utility functions. + +### Step by Step Guide for Cloud Run + +1. **Configure Permissions**: [Grant](https://cloud.google.com/run/docs/securing/managing-access#service-add-principals) the `roles/run.invoker` IAM role on the Cloud + Run service to the principal. This could be your `user account email` or a + `service account`. +2. **Configure Credentials** + - Local Development: Set up + [ADC](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment). + - Google Cloud Environments: When running within Google Cloud (e.g., Compute + Engine, GKE, another Cloud Run service, Cloud Functions), ADC is typically + configured automatically, using the environment's default service account. +3. **Connect to the Toolbox Server** + + ```python + from toolbox_core import auth_methods + + auth_token_provider = auth_methods.aget_google_id_token # can also use sync method + client = ToolboxClient( + URL, + client_headers={"Authorization": auth_token_provider}, + ) + tools = await client.load_toolset() + + # Now, you can use the client as usual. + ``` + + ## Authenticating Tools > [!WARNING] From d578b245b063f5f5927e9268f63c0d9abea57c91 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:44:42 +0530 Subject: [PATCH 08/14] fix toc --- packages/toolbox-core/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index b9e349cc..4f3e9301 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -25,6 +25,12 @@ involving Large Language Models (LLMs). - [Invoking Tools](#invoking-tools) - [Synchronous Usage](#synchronous-usage) - [Use with LangGraph](#use-with-langgraph) +- [Authenticating to the Toolbox Server](#authenticating-to-the-toolbox-server) + - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) + - [How it works](#how-it-works) + - [Configuration](#configuration) + - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) + - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) - [Authenticating Tools](#authenticating-tools) - [When is Authentication Needed?](#when-is-authentication-needed) - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) From 4ae8c421f0460ab5d2aed4226e4243b7d58f2759 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:46:42 +0530 Subject: [PATCH 09/14] lint --- packages/toolbox-core/README.md | 37 +++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 4f3e9301..331a2463 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -1,4 +1,5 @@ ![MCP Toolbox Logo](https://raw.githubusercontent.com/googleapis/genai-toolbox/main/logo.png) + # MCP Toolbox Core SDK [![PyPI version](https://badge.fury.io/py/toolbox-core.svg)](https://badge.fury.io/py/toolbox-core) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/toolbox-core)](https://pypi.org/project/toolbox-core/) [![Coverage Status](https://coveralls.io/repos/github/googleapis/genai-toolbox/badge.svg?branch=main)](https://coveralls.io/github/googleapis/genai-toolbox?branch=main) @@ -20,30 +21,30 @@ involving Large Language Models (LLMs). - [Quickstart](#quickstart) - [Usage](#usage) - [Loading Tools](#loading-tools) - - [Load a toolset](#load-a-toolset) - - [Load a single tool](#load-a-single-tool) + - [Load a toolset](#load-a-toolset) + - [Load a single tool](#load-a-single-tool) - [Invoking Tools](#invoking-tools) - [Synchronous Usage](#synchronous-usage) - [Use with LangGraph](#use-with-langgraph) - [Authenticating to the Toolbox Server](#authenticating-to-the-toolbox-server) - - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) - - [How it works](#how-it-works) - - [Configuration](#configuration) - - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) - - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) + - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) + - [How it works](#how-it-works) + - [Configuration](#configuration) + - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) + - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) - [Authenticating Tools](#authenticating-tools) - - [When is Authentication Needed?](#when-is-authentication-needed) - - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) - - [Step 1: Configure Tools in Toolbox Service](#step-1-configure-tools-in-toolbox-service) - - [Step 2: Configure SDK Client](#step-2-configure-sdk-client) - - [Provide an ID Token Retriever Function](#provide-an-id-token-retriever-function) - - [Option A: Add Authentication to a Loaded Tool](#option-a-add-authentication-to-a-loaded-tool) - - [Option B: Add Authentication While Loading Tools](#option-b-add-authentication-while-loading-tools) - - [Complete Authentication Example](#complete-authentication-example) + - [When is Authentication Needed?](#when-is-authentication-needed) + - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) + - [Step 1: Configure Tools in Toolbox Service](#step-1-configure-tools-in-toolbox-service) + - [Step 2: Configure SDK Client](#step-2-configure-sdk-client) + - [Provide an ID Token Retriever Function](#provide-an-id-token-retriever-function) + - [Option A: Add Authentication to a Loaded Tool](#option-a-add-authentication-to-a-loaded-tool) + - [Option B: Add Authentication While Loading Tools](#option-b-add-authentication-while-loading-tools) + - [Complete Authentication Example](#complete-authentication-example) - [Binding Parameter Values](#binding-parameter-values) - - [Why Bind Parameters?](#why-bind-parameters) - - [Option A: Binding Parameters to a Loaded Tool](#option-a-binding-parameters-to-a-loaded-tool) - - [Option B: Binding Parameters While Loading Tools](#option-b-binding-parameters-while-loading-tools) + - [Why Bind Parameters?](#why-bind-parameters) + - [Option A: Binding Parameters to a Loaded Tool](#option-a-binding-parameters-to-a-loaded-tool) + - [Option B: Binding Parameters While Loading Tools](#option-b-binding-parameters-while-loading-tools) - [Binding Dynamic Values](#binding-dynamic-values) - [Contributing](#contributing) - [License](#license) From 11731fb0c535af2550e33043ef266425cd564ecc Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:47:42 +0530 Subject: [PATCH 10/14] lint --- packages/toolbox-core/README.md | 72 +++++++++++++++++---------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 331a2463..37748757 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -17,35 +17,36 @@ involving Large Language Models (LLMs). -- [Installation](#installation) -- [Quickstart](#quickstart) -- [Usage](#usage) -- [Loading Tools](#loading-tools) - - [Load a toolset](#load-a-toolset) - - [Load a single tool](#load-a-single-tool) -- [Invoking Tools](#invoking-tools) -- [Synchronous Usage](#synchronous-usage) -- [Use with LangGraph](#use-with-langgraph) -- [Authenticating to the Toolbox Server](#authenticating-to-the-toolbox-server) - - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) - - [How it works](#how-it-works) - - [Configuration](#configuration) - - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) - - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) -- [Authenticating Tools](#authenticating-tools) - - [When is Authentication Needed?](#when-is-authentication-needed) - - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) - - [Step 1: Configure Tools in Toolbox Service](#step-1-configure-tools-in-toolbox-service) - - [Step 2: Configure SDK Client](#step-2-configure-sdk-client) - - [Provide an ID Token Retriever Function](#provide-an-id-token-retriever-function) - - [Option A: Add Authentication to a Loaded Tool](#option-a-add-authentication-to-a-loaded-tool) - - [Option B: Add Authentication While Loading Tools](#option-b-add-authentication-while-loading-tools) - - [Complete Authentication Example](#complete-authentication-example) -- [Binding Parameter Values](#binding-parameter-values) - - [Why Bind Parameters?](#why-bind-parameters) - - [Option A: Binding Parameters to a Loaded Tool](#option-a-binding-parameters-to-a-loaded-tool) - - [Option B: Binding Parameters While Loading Tools](#option-b-binding-parameters-while-loading-tools) - - [Binding Dynamic Values](#binding-dynamic-values) +- [MCP Toolbox Core SDK](#mcp-toolbox-core-sdk) + - [Installation](#installation) + - [Quickstart](#quickstart) + - [Usage](#usage) + - [Loading Tools](#loading-tools) + - [Load a toolset](#load-a-toolset) + - [Load a single tool](#load-a-single-tool) + - [Invoking Tools](#invoking-tools) + - [Synchronous Usage](#synchronous-usage) + - [Use with LangGraph](#use-with-langgraph) + - [Authenticating to the Toolbox Server](#authenticating-to-the-toolbox-server) + - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) + - [How it works](#how-it-works) + - [Configuration](#configuration) + - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) + - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) + - [Authenticating Tools](#authenticating-tools) + - [When is Authentication Needed?](#when-is-authentication-needed) + - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) + - [Step 1: Configure Tools in Toolbox Service](#step-1-configure-tools-in-toolbox-service) + - [Step 2: Configure SDK Client](#step-2-configure-sdk-client) + - [Provide an ID Token Retriever Function](#provide-an-id-token-retriever-function) + - [Option A: Add Authentication to a Loaded Tool](#option-a-add-authentication-to-a-loaded-tool) + - [Option B: Add Authentication While Loading Tools](#option-b-add-authentication-while-loading-tools) + - [Complete Authentication Example](#complete-authentication-example) + - [Binding Parameter Values](#binding-parameter-values) + - [Why Bind Parameters?](#why-bind-parameters) + - [Option A: Binding Parameters to a Loaded Tool](#option-a-binding-parameters-to-a-loaded-tool) + - [Option B: Binding Parameters While Loading Tools](#option-b-binding-parameters-while-loading-tools) + - [Binding Dynamic Values](#binding-dynamic-values) - [Contributing](#contributing) - [License](#license) - [Support](#support) @@ -59,7 +60,7 @@ pip install toolbox-core ``` > [!NOTE] -> * The primary `ToolboxClient` is asynchronous and requires using `await` for +> - The primary `ToolboxClient` is asynchronous and requires using `await` for > loading and invoking tools, as shown in most examples. > * Asynchronous code needs to run within an event loop (e.g., using > `asyncio.run()` or in an async framework). See the [Python `asyncio` @@ -72,6 +73,7 @@ pip install toolbox-core Here's a minimal example to get you started. Ensure your Toolbox service is running and accessible. + ```py import asyncio from toolbox_core import ToolboxClient @@ -323,15 +325,18 @@ ensuring only authorized users or applications can invoke them, especially when accessing sensitive data. ### When is Authentication Needed? + Authentication is configured per-tool within the Toolbox service itself. If a tool you intend to use is marked as requiring authentication in the service, you must configure the SDK client to provide the necessary credentials (currently Oauth2 tokens) when invoking that specific tool. ### Supported Authentication Mechanisms + The Toolbox service enables secure tool usage through **Authenticated Parameters**. For detailed information on how these mechanisms work within the Toolbox service and how to configure them, please refer to [Toolbox Service Documentation - Authenticated Parameters](https://googleapis.github.io/genai-toolbox/resources/tools/#authenticated-parameters) ### Step 1: Configure Tools in Toolbox Service + First, ensure the target tool(s) are configured correctly in the Toolbox service to require authentication. Refer to the [Toolbox Service Documentation - Authenticated @@ -434,9 +439,9 @@ fixed and will not be requested or modified by the LLM during tool use. ### Why Bind Parameters? -* **Protecting sensitive information:** API keys, secrets, etc. -* **Enforcing consistency:** Ensuring specific values for certain parameters. -* **Pre-filling known data:** Providing defaults or context. +- **Protecting sensitive information:** API keys, secrets, etc. +- **Enforcing consistency:** Ensuring specific values for certain parameters. +- **Pre-filling known data:** Providing defaults or context. > [!IMPORTANT] > The parameter names used for binding (e.g., `"api_key"`) must exactly match the @@ -464,7 +469,6 @@ bound_tool = tool.bind_params({"param": "value"}) Specify bound parameters directly when loading tools. This applies the binding only to the tools loaded in that specific call. - ```py bound_tool = await toolbox.load_tool("my-tool", bound_params={"param": "value"}) From 9d35164a49658dc4ff15933a48fd8ffc96e2904a Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:48:46 +0530 Subject: [PATCH 11/14] lint --- packages/toolbox-core/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 37748757..1486159c 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -60,13 +60,14 @@ pip install toolbox-core ``` > [!NOTE] +> > - The primary `ToolboxClient` is asynchronous and requires using `await` for > loading and invoking tools, as shown in most examples. -> * Asynchronous code needs to run within an event loop (e.g., using +> - Asynchronous code needs to run within an event loop (e.g., using > `asyncio.run()` or in an async framework). See the [Python `asyncio` > documentation](https://docs.python.org/3/library/asyncio-task.html) for more > details. -> * If you prefer synchronous execution, refer to the [Synchronous +> - If you prefer synchronous execution, refer to the [Synchronous > Usage](#synchronous-usage) section below. ## Quickstart From f196de99f3cc0ff7c0a89c95cc0d338a1e43d1b3 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:49:23 +0530 Subject: [PATCH 12/14] lint --- packages/toolbox-core/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index 1486159c..bc988b33 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -176,8 +176,6 @@ print(result) > often leading to better performance and resource utilization, especially in > applications handling concurrent requests. - - ## Use with LangGraph The Toolbox Core SDK integrates smoothly with frameworks like LangGraph, @@ -241,6 +239,7 @@ platforms like Cloud Run, GKE, or any environment where unauthenticated access This client-to-server authentication ensures that the Toolbox server can verify the identity of the client making the request before any tool is loaded or called. It is different from [Authenticating Tools](#authenticating-tools), which deals with providing credentials for specific tools within an already connected Toolbox session. ### When is Client-to-Server Authentication Needed? + You'll need this type of authentication if your Toolbox server is configured to deny unauthenticated requests. For example: - Your Toolbox server is deployed on Cloud Run and configured to "Require authentication." @@ -310,7 +309,6 @@ For Toolbox servers hosted on Google Cloud (e.g., Cloud Run) and requiring # Now, you can use the client as usual. ``` - ## Authenticating Tools > [!WARNING] From 69c9e2380a0f4fc5d4f38b086c2edc38ba8e8cd9 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 13 May 2025 11:52:13 +0530 Subject: [PATCH 13/14] lint --- packages/toolbox-core/README.md | 59 ++++++++++++++++----------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index bc988b33..f6a0518d 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -17,36 +17,35 @@ involving Large Language Models (LLMs). -- [MCP Toolbox Core SDK](#mcp-toolbox-core-sdk) - - [Installation](#installation) - - [Quickstart](#quickstart) - - [Usage](#usage) - - [Loading Tools](#loading-tools) - - [Load a toolset](#load-a-toolset) - - [Load a single tool](#load-a-single-tool) - - [Invoking Tools](#invoking-tools) - - [Synchronous Usage](#synchronous-usage) - - [Use with LangGraph](#use-with-langgraph) - - [Authenticating to the Toolbox Server](#authenticating-to-the-toolbox-server) - - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) - - [How it works](#how-it-works) - - [Configuration](#configuration) - - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) - - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) - - [Authenticating Tools](#authenticating-tools) - - [When is Authentication Needed?](#when-is-authentication-needed) - - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) - - [Step 1: Configure Tools in Toolbox Service](#step-1-configure-tools-in-toolbox-service) - - [Step 2: Configure SDK Client](#step-2-configure-sdk-client) - - [Provide an ID Token Retriever Function](#provide-an-id-token-retriever-function) - - [Option A: Add Authentication to a Loaded Tool](#option-a-add-authentication-to-a-loaded-tool) - - [Option B: Add Authentication While Loading Tools](#option-b-add-authentication-while-loading-tools) - - [Complete Authentication Example](#complete-authentication-example) - - [Binding Parameter Values](#binding-parameter-values) - - [Why Bind Parameters?](#why-bind-parameters) - - [Option A: Binding Parameters to a Loaded Tool](#option-a-binding-parameters-to-a-loaded-tool) - - [Option B: Binding Parameters While Loading Tools](#option-b-binding-parameters-while-loading-tools) - - [Binding Dynamic Values](#binding-dynamic-values) +- [Installation](#installation) +- [Quickstart](#quickstart) +- [Usage](#usage) +- [Loading Tools](#loading-tools) + - [Load a toolset](#load-a-toolset) + - [Load a single tool](#load-a-single-tool) +- [Invoking Tools](#invoking-tools) +- [Synchronous Usage](#synchronous-usage) +- [Use with LangGraph](#use-with-langgraph) +- [Authenticating to the Toolbox Server](#authenticating-to-the-toolbox-server) + - [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed) + - [How it works](#how-it-works) + - [Configuration](#configuration) + - [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers) + - [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run) +- [Authenticating Tools](#authenticating-tools) + - [When is Authentication Needed?](#when-is-authentication-needed) + - [Supported Authentication Mechanisms](#supported-authentication-mechanisms) + - [Step 1: Configure Tools in Toolbox Service](#step-1-configure-tools-in-toolbox-service) + - [Step 2: Configure SDK Client](#step-2-configure-sdk-client) + - [Provide an ID Token Retriever Function](#provide-an-id-token-retriever-function) + - [Option A: Add Authentication to a Loaded Tool](#option-a-add-authentication-to-a-loaded-tool) + - [Option B: Add Authentication While Loading Tools](#option-b-add-authentication-while-loading-tools) + - [Complete Authentication Example](#complete-authentication-example) +- [Binding Parameter Values](#binding-parameter-values) + - [Why Bind Parameters?](#why-bind-parameters) + - [Option A: Binding Parameters to a Loaded Tool](#option-a-binding-parameters-to-a-loaded-tool) + - [Option B: Binding Parameters While Loading Tools](#option-b-binding-parameters-while-loading-tools) + - [Binding Dynamic Values](#binding-dynamic-values) - [Contributing](#contributing) - [License](#license) - [Support](#support) From 699f8c63c4400d9f4b960f38fa06abc19100f770 Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Fri, 16 May 2025 22:11:28 +0530 Subject: [PATCH 14/14] Update packages/toolbox-core/README.md Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> --- packages/toolbox-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/README.md b/packages/toolbox-core/README.md index f6a0518d..74030312 100644 --- a/packages/toolbox-core/README.md +++ b/packages/toolbox-core/README.md @@ -228,7 +228,7 @@ workflow.add_edge("tools", "agent") app = workflow.compile() ``` -## Authenticating to the Toolbox Server +## Client to Server Authentication This section describes how to authenticate the ToolboxClient itself when connecting to a Toolbox server instance that requires authentication. This is