Skip to content

Commit 14a24f2

Browse files
ad548copybara-github
authored andcommitted
feat(bigtable): Support parameterized views with secure parameter inj…
Merge #6128 …ection Expose a parameterized query tool execute_sql_parameterized that automatically maps and injects secure parameters (like user_id) from the tool context to Bigtable's view_parameters. **Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.** ### Link to Issue or Description of Change **1. Link to an existing issue (if applicable):** - Closes: #_issue_number_ - Related: #_issue_number_ **2. Or, if no issue exists, describe the change:** _If applicable, please follow the issue templates to provide as much detail as possible._ **Problem:** _A clear and concise description of what the problem is._ **Solution:** _A clear and concise description of what you want to happen and why you choose this solution._ ### Testing Plan _Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes._ **Unit Tests:** - [ ] I have added or updated unit tests for my change. - [ ] All unit tests pass locally. _Please include a summary of passed `pytest` results._ **Manual End-to-End (E2E) Tests:** _Please provide instructions on how to manually test your changes, including any necessary setup or configuration. Please provide logs or screenshots to help reviewers better understand the fix._ ### Checklist - [ ] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document. - [ ] I have performed a self-review of my own code. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] New and existing unit tests pass locally with my changes. - [ ] I have manually tested my changes end-to-end. - [ ] Any dependent changes have been merged and published in downstream modules. ### Additional context _Add any other context or screenshots about the feature request here._ COPYBARA_INTEGRATE_REVIEW=#6128 from ad548:feat/bigtable-parameterized-views f5902fd PiperOrigin-RevId: 941207338
1 parent aceb7be commit 14a24f2

5 files changed

Lines changed: 385 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ optional-dependencies.all = [
7070
"google-cloud-aiplatform[agent-engines]>=1.148.1,<2",
7171
"google-cloud-bigquery>=2.2",
7272
"google-cloud-bigquery-storage>=2",
73-
"google-cloud-bigtable>=2.32",
73+
"google-cloud-bigtable>=2.39.1",
7474
"google-cloud-dataplex>=1.7,<3",
7575
"google-cloud-discoveryengine>=0.13.12,<0.14",
7676
"google-cloud-parametermanager>=0.4,<1",
@@ -165,7 +165,7 @@ optional-dependencies.gcp = [
165165
"google-cloud-aiplatform[agent-engines]>=1.148.1,<2",
166166
"google-cloud-bigquery>=2.2",
167167
"google-cloud-bigquery-storage>=2",
168-
"google-cloud-bigtable>=2.32",
168+
"google-cloud-bigtable>=2.39.1",
169169
"google-cloud-dataplex>=1.7,<3",
170170
"google-cloud-discoveryengine>=0.13.12,<0.14",
171171
"google-cloud-parametermanager>=0.4,<1",
@@ -208,7 +208,7 @@ optional-dependencies.test = [
208208
"google-cloud-aiplatform[agent-engines,evaluation]>=1.148.1,<2",
209209
"google-cloud-bigquery>=2.2",
210210
"google-cloud-bigquery-storage>=2",
211-
"google-cloud-bigtable>=2.32",
211+
"google-cloud-bigtable>=2.39.1",
212212
"google-cloud-dataplex>=1.7,<3",
213213
"google-cloud-discoveryengine>=0.13.12,<0.14",
214214
"google-cloud-firestore>=2.11,<3",

src/google/adk/tools/bigtable/bigtable_toolset.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@
1414

1515
from __future__ import annotations
1616

17+
import inspect
18+
from typing import Any
19+
from typing import Callable
1720
from typing import List
1821
from typing import Optional
1922
from typing import Union
2023

2124
from google.adk.agents.readonly_context import ReadonlyContext
25+
from google.auth.credentials import Credentials
26+
from pydantic import BaseModel
2227
from typing_extensions import override
2328

2429
from . import metadata_tool
@@ -29,9 +34,88 @@
2934
from ...tools.base_toolset import BaseToolset
3035
from ...tools.base_toolset import ToolPredicate
3136
from ...tools.google_tool import GoogleTool
37+
from ..tool_context import ToolContext
3238
from .bigtable_credentials import BigtableCredentialsConfig
3339
from .settings import BigtableToolSettings
3440

41+
42+
class BigtableParameterizedViewTool(GoogleTool):
43+
"""Wrapper FunctionTool for Bigtable execute_sql query tool that passes view parameters.
44+
45+
This tool wraps the Bigtable query tool to automatically resolve and inject
46+
parameters from the ToolContext (e.g. user_id) into the query's
47+
view_parameters. The parameter names to resolve are configured via
48+
view_parameter_names.
49+
50+
Example:
51+
If a parameterized view `purchase_history_pv` was created with the query:
52+
`SELECT * FROM purchases WHERE user_id = VIEW_PARAMETERS('user_id')`
53+
54+
By configuring `view_parameter_names=["user_id"]`, the wrapper will
55+
resolve the `user_id` value from the `tool_context.user_id` at runtime and
56+
pass it as `view_parameters={"user_id": user_id}`.
57+
This securely restricts query execution to the logged-in user's data
58+
without exposing the `user_id` parameter to the LLM.
59+
"""
60+
61+
def __init__(
62+
self,
63+
func: Callable[..., Any],
64+
*,
65+
credentials_config: Optional[BigtableCredentialsConfig] = None,
66+
tool_settings: Optional[BigtableToolSettings] = None,
67+
view_parameter_names: Optional[List[str]] = None,
68+
):
69+
"""Initializes the BigtableParameterizedViewTool.
70+
71+
Args:
72+
func: The Bigtable query function to wrap.
73+
credentials_config: The credentials configuration.
74+
tool_settings: The tool settings.
75+
view_parameter_names: A list of parameter names to resolve from
76+
tool_context and pass into view_parameters. This is configured on the
77+
toolset (BigtableToolset) and forwarded here.
78+
"""
79+
super().__init__(
80+
func=func,
81+
credentials_config=credentials_config,
82+
tool_settings=tool_settings,
83+
)
84+
self.name = "execute_sql_parameterized"
85+
self.description = (
86+
"Execute a GoogleSQL query from a Bigtable table using parameterized"
87+
" views to securely check permissions."
88+
)
89+
self.view_parameter_names = view_parameter_names
90+
# Exclude from being parsed and exposed to the LLM when generating tool schemas
91+
self._ignore_params.append("_view_parameters")
92+
93+
@override
94+
async def _run_async_with_credential(
95+
self,
96+
credentials: Credentials,
97+
tool_settings: BaseModel,
98+
args: dict[str, Any],
99+
tool_context: ToolContext,
100+
) -> Any:
101+
args_to_call = args.copy()
102+
signature = inspect.signature(self.func)
103+
if "_view_parameters" in signature.parameters and self.view_parameter_names:
104+
view_params = {}
105+
for param_name in self.view_parameter_names:
106+
# 1. Check if it's a strongly-typed top-level property (like 'user_id')
107+
if (val := getattr(tool_context, param_name, None)) is not None:
108+
view_params[param_name] = val
109+
# 2. Fallback to checking application-level session state
110+
elif tool_context.state and param_name in tool_context.state:
111+
view_params[param_name] = tool_context.state[param_name]
112+
113+
args_to_call["_view_parameters"] = view_params
114+
return await super()._run_async_with_credential(
115+
credentials, tool_settings, args_to_call, tool_context
116+
)
117+
118+
35119
DEFAULT_BIGTABLE_TOOL_NAME_PREFIX = "bigtable"
36120

37121

@@ -55,6 +139,7 @@ def __init__(
55139
tool_filter: Optional[Union[ToolPredicate, List[str]]] = None,
56140
credentials_config: Optional[BigtableCredentialsConfig] = None,
57141
bigtable_tool_settings: Optional[BigtableToolSettings] = None,
142+
view_parameter_names: Optional[List[str]] = None,
58143
):
59144
super().__init__(
60145
tool_filter=tool_filter,
@@ -66,6 +151,7 @@ def __init__(
66151
if bigtable_tool_settings
67152
else BigtableToolSettings()
68153
)
154+
self.view_parameter_names = view_parameter_names
69155

70156
def _is_tool_selected(
71157
self, tool: BaseTool, readonly_context: ReadonlyContext
@@ -102,6 +188,15 @@ async def get_tools(
102188
query_tool.execute_sql,
103189
]
104190
]
191+
if self.view_parameter_names:
192+
all_tools.append(
193+
BigtableParameterizedViewTool(
194+
func=query_tool.execute_sql,
195+
credentials_config=self._credentials_config,
196+
tool_settings=self._tool_settings,
197+
view_parameter_names=self.view_parameter_names,
198+
)
199+
)
105200
return [
106201
tool
107202
for tool in all_tools

src/google/adk/tools/bigtable/query_tool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ async def execute_sql(
4242
tool_context: ToolContext,
4343
parameters: Dict[str, Any] | None = None,
4444
parameter_types: Dict[str, Any] | None = None,
45+
_view_parameters: Dict[str, Any] | None = None,
4546
) -> dict:
4647
"""Execute a GoogleSQL query from a Bigtable table.
4748
@@ -56,6 +57,7 @@ async def execute_sql(
5657
parameters (dict): properties for parameter replacement. Keys must match
5758
the names used in ``query``.
5859
parameter_types (dict): maps explicit types for one or more param values.
60+
_view_parameters (dict): maps properties for parameterized views.
5961
6062
Returns:
6163
dict: Dictionary containing the status and the rows read.
@@ -91,6 +93,7 @@ def _execute_sql():
9193
instance_id=instance_id,
9294
parameters=parameters,
9395
parameter_types=parameter_types,
96+
view_parameters=_view_parameters,
9497
)
9598

9699
rows: List[Dict[str, Any]] = []

tests/unittests/tools/bigtable/test_bigtable_query_tool.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def raise_error():
193193
instance_id=instance_id,
194194
parameters=parameters,
195195
parameter_types=parameter_types,
196+
view_parameters=None,
196197
)
197198
mock_iterator.close.assert_called_once()
198199

@@ -228,3 +229,81 @@ async def test_execute_sql_row_value_circular_reference_fallback():
228229

229230
assert result["status"] == "SUCCESS"
230231
assert result["rows"][0]["col1"] == str(circular_value)
232+
233+
234+
@pytest.mark.asyncio
235+
async def test_execute_sql_with_view_parameters():
236+
"""Test execute_sql with _view_parameters passed."""
237+
project = "my_project"
238+
instance_id = "my_instance"
239+
query = "SELECT * FROM my_table"
240+
credentials = mock.create_autospec(Credentials, instance=True)
241+
tool_context = mock.create_autospec(ToolContext, instance=True)
242+
view_parameters = {"user_id": "test-user-123"}
243+
244+
with mock.patch.object(client, "get_bigtable_data_client") as mock_get_client:
245+
mock_client = mock.MagicMock()
246+
mock_get_client.return_value = mock_client
247+
mock_iterator = mock.create_autospec(ExecuteQueryIterator, instance=True)
248+
mock_client.execute_query.return_value = mock_iterator
249+
mock_iterator.__iter__.return_value = []
250+
251+
result = await execute_sql(
252+
project_id=project,
253+
instance_id=instance_id,
254+
credentials=credentials,
255+
query=query,
256+
settings=BigtableToolSettings(),
257+
tool_context=tool_context,
258+
_view_parameters=view_parameters,
259+
)
260+
261+
assert result["status"] == "SUCCESS"
262+
mock_client.execute_query.assert_called_once_with(
263+
query=query,
264+
instance_id=instance_id,
265+
parameters=None,
266+
parameter_types=None,
267+
view_parameters=view_parameters,
268+
)
269+
270+
271+
@pytest.mark.asyncio
272+
async def test_execute_sql_with_multiple_view_parameters():
273+
"""Test execute_sql with multiple view_parameters of different names."""
274+
project = "my_project"
275+
instance_id = "my_instance"
276+
query = "SELECT * FROM my_table"
277+
credentials = mock.create_autospec(Credentials, instance=True)
278+
tool_context = mock.create_autospec(ToolContext, instance=True)
279+
view_parameters = {
280+
"user_id": "test-user-123",
281+
"tenant_id": "tenant-xyz",
282+
"role": "admin",
283+
}
284+
285+
with mock.patch.object(client, "get_bigtable_data_client") as mock_get_client:
286+
mock_client = mock.MagicMock()
287+
mock_get_client.return_value = mock_client
288+
mock_iterator = mock.create_autospec(ExecuteQueryIterator, instance=True)
289+
mock_client.execute_query.return_value = mock_iterator
290+
mock_iterator.__iter__.return_value = []
291+
292+
result = await execute_sql(
293+
project_id=project,
294+
instance_id=instance_id,
295+
credentials=credentials,
296+
query=query,
297+
settings=BigtableToolSettings(),
298+
tool_context=tool_context,
299+
_view_parameters=view_parameters,
300+
)
301+
302+
assert result["status"] == "SUCCESS"
303+
mock_client.execute_query.assert_called_once_with(
304+
query=query,
305+
instance_id=instance_id,
306+
parameters=None,
307+
parameter_types=None,
308+
view_parameters=view_parameters,
309+
)

0 commit comments

Comments
 (0)