From 14977ca1e236a496e26d1373a0ededca28650fd7 Mon Sep 17 00:00:00 2001 From: Joshua McHenry Date: Fri, 2 Oct 2026 19:25:00 -0400 Subject: [PATCH 1/4] feat(bq_driver): add AllowedProjects DSN option to scope SQLTables Power Query's Navigator opens a connection and immediately calls SQLTables with wildcard catalog/schema arguments. GetFilteredProjectIds answers that by enumerating every project the authenticated principal can reach via the projects.list REST API, then listing datasets for each one. In a large organization the principal can typically see far more projects than it uses. #1241 made that enumeration tolerate projects it cannot read, so the Navigator tree now builds, but it still contacts every visible project: on one enterprise account it takes about 90 seconds before the Navigator appears. #1627 and #1632 avoid enumeration when a concrete catalog is known, but the Navigator's opening call passes SQL_ALL_CATALOGS, so neither applies to that first load. Add an AllowedProjects DSN option naming the projects to expose. When set, GetFilteredProjectIds filters that list and skips projects.list entirely. The existing filter semantics are preserved: entries are matched against the catalog argument with the same LIKE pattern / SQL_ATTR_METADATA_ID rules used elsewhere. AllowedProjects and AdditionalProjects stay orthogonal and composable: if both are set, enumeration starts from the allowlist and still appends the AdditionalProjects ids on top. The setting is exposed in the Advanced Options dialog as a single comma-separated text field, following the MaximumBytesBilled field from #1672. To fit it within the existing dialog height, the rows below Max Retries are respaced to a uniform 25px and the Additional projects box is reduced to the height of the Query properties box. Tests: unit coverage for FilterAllowedProjects and ALLOWEDPROJECTS parsing, a dialog round-trip test, and an integration test asserting that a project the principal can reach but did not allow-list is absent from SQLTables. --- .../bq_driver/internal/driver_adv_opt_form.cc | 42 +++++++++++--- .../bq_driver/internal/driver_adv_opt_form.h | 7 ++- .../internal/driver_adv_opt_form_test.cc | 38 +++++++++++++ .../odbc/bq_driver/internal/driver_form.cc | 1 + .../bq_driver/internal/odbc_conn_handle.cc | 1 + .../bq_driver/internal/odbc_conn_handle.h | 1 + .../internal/odbc_conn_handle_test.cc | 23 ++++++++ .../bq_driver/internal/odbc_sql_tables.cc | 50 +++++++++++++--- .../odbc/bq_driver/internal/odbc_sql_tables.h | 18 +++++- .../internal/odbc_sql_tables_test.cc | 45 +++++++++++++++ .../cloud/odbc/bq_driver/odbc_connection.cc | 1 + .../odbc/bq_driver/odbc_driver_metadata.cc | 6 +- google/cloud/odbc/bq_driver/odbc_windows.cc | 4 ++ google/cloud/odbc/bq_driver/odbc_windows.h | 1 + .../odbc_driver_tests/catalog_test.cc | 57 +++++++++++++++++++ 15 files changed, 271 insertions(+), 24 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.cc b/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.cc index 4df05930b0..f3fd68904a 100644 --- a/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.cc +++ b/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.cc @@ -66,6 +66,7 @@ std::string AdvanceOptions::private_service_connect_uris_; std::string AdvanceOptions::enable_gcd_; std::string AdvanceOptions::universe_domain_; std::string AdvanceOptions::maximum_bytes_billed_; +std::string AdvanceOptions::allowed_projects_; std::string const kLanguageDialect = "SQLDialect"; std::string const kLargeResultsDatasetId = "LargeResultsDatasetId"; @@ -90,6 +91,7 @@ std::string const kPrivateServiceConnectUris = "PrivateServiceConnectUris"; std::string const kEnableGcd = "EnableGCD"; std::string const kUniverseDomain = "UniverseDomain"; std::string const kMaximumBytesBilled = "MaximumBytesBilled"; +std::string const kAllowedProjects = "AllowedProjects"; // Control dimensions and positions int const kHeight = 20; @@ -383,11 +385,11 @@ void AdvanceOptions::CreateAdditionalControls(HFONT h_font) { // max retries HWND h_max_retries_label = - CreateLabel(adv_hwnd, "Max Retries:", kXAxis, kYAxis + 465, kWidth * 7, + CreateLabel(adv_hwnd, "Max Retries:", kXAxis, kYAxis + 460, kWidth * 7, kHeight, WS_VISIBLE | SS_LEFT); SendMessage(h_max_retries_label, WM_SETFONT, (WPARAM)h_font, TRUE); HWND h_max_retries_edit = - CreateEditBox(adv_hwnd, kinputComboBoxXAxis, kYAxis + 465, kEditBoxWidth, + CreateEditBox(adv_hwnd, kinputComboBoxXAxis, kYAxis + 460, kEditBoxWidth, kEditBoxHeight, kIdcMaxRetriesEdit); SendMessage(h_max_retries_edit, WM_SETFONT, (WPARAM)h_font, TRUE); SetWindowSubclass(GetDlgItem(adv_hwnd, kIdcMaxRetriesEdit), InputSubclassProc, @@ -400,10 +402,10 @@ void AdvanceOptions::CreateAdditionalControls(HFONT h_font) { // maximum bytes billed HWND h_maximum_bytes_billed_label = CreateLabel(adv_hwnd, "Maximum bytes billed (bytes):", kXAxis, - kYAxis + 490, kWidth * 4 + 25, kHeight, WS_VISIBLE | SS_LEFT); + kYAxis + 485, kWidth * 4 + 25, kHeight, WS_VISIBLE | SS_LEFT); SendMessage(h_maximum_bytes_billed_label, WM_SETFONT, (WPARAM)h_font, TRUE); HWND h_maximum_bytes_billed_edit = - CreateEditBox(adv_hwnd, kinputComboBoxXAxis, kYAxis + 490, kEditBoxWidth, + CreateEditBox(adv_hwnd, kinputComboBoxXAxis, kYAxis + 485, kEditBoxWidth, kEditBoxHeight, kIdcMaximumBytesBilledEdit); SendMessage(h_maximum_bytes_billed_edit, WM_SETFONT, (WPARAM)h_font, TRUE); SetWindowSubclass(GetDlgItem(adv_hwnd, kIdcMaximumBytesBilledEdit), @@ -413,6 +415,19 @@ void AdvanceOptions::CreateAdditionalControls(HFONT h_font) { GetWindowLongPtr(h_maximum_bytes_billed_edit, GWL_STYLE) | ES_RIGHT | ES_NUMBER); + // allowed projects + HWND h_allowed_projects_label = + CreateLabel(adv_hwnd, "Allowed projects:", kXAxis, kYAxis + 510, + kWidth * 4 + 25, kHeight, WS_VISIBLE | SS_LEFT); + SendMessage(h_allowed_projects_label, WM_SETFONT, (WPARAM)h_font, TRUE); + HWND h_allowed_projects_edit = + CreateEditBox(adv_hwnd, kinputComboBoxXAxis, kYAxis + 510, kEditBoxWidth, + kEditBoxHeight, kIdcAllowedProjectsEdit); + SendMessage(h_allowed_projects_edit, WM_SETFONT, (WPARAM)h_font, TRUE); + SetWindowSubclass(GetDlgItem(adv_hwnd, kIdcAllowedProjectsEdit), + InputSubclassProc, 0, 0); + SetWindowText(h_allowed_projects_edit, allowed_projects_.c_str()); + // TODO(b/497725655): Enable UI feature after public release // HWND h_variables_checkbox = CreateCheckBox( // adv_hwnd, "Use SQL_WVARCHAR instead of SQL_VARCHAR", kXAxis, kYAxis + @@ -423,12 +438,12 @@ void AdvanceOptions::CreateAdditionalControls(HFONT h_font) { // SetWindowSubclass(GetDlgItem(adv_hwnd, kIdcVariableCheckbox), // CheckboxSubclassProc, 0, 0); HWND h_additional_projects_label = - CreateLabel(adv_hwnd, "Additional projects:", kXAxis, kYAxis + 515, + CreateLabel(adv_hwnd, "Additional projects:", kXAxis, kYAxis + 535, kWidth * 5, kHeight, WS_VISIBLE | SS_LEFT); SendMessage(h_additional_projects_label, WM_SETFONT, (WPARAM)h_font, TRUE); HWND h_additional_projects_edit = - CreateScrollableEditBox(adv_hwnd, kXAxis, kYAxis + 535, kWidth + 445, - kHeight + 16, kIdcAdditionalProjectsEdit); + CreateScrollableEditBox(adv_hwnd, kXAxis, kYAxis + 555, kWidth + 445, + kHeight + 6, kIdcAdditionalProjectsEdit); SendMessage(h_additional_projects_edit, WM_SETFONT, (WPARAM)h_font, TRUE); SetWindowText(h_additional_projects_edit, additional_projects_.c_str()); @@ -436,11 +451,11 @@ void AdvanceOptions::CreateAdditionalControls(HFONT h_font) { InputSubclassProc, 0, 0); HWND h_query_properties_label = - CreateLabel(adv_hwnd, "Query properties:", kXAxis, kYAxis + 575, + CreateLabel(adv_hwnd, "Query properties:", kXAxis, kYAxis + 585, kWidth * 5, kHeight, WS_VISIBLE | SS_LEFT); SendMessage(h_query_properties_label, WM_SETFONT, (WPARAM)h_font, TRUE); HWND h_query_properties_edit = - CreateScrollableEditBox(adv_hwnd, kXAxis, kYAxis + 595, kWidth + 445, + CreateScrollableEditBox(adv_hwnd, kXAxis, kYAxis + 605, kWidth + 445, kHeight + 6, kIdcQueryPropertiesEdit); SendMessage(h_query_properties_edit, WM_SETFONT, (WPARAM)h_font, TRUE); @@ -681,6 +696,13 @@ LRESULT CALLBACK AdvanceOptions::AdvanceOptProc(HWND hwnd, UINT u_msg, sizeof(additional_projects_buffer)); additional_projects_ = additional_projects_buffer; + HWND h_allowed_projects_edit = + GetDlgItem(hwnd, kIdcAllowedProjectsEdit); + char allowed_projects_buffer[1024] = {0}; + GetWindowText(h_allowed_projects_edit, allowed_projects_buffer, + sizeof(allowed_projects_buffer)); + allowed_projects_ = allowed_projects_buffer; + HWND h_query_properties_edit = GetDlgItem(hwnd, kIdcQueryPropertiesEdit); char query_properties_buffer[1024] = {0}; @@ -889,6 +911,7 @@ void AdvanceOptions::SetValues(Section const& attribute_map) { enable_gcd_ = GetValueOrDefault(attribute_map, kEnableGcd); universe_domain_ = GetValueOrDefault(attribute_map, kUniverseDomain); maximum_bytes_billed_ = GetValueOrDefault(attribute_map, kMaximumBytesBilled); + allowed_projects_ = GetValueOrDefault(attribute_map, kAllowedProjects); } void AdvanceOptions::ResetToDefaults() { @@ -914,6 +937,7 @@ void AdvanceOptions::ResetToDefaults() { universe_domain_.clear(); // Unset means no cap, which is the shipped default. maximum_bytes_billed_.clear(); + allowed_projects_.clear(); } void AdvanceOptions::Show(HWND hwnd) { diff --git a/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.h b/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.h index 35bd3fc785..308305d58b 100644 --- a/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.h +++ b/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form.h @@ -20,7 +20,7 @@ #pragma comment(lib, "Comctl32.lib") // Link with Comctl32.lib namespace google::cloud::odbc_bq_driver_internal { -// NEXTID:154 +// NEXTID:155 static int const kIdcUseDefaultCheckbox = 128; static int const kIdcDatasetNameEdit = 129; static int const kIdcTempExpirationEdit = 130; @@ -46,6 +46,7 @@ static int const kIdcEnablePscGcdCheckbox = 150; static int const kIdcPrivateServiceNameEdit = 151; static int const kIdcUniverseDomainEdit = 152; static int const kIdcMaximumBytesBilledEdit = 153; +static int const kIdcAllowedProjectsEdit = 154; class AdvanceOptions { public: @@ -109,6 +110,9 @@ class AdvanceOptions { inline std::string const& GetMaximumBytesBilled() const { return maximum_bytes_billed_; } + inline std::string const& GetAllowedProjects() const { + return allowed_projects_; + } void SetValues(Section const& attributes_map); void ResetToDefaults(); @@ -141,6 +145,7 @@ class AdvanceOptions { static std::string enable_gcd_; static std::string universe_domain_; static std::string maximum_bytes_billed_; + static std::string allowed_projects_; static LRESULT CALLBACK AdvanceOptProc(HWND hwnd, UINT uMsg, WPARAM w_param, LPARAM l_param); diff --git a/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form_test.cc b/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form_test.cc index 7af395755b..a211a4d2b2 100644 --- a/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form_test.cc +++ b/google/cloud/odbc/bq_driver/internal/driver_adv_opt_form_test.cc @@ -72,6 +72,31 @@ TEST_F(AdvanceOptionsTest, ShowWindow) { << "Window should be visible after calling Show."; } +TEST_F(AdvanceOptionsTest, AllowedProjectsRoundTripsThroughOk) { + // A saved value should appear in the field when the dialog opens, and OK + // should save back whatever the field then holds. + advance_options->SetValues({{"AllowedProjects", "project-a,project-b"}}); + + // Show() runs a modal GetMessage loop. Queue WM_QUIT first so it builds the + // controls and returns immediately instead of blocking this test; the window + // itself stays alive for the assertions below. + PostQuitMessage(0); + advance_options->Show(nullptr); + HWND hwnd = advance_options->GetHwnd(); + ASSERT_NE(hwnd, nullptr) << "Window should be created and displayed."; + + HWND h_edit = GetDlgItem(hwnd, kIdcAllowedProjectsEdit); + ASSERT_NE(h_edit, nullptr) << "Allowed projects field should be created."; + char buffer[256] = {0}; + GetWindowText(h_edit, buffer, sizeof(buffer)); + EXPECT_STREQ(buffer, "project-a,project-b"); + + SetWindowText(h_edit, "project-a"); + ClickButton(hwnd, kIdcOKButton); + + EXPECT_EQ(advance_options->GetAllowedProjects(), "project-a"); +} + TEST_F(AdvanceOptionsTest, SetValuesValidinput) { Section attribute_map = {{"SQLDialect", "1"}, {"LargeResultsDatasetId", "dataset1"}, @@ -81,6 +106,7 @@ TEST_F(AdvanceOptionsTest, SetValuesValidinput) { {"LargeResultsTempTableExpirationTime", "3600000"}, {"SessionLocation", "USA"}, {"AdditionalProjects", "projectA,projectB"}, + {"AllowedProjects", "projectC,projectD"}, {"QueryProperties", "property1=value1"}, {"MaxThreads", "10"}, {"MaxRetries", "9"}}; @@ -96,6 +122,7 @@ TEST_F(AdvanceOptionsTest, SetValuesValidinput) { EXPECT_EQ(options.GetTempTableExpiration(), "3600000"); EXPECT_EQ(options.GetSessionLocation(), "USA"); EXPECT_EQ(options.GetAdditionalProjects(), "projectA,projectB"); + EXPECT_EQ(options.GetAllowedProjects(), "projectC,projectD"); EXPECT_EQ(options.GetQueryProperties(), "property1=value1"); EXPECT_EQ(options.GetMaxThreads(), "10"); EXPECT_EQ(options.GetMaxRetries(), "9"); @@ -118,8 +145,19 @@ TEST_F(AdvanceOptionsTest, SetValuesMissingkeys) { EXPECT_EQ(options.GetTempTableExpiration(), "3600000"); EXPECT_EQ(options.GetSessionLocation(), ""); EXPECT_EQ(options.GetAdditionalProjects(), ""); + EXPECT_EQ(options.GetAllowedProjects(), ""); EXPECT_EQ(options.GetQueryProperties(), ""); EXPECT_EQ(options.GetAllowHtapiForLargeResultsCheckbox(), ""); } +TEST_F(AdvanceOptionsTest, ResetToDefaultsClearsAllowedProjects) { + AdvanceOptions options; + options.SetValues({{"AllowedProjects", "projectC,projectD"}}); + ASSERT_EQ(options.GetAllowedProjects(), "projectC,projectD"); + + options.ResetToDefaults(); + + EXPECT_EQ(options.GetAllowedProjects(), ""); +} + } // namespace google::cloud::odbc_bq_driver_internal diff --git a/google/cloud/odbc/bq_driver/internal/driver_form.cc b/google/cloud/odbc/bq_driver/internal/driver_form.cc index 9c521cd6b4..ec4a40d3d3 100644 --- a/google/cloud/odbc/bq_driver/internal/driver_form.cc +++ b/google/cloud/odbc/bq_driver/internal/driver_form.cc @@ -223,6 +223,7 @@ static Section BuildTestConnectionAttributes( attributes_map["SessionLocation"] = adv_form.GetSessionLocation(); attributes_map["MaxThreads"] = adv_form.GetMaxThreads(); attributes_map["AdditionalProjects"] = adv_form.GetAdditionalProjects(); + attributes_map["AllowedProjects"] = adv_form.GetAllowedProjects(); attributes_map["QueryProperties"] = adv_form.GetQueryProperties(); attributes_map["UseWVarChar"] = adv_form.GetUseWchar(); attributes_map["EnableSession"] = adv_form.GetEnableSession(); diff --git a/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.cc b/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.cc index 2859ac566e..808cd73b19 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.cc @@ -164,6 +164,7 @@ void ConnectionHandle::SetUp(Section& dsn_section, dsn_.kms_key_name = dsn_section["KMSKEYNAME"]; dsn_.session_location = dsn_section["SESSIONLOCATION"]; dsn_.additional_projects = dsn_section["ADDITIONALPROJECTS"]; + dsn_.allowed_projects = dsn_section["ALLOWEDPROJECTS"]; dsn_.psc = dsn_section["PRIVATESERVICECONNECTURIS"]; dsn_.enable_gcd = dsn_section["ENABLEGCD"] == "1" || dsn_section["ENABLEGCD"] == "true"; diff --git a/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.h b/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.h index f3dc1e4119..a2f1858550 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.h +++ b/google/cloud/odbc/bq_driver/internal/odbc_conn_handle.h @@ -108,6 +108,7 @@ struct Dsn { // Proxy options fields google::cloud::odbc_bigquery_client_interface::ProxyOptions proxy_options; std::string additional_projects; + std::string allowed_projects; std::string psc; bool enable_gcd; std::string universe_domain; diff --git a/google/cloud/odbc/bq_driver/internal/odbc_conn_handle_test.cc b/google/cloud/odbc/bq_driver/internal/odbc_conn_handle_test.cc index 2c7f3c8f65..ef70478f79 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_conn_handle_test.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_conn_handle_test.cc @@ -358,6 +358,29 @@ TEST(ConnectionHandle, DsnSetupListProjectsParentSet) { EXPECT_EQ(actual.list_projects_parent, kDsnListProjectsParent); } +TEST(ConnectionHandle, DsnSetupAllowedProjectsNotSet) { + ConnectionHandle conn_handle; + Section dsn_section; + + conn_handle.SetUp(dsn_section, kDsnName); + + Dsn actual = conn_handle.GetDsn(); + EXPECT_TRUE(actual.allowed_projects.empty()); +} + +TEST(ConnectionHandle, DsnSetupAllowedProjectsSet) { + ConnectionHandle conn_handle; + Section dsn_section; + + dsn_section["ALLOWEDPROJECTS"] = "project-1,project-2"; + conn_handle.SetUp(dsn_section, kDsnName); + + Dsn actual = conn_handle.GetDsn(); + EXPECT_EQ(actual.allowed_projects, "project-1,project-2"); + // AdditionalProjects and AllowedProjects are independent settings. + EXPECT_TRUE(actual.additional_projects.empty()); +} + TEST(ConnectionHandle, SetAttributeSuccessSQLUInt) { ConnectionHandle conn_handle; diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc index d920ab3ccb..ce77665896 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc @@ -111,9 +111,38 @@ StatusRecord ValidateInputParameters( return StatusRecord::Ok(); } +std::vector FilterAllowedProjects( + std::string const& allowed_projects, std::string const& projects_filter, + SQLULEN metadata_id) { + std::vector project_ids; + auto filter_regex = BuildRegex(projects_filter, metadata_id); + std::vector allowed_ids = Split(allowed_projects, ","); + for (auto& project_id : allowed_ids) { + Trim(project_id); + if (project_id.empty()) { + continue; + } + if ((!metadata_id && projects_filter == kMatchAll) || + re2::RE2::FullMatch(project_id, *filter_regex)) { + project_ids.push_back(std::move(project_id)); + } + } + return project_ids; +} + StatusRecordOr> GetFilteredProjectIds( ODBCBQClient& bq_client, std::string const& projects_filter, - SQLULEN metadata_id) { + SQLULEN metadata_id, std::string const& allowed_projects) { + // An explicit allowlist replaces REST-based project discovery entirely. + // projects.list enumerates every project the authenticated principal can + // reach, which is slow and quota-heavy for accounts with access to many + // projects; when the user has named the projects they care about there is + // nothing to discover. + if (!allowed_projects.empty()) { + return FilterAllowedProjects(allowed_projects, projects_filter, + metadata_id); + } + std::vector project_ids; std::unique_ptr filter_regex = BuildRegex(projects_filter, metadata_id); @@ -399,9 +428,10 @@ ResultSet ProcessStringResults( StatusRecordOr GetResultSetForProjects( ODBCBQClient& bq_client, SQLULEN metadata_id, - std::string const& additional_projects) { - auto project_ids_status = - GetFilteredProjectIds(bq_client, kMatchAll, metadata_id); + std::string const& additional_projects, + std::string const& allowed_projects) { + auto project_ids_status = GetFilteredProjectIds( + bq_client, kMatchAll, metadata_id, allowed_projects); if (!project_ids_status) { LOG(ERROR) << "GetResultSetForProjects::GetFilteredProjectIds:: " << project_ids_status.GetStatusRecord().message; @@ -419,9 +449,10 @@ StatusRecordOr GetResultSetForProjects( StatusRecordOr GetResultSetForDatasets( ODBCBQClient& bq_client, SQLULEN metadata_id, - std::string const& catalog_name, std::string const& additional_projects) { - auto project_ids_status = - GetFilteredProjectIds(bq_client, catalog_name, metadata_id); + std::string const& catalog_name, std::string const& additional_projects, + std::string const& allowed_projects) { + auto project_ids_status = GetFilteredProjectIds( + bq_client, catalog_name, metadata_id, allowed_projects); if (!project_ids_status) { LOG(ERROR) << "GetResultSetForDatasets::GetFilteredProjectIds:: " << project_ids_status.GetStatusRecord().message; @@ -481,6 +512,7 @@ StatusRecordOr GetResultSetForTables( std::string const& project_filter, std::string const& dataset_filter, std::string const& table_filter, std::string const& table_type_filter, SQLULEN metadata_id) { + ConnectionHandle& conn_handle = *(stmt_handle.GetConnectionHandle()); // When the catalog argument is an exact project identifier -- either because // SQL_ATTR_METADATA_ID is true, or because the LIKE pattern contains no // wildcard -- skip enumerating every accessible project via projects.list and @@ -492,7 +524,8 @@ StatusRecordOr GetResultSetForTables( project_list = {std::move(*project_literal)}; } else { auto projects_status_record_or = - GetFilteredProjectIds(bq_client, project_filter, metadata_id); + GetFilteredProjectIds(bq_client, project_filter, metadata_id, + conn_handle.GetDsn().allowed_projects); if (!projects_status_record_or) { LOG(ERROR) << "GetResultSetForTables::GetFilteredProjectIds:: " << projects_status_record_or.GetStatusRecord().message; @@ -500,7 +533,6 @@ StatusRecordOr GetResultSetForTables( } project_list = *projects_status_record_or; } - ConnectionHandle& conn_handle = *(stmt_handle.GetConnectionHandle()); // Append additional projects if any project_list = AppendAdditionalProjectsIfMissing( bq_client, metadata_id, std::move(project_list), diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h index 41ce56e93a..0806a841cc 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h @@ -55,12 +55,22 @@ odbc_internal::StatusRecord ValidateInputParameters( const SQLCHAR* table_name, SQLSMALLINT table_name_len, SQLSMALLINT table_type_len, SQLULEN metadata_id); +// Return the project ids from the comma-separated 'allowed_projects' allowlist +// that match 'projects_filter', using the same LIKE-pattern / +// SQL_ATTR_METADATA_ID semantics as GetFilteredProjectIds. Blank entries are +// ignored and surrounding whitespace is trimmed. Makes no REST calls. +std::vector FilterAllowedProjects( + std::string const& allowed_projects, std::string const& projects_filter, + SQLULEN metadata_id); + // Return a list of project ids depending on SQL_ATTR_METADATA_ID and // 'projects_filter'. Returns all project ids if SQL_ATTR_METADATA_ID == // SQL_FALSE and projects_filter == "%". +// When 'allowed_projects' is non-empty it is used as the set of candidate +// projects and projects.list is not called at all. odbc_internal::StatusRecordOr> GetFilteredProjectIds( ODBCBQClient& bq_client, std::string const& projects_filter, - SQLULEN metadata_id); + SQLULEN metadata_id, std::string const& allowed_projects = ""); // Return a list of dataset ids depending on SQL_ATTR_METADATA_ID and // 'datasets_filter'. Returns all dataset ids if SQL_ATTR_METADATA_ID == @@ -123,13 +133,15 @@ ResultSet ProcessStringResults( // Search for all projects and populate ResultSet for it. odbc_internal::StatusRecordOr GetResultSetForProjects( ODBCBQClient& bq_client, SQLULEN metadata_id, - std::string const& additional_projects = ""); + std::string const& additional_projects = "", + std::string const& allowed_projects = ""); // Search for all datasets in all projects and populate ResultSet for it. odbc_internal::StatusRecordOr GetResultSetForDatasets( ODBCBQClient& bq_client, SQLULEN metadata_id, std::string const& catalog_name = kMatchAll, - std::string const& additional_projects = ""); + std::string const& additional_projects = "", + std::string const& allowed_projects = ""); // Search for tables and populate ResultSet according to ODBC spec odbc_internal::StatusRecordOr GetResultSetForTables( diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc index 7e8e31c31d..9fbeda7ef3 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc @@ -23,7 +23,9 @@ using ::google::cloud::bigquery_v2_minimal_internal::QueryParameter; using google::cloud::odbc_internal::SQLStates; using google::cloud::odbc_internal::StatusRecord; using google::cloud::odbc_testing_bq_driver_utils::CastToSQLCHAR; +using ::testing::ElementsAre; using ::testing::HasSubstr; +using ::testing::IsEmpty; TEST(ValidateInputParameters, Success) { StatusRecord status = ValidateInputParameters( @@ -238,6 +240,49 @@ TEST(ConstructQuery, ConstructWithTwoClausesEmptystrings) { EXPECT_EQ(2, named_query_params.size()); } +TEST(FilterAllowedProjects, WildcardReturnsWholeAllowlistTrimmed) { + EXPECT_THAT(FilterAllowedProjects(" project-1 ,project-2,\tproject-3 ", "%", + SQL_FALSE), + ElementsAre("project-1", "project-2", "project-3")); +} + +TEST(FilterAllowedProjects, EmptyAllowlistReturnsNothing) { + EXPECT_THAT(FilterAllowedProjects("", "%", SQL_FALSE), IsEmpty()); +} + +TEST(FilterAllowedProjects, BlankEntriesAreIgnored) { + EXPECT_THAT(FilterAllowedProjects(" , project-1 ,, ,", "%", SQL_FALSE), + ElementsAre("project-1")); +} + +TEST(FilterAllowedProjects, LikePatternFiltersAllowlist) { + EXPECT_THAT(FilterAllowedProjects("prod-a,prod-b,dev-c", "prod%", SQL_FALSE), + ElementsAre("prod-a", "prod-b")); +} + +TEST(FilterAllowedProjects, UnderscoreMatchesSingleCharacter) { + EXPECT_THAT(FilterAllowedProjects("pa,pb,pcc", "p_", SQL_FALSE), + ElementsAre("pa", "pb")); +} + +TEST(FilterAllowedProjects, NonMatchingPatternReturnsNothing) { + EXPECT_THAT(FilterAllowedProjects("prod-a,prod-b", "staging%", SQL_FALSE), + IsEmpty()); +} + +TEST(FilterAllowedProjects, MetadataIdMatchesExactIdCaseInsensitively) { + EXPECT_THAT( + FilterAllowedProjects("Project-1,project-2", "PROJECT-1", SQL_TRUE), + ElementsAre("Project-1")); +} + +TEST(FilterAllowedProjects, MetadataIdTreatsWildcardAsLiteral) { + // With SQL_ATTR_METADATA_ID set the catalog argument is an identifier, not a + // pattern, so "%" matches only a project literally named "%". + EXPECT_THAT(FilterAllowedProjects("project-1,project-2", "%", SQL_TRUE), + IsEmpty()); +} + TEST(CreateResultSetForProjects, CreateResultSetForProjects) { std::vector project_ids = {"id-1", "id-2"}; diff --git a/google/cloud/odbc/bq_driver/odbc_connection.cc b/google/cloud/odbc/bq_driver/odbc_connection.cc index 44ad27886d..290839a608 100644 --- a/google/cloud/odbc/bq_driver/odbc_connection.cc +++ b/google/cloud/odbc/bq_driver/odbc_connection.cc @@ -211,6 +211,7 @@ SQLRETURN HandleDriverPrompt(std::string& conn_string, SQLHWND window_handle, {"SESSIONLOCATION", form.GetAdvanceOptions()->GetSessionLocation()}, {"ENABLESESSION", form.GetAdvanceOptions()->GetEnableSession()}, {"ADDITIONALPROJECTS", form.GetAdvanceOptions()->GetAdditionalProjects()}, + {"ALLOWEDPROJECTS", form.GetAdvanceOptions()->GetAllowedProjects()}, }; handle_ref->SetUp(dsn_section, form.GetDSN()); diff --git a/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc b/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc index fa87214ba9..73d082ca5c 100644 --- a/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc +++ b/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc @@ -646,12 +646,14 @@ SQLRETURN SQLTablesInternal(SQLHSTMT stmt_handle, SQLCHAR* catalog_name, if (!metadata_id && project_filter == SQL_ALL_CATALOGS && dataset_filter.empty() && table_filter.empty()) { result_set_status = GetResultSetForProjects( - bq_client, metadata_id, conn_handle.GetDsn().additional_projects); + bq_client, metadata_id, conn_handle.GetDsn().additional_projects, + conn_handle.GetDsn().allowed_projects); } else if (!metadata_id && project_filter.empty() && dataset_filter == SQL_ALL_SCHEMAS && table_filter.empty()) { result_set_status = GetResultSetForDatasets(bq_client, metadata_id, kMatchAll, - conn_handle.GetDsn().additional_projects); + conn_handle.GetDsn().additional_projects, + conn_handle.GetDsn().allowed_projects); } else if (!metadata_id && project_filter.empty() && dataset_filter.empty() && table_filter.empty() && table_type_filter == SQL_ALL_TABLE_TYPES) { result_set_status = CreateResultSetForTableTypes(); diff --git a/google/cloud/odbc/bq_driver/odbc_windows.cc b/google/cloud/odbc/bq_driver/odbc_windows.cc index 839b57d744..ac42a712f8 100644 --- a/google/cloud/odbc/bq_driver/odbc_windows.cc +++ b/google/cloud/odbc/bq_driver/odbc_windows.cc @@ -145,6 +145,8 @@ bool ConfigDSNInternal(HWND hwnd_parent, WORD f_request, LPCSTR lpsz_driver, GetValueOrDefault(section, max_retries_key, kDefaultMaxRetries); std::string additional_projects = GetValueOrDefault(section, additional_projects_key); + std::string allowed_projects = + GetValueOrDefault(section, allowed_projects_key); std::string query_properties = GetValueOrDefault(section, query_properties_key); // std::string use_wchar = GetValueOrDefault(section, use_wchar_key); @@ -196,6 +198,7 @@ bool ConfigDSNInternal(HWND hwnd_parent, WORD f_request, LPCSTR lpsz_driver, {temp_expiration_key, temp_expiration}, {session_location_key, session_location}, {additional_projects_key, additional_projects}, + {allowed_projects_key, allowed_projects}, {query_properties_key, query_properties}, // {use_wchar_key, use_wchar}, {enable_session_key, enable_session}, @@ -260,6 +263,7 @@ bool ConfigDSNInternal(HWND hwnd_parent, WORD f_request, LPCSTR lpsz_driver, temp_expiration = advance_form.GetTempTableExpiration(); session_location = advance_form.GetSessionLocation(); additional_projects = advance_form.GetAdditionalProjects(); + allowed_projects = advance_form.GetAllowedProjects(); query_properties = advance_form.GetQueryProperties(); // use_wchar = advance_form.GetUseWchar(); enable_session = advance_form.GetEnableSession(); diff --git a/google/cloud/odbc/bq_driver/odbc_windows.h b/google/cloud/odbc/bq_driver/odbc_windows.h index 20917a8337..2f968e4591 100644 --- a/google/cloud/odbc/bq_driver/odbc_windows.h +++ b/google/cloud/odbc/bq_driver/odbc_windows.h @@ -44,6 +44,7 @@ std::string const default_string_length_key = "DefaultStringColumnLength"; std::string const temp_expiration_key = "LargeResultsTempTableExpirationTime"; std::string const session_location_key = "SessionLocation"; std::string const additional_projects_key = "AdditionalProjects"; +std::string const allowed_projects_key = "AllowedProjects"; std::string const query_properties_key = "QueryProperties"; std::string const max_threads_key = "MaxThreads"; std::string const max_retries_key = "MaxRetries"; diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc index 9c91207168..09797dd860 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc @@ -506,6 +506,63 @@ error } */ +TEST(CatalogTest, SQLTables_AllowedProjects) { + // First learn every project the test principal can reach, so the allow-listed + // run below can be compared against it. + std::set all_catalogs; + { + auto conn = std::make_shared(); + EXPECT_EQ(Connect(kDefaultConnectionString, conn), SQL_SUCCESS); + auto status = SQLSetStmtAttr(conn->hstmt, SQL_ATTR_METADATA_ID, + (SQLPOINTER)SQL_FALSE, 0); + CheckError(status, "SQLSetStmtAttr", conn); + for (auto const& result : + Catalog::GetTables(conn, SQL_ALL_CATALOGS, "", "")) { + if (result.project_name.has_value()) { + all_catalogs.insert(result.project_name.value()); + } + } + EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); + } + ASSERT_TRUE(all_catalogs.find(kCatalogName) != all_catalogs.end()) + << "Default project/catalog not found without an allowlist."; + if (all_catalogs.size() < 2) { + GTEST_LOG_(WARNING) + << "Test principal can reach only " << all_catalogs.size() + << " project(s), so this run does not exercise exclusion."; + } + + auto conn = std::make_shared(); + std::string connection_string = + kDefaultConnectionString + ";AllowedProjects=" + kCatalogName; + EXPECT_EQ(Connect(connection_string, conn), SQL_SUCCESS); + + auto status = SQLSetStmtAttr(conn->hstmt, SQL_ATTR_METADATA_ID, + (SQLPOINTER)SQL_FALSE, 0); + CheckError(status, "SQLSetStmtAttr", conn); + + std::vector results = + Catalog::GetTables(conn, SQL_ALL_CATALOGS, "", ""); + + std::set catalogs; + for (auto const& result : results) { + if (result.project_name.has_value()) { + catalogs.insert(result.project_name.value()); + } + + EXPECT_FALSE(result.dataset_name.has_value()); + EXPECT_FALSE(result.table_name.has_value()); + EXPECT_FALSE(result.table_type.has_value()); + EXPECT_FALSE(result.description.has_value()); + } + + // Exactly the allowlist: any other project the principal can reach must be + // absent, which is only possible if projects.list was not consulted. + EXPECT_EQ(catalogs, std::set{kCatalogName}); + + EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); +} + TEST(CatalogTest, SQLTables_AllDatasets) { auto conn = std::make_shared(); EXPECT_EQ(Connect(kDefaultConnectionString, conn), SQL_SUCCESS); From 42a36628ec04d9d6ca60f74314cf9cfc536a79e2 Mon Sep 17 00:00:00 2001 From: Sachin Purohit Date: Thu, 22 Oct 2026 20:34:42 +0000 Subject: [PATCH 2/4] fix(bq_driver): deduplicate AllowedProjects and enforce allowlist on exact catalog filters in SQLTables --- .../bq_driver/internal/odbc_sql_tables.cc | 22 +++++++++++++++++-- .../odbc/bq_driver/internal/odbc_sql_tables.h | 3 ++- .../internal/odbc_sql_tables_test.cc | 7 ++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc index ce77665896..9451418d75 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc @@ -18,6 +18,7 @@ #include "google/cloud/odbc/bq_driver/internal/utils.h" #include #include +#include namespace google::cloud::odbc_bq_driver_internal { @@ -115,11 +116,15 @@ std::vector FilterAllowedProjects( std::string const& allowed_projects, std::string const& projects_filter, SQLULEN metadata_id) { std::vector project_ids; + // Track seen project IDs to eliminate duplicates and prevent duplicate + // queries across projects, datasets, and tables, while preserving the + // user-specified order of appearance. + std::unordered_set seen; auto filter_regex = BuildRegex(projects_filter, metadata_id); std::vector allowed_ids = Split(allowed_projects, ","); for (auto& project_id : allowed_ids) { Trim(project_id); - if (project_id.empty()) { + if (project_id.empty() || !seen.insert(project_id).second) { continue; } if ((!metadata_id && projects_filter == kMatchAll) || @@ -521,7 +526,20 @@ StatusRecordOr GetResultSetForTables( std::vector project_list; if (auto project_literal = LiteralFromOdbcPattern(project_filter, metadata_id)) { - project_list = {std::move(*project_literal)}; + auto const& allowed_projects = conn_handle.GetDsn().allowed_projects; + if (!allowed_projects.empty()) { + // When an explicit allowlist is configured, verify that the literal + // project is present in the allowlist. Since LiteralFromOdbcPattern + // extracts an exact identifier (with escapes resolved), match it as + // an exact identifier (SQL_TRUE) so any '_' or '%' in the project ID + // is treated as a literal character rather than an ODBC wildcard. + // This ensures exact catalog filters cannot bypass the AllowedProjects + // restriction. + project_list = + FilterAllowedProjects(allowed_projects, *project_literal, SQL_TRUE); + } else { + project_list = {std::move(*project_literal)}; + } } else { auto projects_status_record_or = GetFilteredProjectIds(bq_client, project_filter, metadata_id, diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h index 0806a841cc..71f6df4f99 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h @@ -58,7 +58,8 @@ odbc_internal::StatusRecord ValidateInputParameters( // Return the project ids from the comma-separated 'allowed_projects' allowlist // that match 'projects_filter', using the same LIKE-pattern / // SQL_ATTR_METADATA_ID semantics as GetFilteredProjectIds. Blank entries are -// ignored and surrounding whitespace is trimmed. Makes no REST calls. +// ignored, surrounding whitespace is trimmed, and duplicate entries are +// deduplicated while preserving order. Makes no REST calls. std::vector FilterAllowedProjects( std::string const& allowed_projects, std::string const& projects_filter, SQLULEN metadata_id); diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc index 9fbeda7ef3..04477d7cdf 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc @@ -255,6 +255,13 @@ TEST(FilterAllowedProjects, BlankEntriesAreIgnored) { ElementsAre("project-1")); } +TEST(FilterAllowedProjects, DuplicateEntriesAreDeduplicatedPreservingOrder) { + EXPECT_THAT(FilterAllowedProjects( + "project-1, project-2, project-1, project-3, project-2", "%", + SQL_FALSE), + ElementsAre("project-1", "project-2", "project-3")); +} + TEST(FilterAllowedProjects, LikePatternFiltersAllowlist) { EXPECT_THAT(FilterAllowedProjects("prod-a,prod-b,dev-c", "prod%", SQL_FALSE), ElementsAre("prod-a", "prod-b")); From 414e4e627ffe014e1aa169c416fecd672e4e83e7 Mon Sep 17 00:00:00 2001 From: Sachin Purohit Date: Thu, 22 Oct 2026 20:43:51 +0000 Subject: [PATCH 3/4] fix(bq_driver): support SSL trust store, PSC, and proxy options in driver form and prompt (#1690) --- .../odbc/bq_driver/internal/driver_form.cc | 39 ++++++++++++++++--- .../odbc/bq_driver/internal/driver_form.h | 4 +- .../cloud/odbc/bq_driver/odbc_connection.cc | 4 ++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/driver_form.cc b/google/cloud/odbc/bq_driver/internal/driver_form.cc index ec4a40d3d3..2c7bc8ea25 100644 --- a/google/cloud/odbc/bq_driver/internal/driver_form.cc +++ b/google/cloud/odbc/bq_driver/internal/driver_form.cc @@ -243,7 +243,8 @@ static Section BuildTestConnectionAttributes( StatusRecordOr DriverForm::GetCatalogAndDataset( std::string const& action, std::string const& key_file_path, - std::string const& oauth_token, std::string const& catalog_name) { + std::string const& oauth_token, std::string const& catalog_name, + std::string const& trusted_certs, bool use_system_trust_store) { OauthMechanism oauth_value; if (oauth_token == "Service Authentication") { oauth_value = OauthMechanism::kServiceAccount; @@ -260,9 +261,27 @@ StatusRecordOr DriverForm::GetCatalogAndDataset( oauth_value = OauthMechanism::kExternalUser; } + ConnProps conn_props; + conn_props.auth_mechanism = oauth_value; + conn_props.credentials_file_path = key_file_path; + conn_props.ssl_credentials.pem_root_certs = + !trusted_certs.empty() ? trusted_certs : GetDefaultPemFile(); + conn_props.ssl_credentials.use_system_trust_store = use_system_trust_store; + + ProxyOptions proxy_form; + conn_props.proxy_options.hostname = proxy_form.GetProxyHost(); + conn_props.proxy_options.port = proxy_form.GetProxyPort(); + conn_props.proxy_options.username = proxy_form.GetProxyUsername(); + conn_props.proxy_options.password = proxy_form.GetProxyPass(); + + AdvanceOptions adv_form; + conn_props.psc = adv_form.GetPrivateServiceConnectUris(); + conn_props.gcd.enable_gcd = + (adv_form.GetEnableGcd() == "1" || adv_form.GetEnableGcd() == "true"); + conn_props.gcd.universe_domain = adv_form.GetUniverseDomain(); + SQLULEN metadata_id = 0; - auto bq_client_ptr = - ODBCBQClient::CreateBQClient({oauth_value, key_file_path}); + auto bq_client_ptr = ODBCBQClient::CreateBQClient(conn_props); if (!bq_client_ptr) { return bq_client_ptr.GetStatusRecord(); } @@ -766,11 +785,13 @@ void EvaluateFields(HWND hwnd) { void PopulateDropdown(HWND h_dataset_box, std::string text, std::string key_file, std::string conn_props, - std::string catalog) { + std::string catalog, std::string trusted_certs = "", + bool use_system_trust_store = false) { SendMessage(h_dataset_box, CB_RESETCONTENT, 0, 0); StatusRecordOr status_record = - DriverForm::GetCatalogAndDataset(text, key_file, conn_props, catalog); + DriverForm::GetCatalogAndDataset(text, key_file, conn_props, catalog, + trusted_certs, use_system_trust_store); if (!status_record.Ok()) { MessageBox(h_dataset_box, status_record.GetStatusRecord().message.c_str(), @@ -827,8 +848,14 @@ StatusRecord HandleDropdown(HWND hwnd, int control_id, char const* field_type, if (has_auth && has_catalog && (is_adc || has_key)) { char const* key_to_use = is_adc ? "" : key_buffer; + char trusted_cert_buffer[256] = {}; + RetrieveFieldText(hwnd, kIdcTrustedCertEdit, trusted_cert_buffer, + sizeof(trusted_cert_buffer)); + bool use_system_trust_store = + (IsDlgButtonChecked(hwnd, kIdcSystemTrustStoreCheckbox) == BST_CHECKED); PopulateDropdown(h_control, field_type, key_to_use, auth_buffer, - catalog_buffer); + catalog_buffer, trusted_cert_buffer, + use_system_trust_store); return StatusRecord::Ok(); } diff --git a/google/cloud/odbc/bq_driver/internal/driver_form.h b/google/cloud/odbc/bq_driver/internal/driver_form.h index fe0e052deb..67b7bd212c 100644 --- a/google/cloud/odbc/bq_driver/internal/driver_form.h +++ b/google/cloud/odbc/bq_driver/internal/driver_form.h @@ -102,7 +102,9 @@ class DriverForm { static odbc_internal::StatusRecordOr GetCatalogAndDataset( std::string const& action, std::string const& key_file_path, - std::string const& oauth_token, std::string const& catalog_name = ""); + std::string const& oauth_token, std::string const& catalog_name = "", + std::string const& trusted_certs = "", + bool use_system_trust_store = false); inline void SetLogTraceValues(Section const& attributes_map) { log_trace_dialog_.SetValues(attributes_map); diff --git a/google/cloud/odbc/bq_driver/odbc_connection.cc b/google/cloud/odbc/bq_driver/odbc_connection.cc index 290839a608..d65fde65c2 100644 --- a/google/cloud/odbc/bq_driver/odbc_connection.cc +++ b/google/cloud/odbc/bq_driver/odbc_connection.cc @@ -212,6 +212,10 @@ SQLRETURN HandleDriverPrompt(std::string& conn_string, SQLHWND window_handle, {"ENABLESESSION", form.GetAdvanceOptions()->GetEnableSession()}, {"ADDITIONALPROJECTS", form.GetAdvanceOptions()->GetAdditionalProjects()}, {"ALLOWEDPROJECTS", form.GetAdvanceOptions()->GetAllowedProjects()}, + {"PRIVATESERVICECONNECTURIS", + form.GetAdvanceOptions()->GetPrivateServiceConnectUris()}, + {"ENABLEGCD", form.GetAdvanceOptions()->GetEnableGcd()}, + {"UNIVERSEDOMAIN", form.GetAdvanceOptions()->GetUniverseDomain()}, }; handle_ref->SetUp(dsn_section, form.GetDSN()); From 3fcffb3f6992830085c3ab6335515b086fbccb07 Mon Sep 17 00:00:00 2001 From: Sachin Purohit Date: Thu, 22 Oct 2026 21:20:24 +0000 Subject: [PATCH 4/4] test(integration): guard SQLTables_AllowedProjects under BQ_DRIVER_INTEGRATION_TESTS --- .../odbc/integration_tests/odbc_driver_tests/catalog_test.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc index 09797dd860..1f80ebbc7b 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc @@ -506,6 +506,7 @@ error } */ +#ifdef BQ_DRIVER_INTEGRATION_TESTS TEST(CatalogTest, SQLTables_AllowedProjects) { // First learn every project the test principal can reach, so the allow-listed // run below can be compared against it. @@ -562,6 +563,7 @@ TEST(CatalogTest, SQLTables_AllowedProjects) { EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); } +#endif // BQ_DRIVER_INTEGRATION_TESTS TEST(CatalogTest, SQLTables_AllDatasets) { auto conn = std::make_shared();