diff --git a/.github/scripts/run_iam_integration_tests.sh b/.github/scripts/run_iam_integration_tests.sh index 43cf89f56c..516ee6700d 100755 --- a/.github/scripts/run_iam_integration_tests.sh +++ b/.github/scripts/run_iam_integration_tests.sh @@ -2,7 +2,7 @@ set -euo pipefail -IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' +IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility|OdbcAuthentication)\.' IAM_CONTAINER_NAME="${IAM_CONTAINER_NAME:-ydb-iam}" IAM_CTEST_JOBS="${IAM_CTEST_JOBS:-2}" IAM_READY_ATTEMPTS="${IAM_READY_ATTEMPTS:-60}" @@ -16,7 +16,7 @@ cleanup_iam() { wait_for_iam_ydb() { for _ in $(seq 1 "${IAM_READY_ATTEMPTS}"); do if docker exec -e "YDB_TOKEN=${IAM_TOKEN}" "${IAM_CONTAINER_NAME}" /ydb \ - --endpoint grpc://localhost:2136 \ + --endpoint grpc://localhost:2236 \ --database /local \ sql -s 'select 1' >/dev/null 2>&1; then return 0 @@ -29,12 +29,22 @@ wait_for_iam_ydb() { return 1 } +provision_odbc_static_user() { + docker exec -e "YDB_TOKEN=${IAM_TOKEN}" "${IAM_CONTAINER_NAME}" /ydb \ + --endpoint grpc://localhost:2236 \ + --database /local \ + sql -s "CREATE USER odbcauth PASSWORD '12345678'" +} + trap cleanup_iam EXIT cleanup_iam docker run -d --name "${IAM_CONTAINER_NAME}" --hostname localhost \ - -p 2235:2135 -p 2236:2136 -p 28765:8765 \ + -p 2235:2235 -p 2236:2236 -p 28765:28765 \ -v /tmp/ydb_iam_certs:/ydb_certs \ + -e GRPC_TLS_PORT=2235 \ + -e GRPC_PORT=2236 \ + -e MON_PORT=28765 \ -e YDB_USE_IN_MEMORY_PDISKS=true \ -e YDB_TABLE_ENABLE_PREPARED_DDL=true \ -e YDB_ENFORCE_USER_TOKEN_REQUIREMENT=true \ @@ -42,6 +52,8 @@ docker run -d --name "${IAM_CONTAINER_NAME}" --hostname localhost \ ghcr.io/ydb-platform/local-ydb:trunk wait_for_iam_ydb +provision_odbc_static_user YDB_ENDPOINT=localhost:2236 YDB_DATABASE=/local \ +YDB_ODBC_STATIC_USER=odbcauth YDB_ODBC_STATIC_PASSWORD=12345678 \ ctest -j"${IAM_CTEST_JOBS}" --test-dir build -R "${IAM_REGEX}" --output-on-failure diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c6d978e0df..cd95e56c85 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -77,7 +77,7 @@ jobs: run: | set -euo pipefail - IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' + IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility|OdbcAuthentication)\.' FLAKY_REGEX='(ManyMessages|DiscoveryHang|DescribeHang)' EXCLUDE_REGEX="${IAM_REGEX}|${FLAKY_REGEX}" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0a3ad051f1..b20b416752 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -142,7 +142,7 @@ jobs: if: github.event_name != 'pull_request' || github.base_ref == 'main' shell: bash run: | - IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility)\.' + IAM_REGEX='^(DriverAuth|TMetadataFixture|TJwtIamFixture|TOAuthIamFixture|OAuth_WithFacility|OdbcAuthentication)\.' YDB_VERSION=${{ matrix.ydb-version }} ctest -j2 --preset integration \ -E "${IAM_REGEX}" --output-on-failure @@ -157,7 +157,14 @@ jobs: shell: bash run: | YDB_VERSION=${{ matrix.ydb-version }} \ - ctest --test-dir build/odbc/tests/integration -j2 --output-on-failure + ctest --test-dir build/odbc/tests/integration -j2 \ + -E '^OdbcAuthentication\.' --output-on-failure + + case '${{ matrix.ydb-version }}' in + 25.1|trunk) + ./.github/scripts/run_iam_integration_tests.sh + ;; + esac test-install: if: github.event_name != 'pull_request' || github.base_ref == 'main' diff --git a/odbc/CMakeLists.txt b/odbc/CMakeLists.txt index c8c10f7d05..f5416b7b60 100644 --- a/odbc/CMakeLists.txt +++ b/odbc/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(ydb-odbc SHARED src/utils/error_manager.cpp src/odbc_driver.cpp src/connection_attr.cpp + src/connection_config.cpp src/connection.cpp src/statement_attr.cpp src/statement.cpp @@ -34,6 +35,9 @@ target_link_libraries(ydb-odbc YDB-CPP-SDK::Table YDB-CPP-SDK::Scheme YDB-CPP-SDK::Driver + YDB-CPP-SDK::Credentials + YDB-CPP-SDK::Helpers + YDB-CPP-SDK::Iam ODBC::ODBC odbcinst ) diff --git a/odbc/README.md b/odbc/README.md index 56219dbea3..b4a9b63483 100644 --- a/odbc/README.md +++ b/odbc/README.md @@ -60,6 +60,52 @@ YDB=YDB ODBC Driver Driver=YDB Server=localhost:2136 Database=/local +AuthMode=Anonymous +``` + +`SQLDriverConnect` may also combine a DSN with explicit attributes. Values in +the connection string take precedence over values from the DSN. The user name +and password passed to `SQLConnect` take precedence over `User` and `Password` +in the DSN. + +### Connection attributes + +| Attribute | Meaning | +| --- | --- | +| `Endpoint` | YDB endpoint. `Server` is an alias. A `grpc://` prefix forces a plaintext connection; `grpcs://` enables TLS. | +| `Database` | YDB database path. | +| `DSN` | DSN section to load before applying the remaining connection-string attributes. | +| `AuthMode` | `Anonymous`, `Token`, `Static`, `Metadata`, `ServiceAccount`, `OAuth2`, or `Environment`. Values are case-insensitive. | +| `Token` | Access token for `Token` mode. `AccessToken` is an alias. | +| `User`, `Password` | Credentials for `Static` mode. `UID` and `PWD` are aliases. | +| `MetadataHost`, `MetadataPort` | Optional metadata service address for `Metadata` mode. | +| `ServiceAccountKeyFile` | Path to a service-account JSON key for `ServiceAccount` mode. `SaFile` is an alias. | +| `OAuth2KeyFile` | Path to an OAuth 2.0 token-exchange configuration file for `OAuth2` mode. | +| `IamEndpoint` | IAM gRPC endpoint for service-account authentication, or HTTP token endpoint override for OAuth 2.0 token exchange. | +| `RootCertificate` | Path to a PEM root-certificate file. `CaFile` is an alias. | +| `ClientCertificate`, `ClientPrivateKey` | Paths to the PEM client certificate and private key. They must be specified together. | + +If `AuthMode` is omitted, the driver infers it from exactly one credential +family (`Token`, static user/password, metadata settings, service-account key, +or OAuth 2.0 key). With no credential attributes it uses `Anonymous`. Conflicting +families and incomplete credentials are rejected with SQLSTATE `28000`. +`Environment` uses the SDK's standard `YDB_*_CREDENTIALS` variables. + +Unrecognized connection-string attributes are ignored after reporting SQLSTATE +`01S00`; `SQLDriverConnect` completes with `SQL_SUCCESS_WITH_INFO`. This allows +ODBC applications to supply tool-specific attributes such as `APP` or `WSID`. + +Certificate attributes contain file paths, not inline PEM. The driver reads the +files while establishing the ODBC connection. Supplying certificates enables +TLS; certificates cannot be combined with an explicitly plaintext `grpc://` +endpoint. + +Examples: + +```text +Driver=YDB;Endpoint=grpcs://ydb.example.net:2135;Database=/production;AuthMode=Token;Token=... +DSN=YDB;AuthMode=Static;UID=app;PWD=secret +Driver=YDB;Endpoint=localhost:2136;Database=/local;AuthMode=ServiceAccount;SaFile=/run/secrets/sa.json;IamEndpoint=grpc://localhost:4284 ``` ## Usage @@ -102,6 +148,11 @@ SQLCHAR connStr[] = "Driver=YDB;Endpoint=localhost:2136;Database=/local"; SQLDriverConnect(dbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); ``` +For `INSERT`, `UPDATE`, `DELETE`, `UPSERT`, and `REPLACE`, `SQLRowCount` +returns the affected-row count reported by YDB query statistics. Counts from +executed parameter-array entries are summed; ignored entries are not counted. +For statements without an applicable count, it returns `-1`. + ## Parameters `?` placeholders are rewritten to `$p1`, `$p2`, ... with auto-generated `DECLARE $pN AS ?;` diff --git a/odbc/odbc.ini b/odbc/odbc.ini index a1ba3c951c..f7334b046f 100644 --- a/odbc/odbc.ini +++ b/odbc/odbc.ini @@ -6,4 +6,4 @@ Driver=YDB Description=YDB Database Connection Server=localhost:2136 Database=/local -AuthMode=none +AuthMode=Anonymous diff --git a/odbc/src/connection.cpp b/odbc/src/connection.cpp index fca372daaa..b055b965d2 100644 --- a/odbc/src/connection.cpp +++ b/odbc/src/connection.cpp @@ -1,11 +1,9 @@ #include "connection.h" #include "statement.h" -#include "utils/util.h" #include #include -#include #include #include #include @@ -13,8 +11,6 @@ #include #include -#include - namespace NYdb::NOdbc { TConnection::~TConnection() { @@ -27,53 +23,63 @@ void TConnection::DestroyYdbState() { Ydb_.reset(); } -SQLRETURN TConnection::DriverConnect(const std::string& connectionString) { - const std::map params = ParseConnectionString(connectionString); - Endpoint_ = params.contains("Server") ? params.at("Server") : params.contains("Endpoint") ? params.at("Endpoint") : ""; - Database_ = params.contains("Database") ? params.at("Database") : ""; - DataSourceName_ = params.contains("DSN") ? params.at("DSN") : ""; - - if (Endpoint_.empty() || Database_.empty()) { - throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in connection string"); +SQLRETURN TConnection::DriverConnect(std::string_view connectionString) { + std::vector ignoredAttributes; + TConnectionParameters explicitParameters = + ParseAndNormalizeConnectionString(connectionString, ignoredAttributes); + const auto dsnIt = explicitParameters.find("DSN"); + TConnectionParameters parameters; + if (dsnIt != explicitParameters.end() && !dsnIt->second.empty()) { + parameters = ReadDsnParameters(dsnIt->second); + } + OverlayConnectionParameters(parameters, explicitParameters); + ApplyResolvedSettings(ResolveConnectionSettings(std::move(parameters))); + + if (!ignoredAttributes.empty()) { + std::string message = ignoredAttributes.size() == 1 + ? "Invalid connection string attribute ignored: " + : "Invalid connection string attributes ignored: "; + for (size_t i = 0; i < ignoredAttributes.size(); ++i) { + if (i != 0) { + message += ", "; + } + message += ignoredAttributes[i]; + } + return AddError("01S00", 0, message, SQL_SUCCESS_WITH_INFO); } - - TConnectionAttributes::NormalizeCatalogPath(Database_); - RecreateYdbClients(); - Attributes_.SetCurrentCatalog(Database_); return SQL_SUCCESS; } -SQLRETURN TConnection::Connect(const std::string& serverName, - const std::string& userName, - const std::string& auth) { - DataSourceName_ = serverName; - - char endpoint[256] = {0}; - char server[256] = {0}; - char database[256] = {0}; - - SQLGetPrivateProfileString(serverName.c_str(), "Endpoint", "", endpoint, sizeof(endpoint), nullptr); - SQLGetPrivateProfileString(serverName.c_str(), "Server", "", server, sizeof(server), nullptr); - SQLGetPrivateProfileString(serverName.c_str(), "Database", "", database, sizeof(database), nullptr); - - Endpoint_ = endpoint[0] ? endpoint : server; - Database_ = database; - - if (Endpoint_.empty() || Database_.empty()) { - throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database in DSN"); +SQLRETURN TConnection::Connect(std::string_view serverName, + std::string_view userName, + std::string_view auth) { + TConnectionParameters parameters = ReadDsnParameters(serverName); + if (!userName.empty() || !auth.empty()) { + for (const std::string_view key : { + "Token", "MetadataHost", "MetadataPort", "ServiceAccountKeyFile", + "OAuth2KeyFile", "IamEndpoint"}) { + parameters.erase(std::string(key)); + } + parameters["AuthMode"] = "Static"; } - - TConnectionAttributes::NormalizeCatalogPath(Database_); - RecreateYdbClients(); - Attributes_.SetCurrentCatalog(Database_); + if (!userName.empty()) { + parameters["User"] = std::string(userName); + } + if (!auth.empty()) { + parameters["Password"] = std::string(auth); + } + ApplyResolvedSettings(ResolveConnectionSettings(std::move(parameters), std::string(serverName))); return SQL_SUCCESS; } SQLRETURN TConnection::Disconnect() { DestroyYdbState(); + DriverConfig_.reset(); DbmsVersionCache_.reset(); + Endpoint_.clear(); + Database_.clear(); DataSourceName_.clear(); return SQL_SUCCESS; } @@ -258,15 +264,34 @@ const std::string& TConnection::GetDbmsVersion() { } void TConnection::RecreateYdbClients() { + if (!DriverConfig_) { + throw TOdbcException("08003", 0, "Connection configuration is not available"); + } DestroyYdbState(); DbmsVersionCache_.reset(); - Ydb_.emplace(Endpoint_, Database_); + Ydb_.emplace(*DriverConfig_); } -void TConnection::RebindToDatabase(const std::string& newDatabase) { - std::string db = newDatabase; +void TConnection::ApplyResolvedSettings(TResolvedConnectionSettings&& settings) { + TConnectionAttributes::NormalizeCatalogPath(settings.Database); + settings.DriverConfig.SetDatabase(settings.Database); + + Endpoint_ = std::move(settings.Endpoint); + Database_ = std::move(settings.Database); + DataSourceName_ = std::move(settings.DataSourceName); + DriverConfig_.emplace(std::move(settings.DriverConfig)); + RecreateYdbClients(); + Attributes_.SetCurrentCatalog(Database_); +} + +void TConnection::RebindToDatabase(std::string_view newDatabase) { + if (!DriverConfig_) { + throw TOdbcException("08003", 0, "Connection configuration is not available"); + } + std::string db(newDatabase); TConnectionAttributes::NormalizeCatalogPath(db); Database_ = std::move(db); + DriverConfig_->SetDatabase(Database_); Attributes_.SetCurrentCatalog(Database_); RecreateYdbClients(); } diff --git a/odbc/src/connection.h b/odbc/src/connection.h index ae921ecd1a..74437b73eb 100644 --- a/odbc/src/connection.h +++ b/odbc/src/connection.h @@ -2,6 +2,7 @@ #include "environment.h" #include "connection_attr.h" +#include "connection_config.h" #include "utils/error_manager.h" #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include @@ -32,8 +34,8 @@ class TConnection : public TErrorManager { NScheme::TSchemeClient SchemeClient; NTable::TTableClient TableClient; - TYdbState(const std::string& endpoint, const std::string& database) - : Driver(TDriverConfig().SetEndpoint(endpoint).SetDatabase(database)) + explicit TYdbState(const TDriverConfig& config) + : Driver(config) , QueryClient(Driver) , SchemeClient(Driver) , TableClient(Driver) @@ -45,6 +47,7 @@ class TConnection : public TErrorManager { }; std::optional Ydb_; + std::optional DriverConfig_; std::optional Tx_; std::optional QuerySession_; @@ -59,16 +62,17 @@ class TConnection : public TErrorManager { std::unordered_set Descriptors_; void DestroyYdbState(); + void ApplyResolvedSettings(TResolvedConnectionSettings&& settings); void RecreateYdbClients(); - void RebindToDatabase(const std::string& newDatabase); + void RebindToDatabase(std::string_view newDatabase); public: ~TConnection(); - SQLRETURN Connect(const std::string& serverName, - const std::string& userName, - const std::string& auth); + SQLRETURN Connect(std::string_view serverName, + std::string_view userName, + std::string_view auth); - SQLRETURN DriverConnect(const std::string& connectionString); + SQLRETURN DriverConnect(std::string_view connectionString); SQLRETURN Disconnect(); std::unique_ptr CreateStatement(); diff --git a/odbc/src/connection_config.cpp b/odbc/src/connection_config.cpp new file mode 100644 index 0000000000..db1735def5 --- /dev/null +++ b/odbc/src/connection_config.cpp @@ -0,0 +1,435 @@ +#include "connection_config.h" + +#include "utils/error_manager.h" +#include "utils/util.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NYdb::NOdbc { + +namespace { + +std::string ToLower(std::string_view value) { + std::string result(value); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return result; +} + +std::optional CanonicalKey(std::string_view key) { + const std::string lower = ToLower(key); + if (lower == "driver") return "Driver"; + if (lower == "description") return "Description"; + if (lower == "dsn") return "DSN"; + if (lower == "server" || lower == "endpoint") return "Endpoint"; + if (lower == "database") return "Database"; + if (lower == "authmode") return "AuthMode"; + if (lower == "token" || lower == "accesstoken") return "Token"; + if (lower == "user" || lower == "uid") return "User"; + if (lower == "password" || lower == "pwd") return "Password"; + if (lower == "metadatahost") return "MetadataHost"; + if (lower == "metadataport") return "MetadataPort"; + if (lower == "serviceaccountkeyfile" || lower == "safile") return "ServiceAccountKeyFile"; + if (lower == "oauth2keyfile") return "OAuth2KeyFile"; + if (lower == "iamendpoint") return "IamEndpoint"; + if (lower == "rootcertificate" || lower == "cafile") return "RootCertificate"; + if (lower == "clientcertificate") return "ClientCertificate"; + if (lower == "clientprivatekey") return "ClientPrivateKey"; + return std::nullopt; +} + +[[noreturn]] void ThrowInvalidAttribute(std::string_view attribute, std::string_view detail) { + throw TOdbcException("HY024", 0, "Invalid connection string attribute " + + std::string(attribute) + ": " + std::string(detail)); +} + +bool Has(const TConnectionParameters& parameters, std::string_view key) { + return parameters.contains(std::string(key)); +} + +std::string_view Get(const TConnectionParameters& parameters, std::string_view key) { + const auto it = parameters.find(std::string(key)); + return it == parameters.end() ? std::string_view{} : std::string_view(it->second); +} + +std::string_view RequireNonEmpty( + const TConnectionParameters& parameters, + std::string_view key, + std::string_view authMode) +{ + const auto value = Get(parameters, key); + if (value.empty()) { + throw TOdbcException("28000", 0, std::string(authMode) + + " authentication requires " + std::string(key)); + } + return value; +} + +std::string ReadDsnValue(std::string_view dsn, std::string_view key) { + const std::string dsnName(dsn); + const std::string attribute(key); + std::vector buffer(256); + while (buffer.size() <= 1024 * 1024) { + const int length = SQLGetPrivateProfileString( + dsnName.c_str(), attribute.c_str(), "", buffer.data(), static_cast(buffer.size()), nullptr); + if (length < 0) { + return {}; + } + if (static_cast(length) + 1 < buffer.size()) { + return std::string(buffer.data(), static_cast(length)); + } + buffer.resize(buffer.size() * 2); + } + throw TOdbcException("08001", 0, "DSN attribute is too large: " + attribute); +} + +std::string ReadFile(std::string_view attribute, std::string_view path) { + const std::string pathString(path); + std::ifstream input(pathString, std::ios::binary); + if (!input) { + throw TOdbcException("08001", 0, "Unable to read " + std::string(attribute) + " file: " + pathString); + } + std::string content{ + std::istreambuf_iterator(input), + std::istreambuf_iterator()}; + if (content.empty()) { + throw TOdbcException("08001", 0, std::string(attribute) + " file is empty: " + pathString); + } + return content; +} + +struct TEndpointSettings { + std::string Endpoint; + bool Secure = false; + bool ExplicitlyInsecure = false; +}; + +TEndpointSettings ParseYdbEndpoint(std::string_view value) { + constexpr std::string_view grpc = "grpc://"; + constexpr std::string_view grpcs = "grpcs://"; + if (value.starts_with(grpc)) { + return {std::string(value.substr(grpc.size())), false, true}; + } + if (value.starts_with(grpcs)) { + return {std::string(value.substr(grpcs.size())), true, false}; + } + if (value.find("://") != std::string::npos) { + ThrowInvalidAttribute("Endpoint", "only grpc:// and grpcs:// protocols are supported"); + } + return {std::string(value), false, false}; +} + +void ApplyIamEndpoint(TIamJwtFilename& params, std::string_view value) { + if (value.empty()) { + return; + } + constexpr std::string_view grpc = "grpc://"; + constexpr std::string_view grpcs = "grpcs://"; + if (value.starts_with(grpc)) { + params.Endpoint = std::string(value.substr(grpc.size())); + params.EnableSsl = false; + } else if (value.starts_with(grpcs)) { + params.Endpoint = std::string(value.substr(grpcs.size())); + params.EnableSsl = true; + } else if (value.find("://") != std::string::npos) { + ThrowInvalidAttribute("IamEndpoint", "service-account IAM supports grpc:// and grpcs://"); + } else { + params.Endpoint = std::string(value); + params.EnableSsl = true; + } +} + +uint32_t ParseMetadataPort(std::string_view value) { + if (value.empty()) { + ThrowInvalidAttribute("MetadataPort", "value is empty"); + } + uint32_t port = 0; + const auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), port); + if (error != std::errc() || end != value.data() + value.size() || port == 0 || port > 65535) { + ThrowInvalidAttribute("MetadataPort", "expected an integer from 1 to 65535"); + } + return port; +} + +EAuthenticationMode ParseAuthMode(std::string_view value) { + const std::string mode = ToLower(value); + if (mode == "anonymous") return EAuthenticationMode::Anonymous; + if (mode == "token") return EAuthenticationMode::Token; + if (mode == "static") return EAuthenticationMode::Static; + if (mode == "metadata") return EAuthenticationMode::Metadata; + if (mode == "serviceaccount") return EAuthenticationMode::ServiceAccount; + if (mode == "oauth2") return EAuthenticationMode::OAuth2; + if (mode == "environment") return EAuthenticationMode::Environment; + throw TOdbcException("28000", 0, "Unknown authentication mode: " + std::string(value)); +} + +EAuthenticationMode ResolveAuthMode(const TConnectionParameters& parameters) { + const bool token = Has(parameters, "Token"); + const bool staticCredentials = Has(parameters, "User") || Has(parameters, "Password"); + const bool metadata = Has(parameters, "MetadataHost") || Has(parameters, "MetadataPort"); + const bool serviceAccount = Has(parameters, "ServiceAccountKeyFile"); + const bool oauth2 = Has(parameters, "OAuth2KeyFile"); + const size_t familyCount = static_cast(token) + static_cast(staticCredentials) + + static_cast(metadata) + static_cast(serviceAccount) + static_cast(oauth2); + + EAuthenticationMode mode; + if (Has(parameters, "AuthMode")) { + mode = ParseAuthMode(Get(parameters, "AuthMode")); + } else if (familyCount == 0) { + if (Has(parameters, "IamEndpoint")) { + throw TOdbcException("28000", 0, "IamEndpoint requires ServiceAccount or OAuth2 authentication"); + } + mode = EAuthenticationMode::Anonymous; + } else if (familyCount > 1) { + throw TOdbcException("28000", 0, "Authentication mode is ambiguous"); + } else if (token) { + mode = EAuthenticationMode::Token; + } else if (staticCredentials) { + mode = EAuthenticationMode::Static; + } else if (metadata) { + mode = EAuthenticationMode::Metadata; + } else if (serviceAccount) { + mode = EAuthenticationMode::ServiceAccount; + } else { + mode = EAuthenticationMode::OAuth2; + } + + const bool modeMatchesFamily = + (mode == EAuthenticationMode::Token && token && familyCount == 1) || + (mode == EAuthenticationMode::Static && staticCredentials && familyCount == 1) || + (mode == EAuthenticationMode::Metadata && (!familyCount || (metadata && familyCount == 1))) || + (mode == EAuthenticationMode::ServiceAccount && serviceAccount && familyCount == 1) || + (mode == EAuthenticationMode::OAuth2 && oauth2 && familyCount == 1) || + ((mode == EAuthenticationMode::Anonymous || mode == EAuthenticationMode::Environment) && familyCount == 0); + if (!modeMatchesFamily) { + throw TOdbcException("28000", 0, "Credential attributes conflict with the selected authentication mode"); + } + if (Has(parameters, "IamEndpoint") && mode != EAuthenticationMode::ServiceAccount && + mode != EAuthenticationMode::OAuth2) { + throw TOdbcException("28000", 0, "IamEndpoint is valid only for ServiceAccount or OAuth2 authentication"); + } + return mode; +} + +} // namespace + +TConnectionParameters ParseAndNormalizeConnectionString( + std::string_view connectionString, + std::vector& ignoredAttributes) +{ + TConnectionParameters parameters; + for (const auto& [key, value] : ParseConnectionStringEntries(connectionString)) { + const auto canonical = CanonicalKey(key); + if (!canonical) { + ignoredAttributes.push_back(key); + continue; + } + parameters[*canonical] = value; + } + return parameters; +} + +TConnectionParameters ReadDsnParameters(std::string_view dsn) { + TConnectionParameters parameters; + // Aliases are read first so the canonical spelling wins inside a DSN. + static constexpr std::array keys = { + "Server", "UID", "PWD", "AccessToken", "SaFile", "CaFile", + "Driver", "Description", "Endpoint", "Database", "AuthMode", "Token", + "User", "Password", "MetadataHost", "MetadataPort", "ServiceAccountKeyFile", + "OAuth2KeyFile", "IamEndpoint", "RootCertificate", "ClientCertificate", + "ClientPrivateKey", "DSN"}; + for (const char* key : keys) { + std::string value = ReadDsnValue(dsn, key); + if (!value.empty()) { + parameters[*CanonicalKey(key)] = std::move(value); + } + } + return parameters; +} + +void OverlayConnectionParameters(TConnectionParameters& destination, const TConnectionParameters& source) { + std::optional selectedMode; + if (Has(source, "AuthMode")) { + selectedMode = ParseAuthMode(Get(source, "AuthMode")); + } else { + const bool token = Has(source, "Token"); + const bool staticCredentials = Has(source, "User") || Has(source, "Password"); + const bool metadata = Has(source, "MetadataHost") || Has(source, "MetadataPort"); + const bool serviceAccount = Has(source, "ServiceAccountKeyFile"); + const bool oauth2 = Has(source, "OAuth2KeyFile"); + const size_t familyCount = static_cast(token) + static_cast(staticCredentials) + + static_cast(metadata) + static_cast(serviceAccount) + static_cast(oauth2); + if (familyCount == 1) { + selectedMode = token ? EAuthenticationMode::Token + : staticCredentials ? EAuthenticationMode::Static + : metadata ? EAuthenticationMode::Metadata + : serviceAccount ? EAuthenticationMode::ServiceAccount + : EAuthenticationMode::OAuth2; + destination.erase("AuthMode"); + } + } + + if (selectedMode) { + const auto belongsToSelectedMode = [selectedMode](std::string_view key) { + switch (*selectedMode) { + case EAuthenticationMode::Token: + return key == "Token"; + case EAuthenticationMode::Static: + return key == "User" || key == "Password"; + case EAuthenticationMode::Metadata: + return key == "MetadataHost" || key == "MetadataPort"; + case EAuthenticationMode::ServiceAccount: + return key == "ServiceAccountKeyFile" || key == "IamEndpoint"; + case EAuthenticationMode::OAuth2: + return key == "OAuth2KeyFile" || key == "IamEndpoint"; + case EAuthenticationMode::Anonymous: + case EAuthenticationMode::Environment: + return false; + } + return false; + }; + for (const std::string_view key : { + "Token", "User", "Password", "MetadataHost", "MetadataPort", + "ServiceAccountKeyFile", "OAuth2KeyFile", "IamEndpoint"}) { + if (!belongsToSelectedMode(key)) { + destination.erase(std::string(key)); + } + } + } + + for (const auto& [key, value] : source) { + destination[key] = value; + } +} + +TResolvedConnectionSettings ResolveConnectionSettings( + TConnectionParameters parameters, + std::string dataSourceName) +{ + const std::string endpointValue(Get(parameters, "Endpoint")); + const std::string database(Get(parameters, "Database")); + if (endpointValue.empty() || database.empty()) { + throw TOdbcException("08001", 0, "Missing Endpoint (or Server) or Database"); + } + + const TEndpointSettings endpoint = ParseYdbEndpoint(endpointValue); + const bool hasRoot = Has(parameters, "RootCertificate"); + const bool hasClientCert = Has(parameters, "ClientCertificate"); + const bool hasClientKey = Has(parameters, "ClientPrivateKey"); + if (hasClientCert != hasClientKey) { + throw TOdbcException("08001", 0, + "ClientCertificate and ClientPrivateKey must be specified together"); + } + const bool hasTlsFiles = hasRoot || hasClientCert; + if (endpoint.ExplicitlyInsecure && hasTlsFiles) { + ThrowInvalidAttribute("Endpoint", "grpc:// cannot be combined with TLS certificate attributes"); + } + + const EAuthenticationMode authMode = ResolveAuthMode(parameters); + TDriverConfig driverConfig = authMode == EAuthenticationMode::Environment + ? CreateFromEnvironment() + : TDriverConfig(); + driverConfig.SetEndpoint(endpoint.Endpoint).SetDatabase(database); + + switch (authMode) { + case EAuthenticationMode::Anonymous: + driverConfig.SetCredentialsProviderFactory(CreateInsecureCredentialsProviderFactory()); + break; + case EAuthenticationMode::Token: + driverConfig.SetCredentialsProviderFactory(CreateOAuthCredentialsProviderFactory( + std::string(RequireNonEmpty(parameters, "Token", "Token")))); + break; + case EAuthenticationMode::Static: + driverConfig.SetCredentialsProviderFactory(CreateLoginCredentialsProviderFactory({ + .User = std::string(RequireNonEmpty(parameters, "User", "Static")), + .Password = std::string(RequireNonEmpty(parameters, "Password", "Static")), + })); + break; + case EAuthenticationMode::Metadata: { + TIamHost params; + if (Has(parameters, "MetadataHost")) { + params.Host = std::string(RequireNonEmpty(parameters, "MetadataHost", "Metadata")); + } + if (Has(parameters, "MetadataPort")) { + params.Port = ParseMetadataPort(Get(parameters, "MetadataPort")); + } + driverConfig.SetCredentialsProviderFactory(CreateIamCredentialsProviderFactory(params)); + break; + } + case EAuthenticationMode::ServiceAccount: { + TIamJwtFilename params; + params.JwtFilename = std::string(RequireNonEmpty(parameters, "ServiceAccountKeyFile", "ServiceAccount")); + ApplyIamEndpoint(params, Get(parameters, "IamEndpoint")); + try { + driverConfig.SetCredentialsProviderFactory(CreateIamJwtFileCredentialsProviderFactory(params)); + } catch (const std::exception& ex) { + throw TOdbcException("08001", 0, + "Unable to load ServiceAccountKeyFile " + params.JwtFilename + ": " + ex.what()); + } + break; + } + case EAuthenticationMode::OAuth2: { + const std::string path(RequireNonEmpty(parameters, "OAuth2KeyFile", "OAuth2")); + try { + driverConfig.SetCredentialsProviderFactory( + CreateOauth2TokenExchangeFileCredentialsProviderFactory( + path, std::string(Get(parameters, "IamEndpoint")))); + } catch (const std::exception& ex) { + throw TOdbcException("08001", 0, + "Unable to load OAuth2KeyFile " + path + ": " + ex.what()); + } + break; + } + case EAuthenticationMode::Environment: + break; + } + + const bool secure = endpoint.Secure || hasTlsFiles; + std::string rootPem; + std::string clientCertPem; + std::string clientKeyPem; + if (hasRoot) { + rootPem = ReadFile("RootCertificate", Get(parameters, "RootCertificate")); + } + if (hasClientCert) { + clientCertPem = ReadFile("ClientCertificate", Get(parameters, "ClientCertificate")); + clientKeyPem = ReadFile("ClientPrivateKey", Get(parameters, "ClientPrivateKey")); + } + if (secure) { + driverConfig.UseSecureConnection(rootPem); + } + if (hasClientCert) { + driverConfig.UseClientCertificate(clientCertPem, clientKeyPem); + } + + if (dataSourceName.empty()) { + dataSourceName = std::string(Get(parameters, "DSN")); + } + return { + .Endpoint = endpoint.Endpoint, + .Database = database, + .DataSourceName = std::move(dataSourceName), + .DriverConfig = std::move(driverConfig), + }; +} + +} // namespace NYdb::NOdbc diff --git a/odbc/src/connection_config.h b/odbc/src/connection_config.h new file mode 100644 index 0000000000..2caa640e50 --- /dev/null +++ b/odbc/src/connection_config.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace NYdb::NOdbc { + +enum class EAuthenticationMode { + Anonymous, + Token, + Static, + Metadata, + ServiceAccount, + OAuth2, + Environment, +}; + +using TConnectionParameters = std::map; + +struct TResolvedConnectionSettings { + std::string Endpoint; + std::string Database; + std::string DataSourceName; + TDriverConfig DriverConfig; +}; + +TConnectionParameters ParseAndNormalizeConnectionString( + std::string_view connectionString, + std::vector& ignoredAttributes); +TConnectionParameters ReadDsnParameters(std::string_view dsn); +void OverlayConnectionParameters(TConnectionParameters& destination, const TConnectionParameters& source); + +TResolvedConnectionSettings ResolveConnectionSettings( + TConnectionParameters parameters, + std::string dataSourceName = {}); + +} // namespace NYdb::NOdbc diff --git a/odbc/src/statement.cpp b/odbc/src/statement.cpp index e56425524e..419d8f5378 100644 --- a/odbc/src/statement.cpp +++ b/odbc/src/statement.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace NYdb::NOdbc { @@ -70,7 +71,7 @@ namespace { } bool StartsWithStatement( - const std::string& queryText, + std::string_view queryText, std::initializer_list keywords) { size_t i = 0; while (i < queryText.size()) { @@ -97,13 +98,43 @@ namespace { const size_t remaining = queryText.size() - i; for (const std::string_view keyword : keywords) { if (StartsWithPrefix( - queryText.c_str() + i, remaining, keyword.data(), keyword.size())) { + queryText.data() + i, remaining, keyword.data(), keyword.size())) { return true; } } return false; } + std::optional ExtractAffectedRows(const NQuery::TExecuteQueryResult& result) { + const auto& stats = result.GetStats(); + if (!stats) { + return std::nullopt; + } + + const uint64_t maxSqlLen = static_cast(std::numeric_limits::max()); + uint64_t affectedRows = 0; + bool hasTableAccess = false; + for (const auto& phase : stats->GetQueryPhases()) { + for (const auto& table : phase.GetTableAccess()) { + hasTableAccess = true; + const uint64_t updates = table.GetUpdates().GetRows(); + const uint64_t deletes = table.GetDeletes().GetRows(); + if (updates > maxSqlLen - affectedRows) { + return std::nullopt; + } + affectedRows += updates; + if (deletes > maxSqlLen - affectedRows) { + return std::nullopt; + } + affectedRows += deletes; + } + } + if (affectedRows == 0 && (!hasTableAccess || !result.GetResultSets().empty())) { + return std::nullopt; + } + return static_cast(affectedRows); + } + } TStatement::TStatement(TConnection* conn) @@ -135,6 +166,7 @@ void TStatement::DetachDescriptor(TDescriptor* desc) { SQLRETURN TStatement::Prepare(const std::string& statementText) { RowsFetched_ = 0; + RowCount_ = -1; SetCursor(nullptr); PreparedQuery_ = statementText; IsPrepared_ = true; @@ -173,6 +205,9 @@ SQLRETURN TStatement::Execute() { } SQLRETURN TStatement::ExecuteInternal() { + RowCount_ = 0; + bool hasSuccessfulParamSet = false; + bool rowCountUsable = true; const SQLULEN paramsetSize = ParamCount_ > 0 ? CurrentAppParamDesc_->GetArraySize() : 1; SQLUSMALLINT* const operations = CurrentAppParamDesc_->GetArrayStatusPtr(); SQLUSMALLINT* const statuses = ImpParamDesc_.GetArrayStatusPtr(); @@ -201,7 +236,16 @@ SQLRETURN TStatement::ExecuteInternal() { } return AddError("HY024", 0, "Invalid parameter operation value"); } - const SQLRETURN rc = ExecuteParamSet(paramSet); + std::optional affectedRows; + SQLRETURN rc; + try { + rc = ExecuteParamSet(paramSet, affectedRows); + } catch (...) { + if (!hasSuccessfulParamSet) { + RowCount_ = -1; + } + throw; + } if (statuses) { statuses[paramSet] = rc == SQL_SUCCESS_WITH_INFO ? SQL_PARAM_SUCCESS_WITH_INFO @@ -211,8 +255,20 @@ SQLRETURN TStatement::ExecuteInternal() { *processed = paramSet + 1; } if (rc == SQL_ERROR) { + if (!hasSuccessfulParamSet) { + RowCount_ = -1; + } return SQL_ERROR; } + hasSuccessfulParamSet = true; + if (rowCountUsable) { + if (!affectedRows || *affectedRows > std::numeric_limits::max() - RowCount_) { + RowCount_ = -1; + rowCountUsable = false; + } else { + RowCount_ += *affectedRows; + } + } if (rc == SQL_SUCCESS_WITH_INFO) { result = SQL_SUCCESS_WITH_INFO; } @@ -220,7 +276,10 @@ SQLRETURN TStatement::ExecuteInternal() { return result; } -SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { +SQLRETURN TStatement::ExecuteParamSet( + SQLULEN paramSet, + std::optional& affectedRows) +{ RowsFetched_ = 0; SetCursor(nullptr); auto client = Conn_->GetClient(); @@ -239,11 +298,12 @@ SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { const NYdb::NRetry::TRetryOperationSettings retrySettings = MakeAutocommitRetrySettings(); const NYdb::TStatus execStatus = client->RetryQuerySync( - [this, ¶ms](NQuery::TSession session) -> NYdb::TStatus { + [this, ¶ms, &affectedRows](NQuery::TSession session) -> NYdb::TStatus { NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); if (!result.IsSuccess()) { return StatusFrom(result); } + affectedRows = ExtractAffectedRows(result); SetCursor(CreateExecCursor(result)); return NYdb::TStatus(EStatus::SUCCESS, NYdb::NIssue::TIssues()); }, @@ -254,9 +314,9 @@ SQLRETURN TStatement::ExecuteParamSet(SQLULEN paramSet) { NQuery::TSession& session = Conn_->GetOrCreateQuerySession(); NQuery::TExecuteQueryResult result = ExecuteQuery(session, params); NStatusHelpers::ThrowOnError(result); + affectedRows = ExtractAffectedRows(result); SetCursor(CreateExecCursor(result)); } - RowCount_ = -1; InAtExec_ = false; NeedDataParam_ = 0; NeedDataTokenDelivered_ = false; @@ -290,7 +350,10 @@ NYdb::NRetry::TRetryOperationSettings TStatement::MakeAutocommitRetrySettings() return settings; } -NQuery::TExecuteQueryResult TStatement::ExecuteQuery(NQuery::TSession& session, const NYdb::TParams& params) { +NQuery::TExecuteQueryResult TStatement::ExecuteQuery( + NQuery::TSession& session, + const NYdb::TParams& params) +{ const std::string sqlAfterEscapes = Attributes_.GetNoScanMode() == SQL_NOSCAN_ON ? PreparedQuery_ : RewriteOdbcEscapes(PreparedQuery_); @@ -303,6 +366,7 @@ NQuery::TExecuteQueryResult TStatement::ExecuteQuery(NQuery::TSession& session, rewritten.Sql, {"CREATE", "DROP", "ALTER", "GRANT", "REVOKE"}); const std::string queryText = Conn_->WrapQueryForCurrentCatalog(rewritten.Sql); NQuery::TExecuteQuerySettings execSettings; + execSettings.StatsMode(NQuery::EStatsMode::Basic); const SQLUINTEGER queryTimeoutSec = Attributes_.GetQueryTimeoutSec(); if (queryTimeoutSec > 0) { execSettings.ClientTimeout(TDuration::Seconds(queryTimeoutSec)); @@ -625,6 +689,7 @@ SQLRETURN TStatement::NumParams(SQLSMALLINT* paramCount) { void TStatement::ResetForMetadata() { ClearErrors(); RowsFetched_ = 0; + RowCount_ = -1; SetCursor(nullptr); } @@ -917,12 +982,8 @@ SQLRETURN TStatement::GetDiagField( SQLPOINTER diagInfoPtr, SQLSMALLINT bufferLength, SQLSMALLINT* stringLengthPtr) { - if (recNumber == 0 && diagIdentifier == SQL_DIAG_ROW_COUNT) { - if (!diagInfoPtr) { - return SQL_ERROR; - } - *reinterpret_cast(diagInfoPtr) = -1; - return SQL_SUCCESS; + if (diagIdentifier == SQL_DIAG_ROW_COUNT) { + return RowCount(static_cast(diagInfoPtr)); } return TErrorManager::GetDiagField(recNumber, diagIdentifier, diagInfoPtr, bufferLength, stringLengthPtr); } diff --git a/odbc/src/statement.h b/odbc/src/statement.h index c67848d4be..643ca0bd0d 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -115,7 +116,7 @@ class TStatement : public TErrorManager { std::vector GetDataOffsets_; SQLRETURN BuildParams(NYdb::TParams& out, SQLULEN paramSet); - SQLRETURN ExecuteParamSet(SQLULEN paramSet); + SQLRETURN ExecuteParamSet(SQLULEN paramSet, std::optional& affectedRows); void FillBoundColumns(); std::vector GetBoundParams(SQLULEN paramSet) const; void SetCursor(std::unique_ptr cursor); diff --git a/odbc/src/utils/util.cpp b/odbc/src/utils/util.cpp index 63d1da10cc..ba587c6edf 100644 --- a/odbc/src/utils/util.cpp +++ b/odbc/src/utils/util.cpp @@ -100,15 +100,15 @@ bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t pre return true; } -std::map ParseConnectionString(const std::string& connectionString) { - std::map params; +TConnectionStringEntries ParseConnectionStringEntries(std::string_view connectionString) { + TConnectionStringEntries entries; size_t pos = 0; while (pos < connectionString.size()) { const size_t eq = connectionString.find('=', pos); if (eq == std::string::npos) { break; } - std::string key = connectionString.substr(pos, eq - pos); + std::string key(connectionString.substr(pos, eq - pos)); TrimInPlace(key); if (key.empty()) { break; @@ -140,7 +140,8 @@ std::map ParseConnectionString(const std::string& conn valueEnd = connectionString.size(); pos = connectionString.size(); } - params[key] = connectionString.substr(valueStart, valueEnd - valueStart); + entries.emplace_back( + std::move(key), std::string(connectionString.substr(valueStart, valueEnd - valueStart))); continue; } @@ -151,9 +152,17 @@ std::map ParseConnectionString(const std::string& conn } else { pos = connectionString.size(); } - std::string val = connectionString.substr(valueStart, valueEnd - valueStart); + std::string val(connectionString.substr(valueStart, valueEnd - valueStart)); TrimInPlace(val); - params[key] = val; + entries.emplace_back(std::move(key), std::move(val)); + } + return entries; +} + +std::map ParseConnectionString(std::string_view connectionString) { + std::map params; + for (auto&& [key, value] : ParseConnectionStringEntries(connectionString)) { + params[std::move(key)] = std::move(value); } return params; } diff --git a/odbc/src/utils/util.h b/odbc/src/utils/util.h index 9914ff9bac..adb5d5d490 100644 --- a/odbc/src/utils/util.h +++ b/odbc/src/utils/util.h @@ -7,6 +7,9 @@ #include #include +#include +#include +#include namespace NYdb::NOdbc { @@ -16,6 +19,10 @@ std::string GetString(SQLWCHAR* str, SQLINTEGER length); bool StartsWithPrefix(const char* s, size_t sLen, const char* prefix, size_t prefixLen); -std::map ParseConnectionString(const std::string& connectionString); +using TConnectionStringEntries = std::vector>; + +TConnectionStringEntries ParseConnectionStringEntries(std::string_view connectionString); + +std::map ParseConnectionString(std::string_view connectionString); } // namespace NYdb::NOdbc diff --git a/odbc/tests/CMakeLists.txt b/odbc/tests/CMakeLists.txt index 8abcd08183..916fa4a0b8 100644 --- a/odbc/tests/CMakeLists.txt +++ b/odbc/tests/CMakeLists.txt @@ -15,6 +15,7 @@ Driver=YDB Description=YDB Database Connection Server=${YDB_ODBC_DSN_SERVER} Database=${YDB_ODBC_DSN_DATABASE} +AuthMode=Anonymous ") add_subdirectory(integration) diff --git a/odbc/tests/integration/CMakeLists.txt b/odbc/tests/integration/CMakeLists.txt index 1b116da8bd..a2419c0e45 100644 --- a/odbc/tests/integration/CMakeLists.txt +++ b/odbc/tests/integration/CMakeLists.txt @@ -13,6 +13,15 @@ add_odbc_test(NAME odbc-connection_api_it connection_api_it.cpp ) +add_odbc_test(NAME odbc-authentication_it + SOURCES + authentication_it.cpp + LINK_LIBRARIES + tests-iam-mocks + client-oauth2-ut-helpers + cpp-testing-unittest +) + add_odbc_test(NAME odbc-statement_api_it SOURCES statement_api_it.cpp diff --git a/odbc/tests/integration/authentication_it.cpp b/odbc/tests/integration/authentication_it.cpp new file mode 100644 index 0000000000..4fc9eae283 --- /dev/null +++ b/odbc/tests/integration/authentication_it.cpp @@ -0,0 +1,207 @@ +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +using namespace NYdb::NTest; + +namespace { + +constexpr std::string_view RootToken = "root@builtin"; + +class TScopedEnvironmentVariable { +public: + TScopedEnvironmentVariable(std::string_view name, std::string_view value) + : Name_(name) + { + if (const char* oldValue = std::getenv(Name_.c_str())) { + OldValue_ = oldValue; + } + setenv(Name_.c_str(), std::string(value).c_str(), 1); + } + + ~TScopedEnvironmentVariable() { + if (OldValue_) { + setenv(Name_.c_str(), OldValue_->c_str(), 1); + } else { + unsetenv(Name_.c_str()); + } + } + +private: + std::string Name_; + std::optional OldValue_; +}; + +class OdbcAuthentication : public ::testing::Test { +protected: + void SetUp() override { + const char* endpoint = std::getenv("YDB_ENDPOINT"); + const char* database = std::getenv("YDB_DATABASE"); + if (!endpoint || !database) { + GTEST_SKIP() << "Authentication integration tests require the IAM-enabled YDB fixture"; + } + Endpoint_ = endpoint; + Database_ = database; + AllocEnv(&Env_); + } + + void TearDown() override { + Disconnect(); + if (Env_ != SQL_NULL_HENV) { + SQLFreeHandle(SQL_HANDLE_ENV, Env_); + } + } + + void Connect(std::string_view authenticationAttributes) { + Disconnect(); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, Env_, &Dbc_), SQL_SUCCESS); + std::string connectionString = "Driver=" ODBC_DRIVER_PATH ";Endpoint=" + Endpoint_ + + ";Database=" + Database_ + ";" + std::string(authenticationAttributes); + const SQLRETURN rc = SQLDriverConnect( + Dbc_, nullptr, reinterpret_cast(connectionString.data()), SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + CHECK_ODBC_OK(rc, Dbc_, SQL_HANDLE_DBC); + } + + void Disconnect() { + if (Dbc_ != SQL_NULL_HDBC) { + SQLDisconnect(Dbc_); + SQLFreeHandle(SQL_HANDLE_DBC, Dbc_); + Dbc_ = SQL_NULL_HDBC; + } + } + + void Execute(std::string_view query) { + SQLHSTMT statement = SQL_NULL_HSTMT; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc_, &statement), SQL_SUCCESS); + std::string queryString(query); + const SQLRETURN rc = SQLExecDirect( + statement, reinterpret_cast(queryString.data()), SQL_NTS); + CHECK_ODBC_OK(rc, statement, SQL_HANDLE_STMT); + SQLFreeHandle(SQL_HANDLE_STMT, statement); + } + + void SelectOne() { + Execute("SELECT 1"); + } + + void ExpectSelectOneAuthFailure() { + SQLHSTMT statement = SQL_NULL_HSTMT; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc_, &statement), SQL_SUCCESS); + SQLCHAR query[] = "SELECT 1"; + const SQLRETURN rc = SQLExecDirect(statement, query, SQL_NTS); + EXPECT_EQ(rc, SQL_ERROR); + if (rc == SQL_ERROR) { + const std::string error = GetOdbcError(statement, SQL_HANDLE_STMT); + EXPECT_TRUE(SqlStatePrefix(error, "28000")) << error; + } + EXPECT_EQ(SQLFreeHandle(SQL_HANDLE_STMT, statement), SQL_SUCCESS); + } + + SQLHENV Env_ = SQL_NULL_HENV; + SQLHDBC Dbc_ = SQL_NULL_HDBC; + std::string Endpoint_; + std::string Database_; +}; + +} // namespace + +TEST_F(OdbcAuthentication, TokenAndAccessTokenAlias) { + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Token;Token=root@builtin;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + + ASSERT_NO_FATAL_FAILURE(Connect("AccessToken=root@builtin;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} + +TEST_F(OdbcAuthentication, Anonymous) { + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Anonymous;")); + ASSERT_NO_FATAL_FAILURE(ExpectSelectOneAuthFailure()); +} + +TEST_F(OdbcAuthentication, StaticUserAndPasswordAliases) { + const char* user = std::getenv("YDB_ODBC_STATIC_USER"); + const char* password = std::getenv("YDB_ODBC_STATIC_PASSWORD"); + if (!user || !password) { + GTEST_SKIP() << "Static authentication requires credentials provisioned by the IAM fixture"; + } + + ASSERT_NO_FATAL_FAILURE(Connect( + "AuthMode=Static;UID=" + std::string(user) + ";PWD=" + std::string(password) + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} + +TEST_F(OdbcAuthentication, MetadataService) { + TMetadataServer server; + server.SetResponse(HTTP_OK, MakeTokenResponse(std::string(RootToken), 3600)); + + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Metadata;MetadataHost=127.0.0.1;MetadataPort=" + + std::to_string(server.Port) + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + + EXPECT_GE(server.GetRequestCount(), 1); + AssertMetadataRequestShape(server); +} + +TEST_F(OdbcAuthentication, ServiceAccountFileAndAlias) { + TIamTokenServiceStub stub; + stub.SetResponseToken(std::string(RootToken)); + TIamGrpcServer server(&stub); + ASSERT_TRUE(server.Start()); + + TTempDir tempDirectory; + const TString keyPath = tempDirectory.Path() / "service-account.json"; + TFileOutput(keyPath).Write(MakeJwtKeyFileContent()); + + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=ServiceAccount;SaFile=" + std::string(keyPath) + + ";IamEndpoint=grpc://" + server.Endpoint() + ";")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); + + EXPECT_GE(stub.GetRequestCount(), 1); + ASSERT_TRUE(stub.HasLastRequest()); + AssertIamJwt(stub.GetLastRequest().jwt()); +} + +TEST_F(OdbcAuthentication, OAuth2TokenExchangeFile) { + TTestTokenExchangeServer server; + server.Check.ExpectedInputParams.emplace("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"); + server.Check.ExpectedInputParams.emplace("requested_token_type", "urn:ietf:params:oauth:token-type:access_token"); + server.Check.ExpectedInputParams.emplace("subject_token", "odbc-subject-token"); + server.Check.ExpectedInputParams.emplace("subject_token_type", "urn:ietf:params:oauth:token-type:access_token"); + server.Check.Response = + R"({"access_token":"root@builtin","token_type":"bearer","expires_in":3600})"; + + TTempDir tempDirectory; + const TString configPath = tempDirectory.Path() / "oauth2.json"; + TFileOutput(configPath).Write( + R"({"subject-credentials":{"type":"Fixed","token":"odbc-subject-token","token-type":"urn:ietf:params:oauth:token-type:access_token"}})"); + + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=OAuth2;OAuth2KeyFile=" + std::string(configPath) + + ";IamEndpoint=" + server.GetEndpoint() + ";")); + // The local IAM fixture accepts builtin tokens, not OAuth "Bearer" credentials. + ASSERT_NO_FATAL_FAILURE(ExpectSelectOneAuthFailure()); + server.CheckExpectations(); +} + +TEST_F(OdbcAuthentication, EnvironmentAccessToken) { + TScopedEnvironmentVariable serviceAccount("YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS", ""); + TScopedEnvironmentVariable anonymous("YDB_ANONYMOUS_CREDENTIALS", "0"); + TScopedEnvironmentVariable metadata("YDB_METADATA_CREDENTIALS", "0"); + TScopedEnvironmentVariable oauth2("YDB_OAUTH2_KEY_FILE", ""); + TScopedEnvironmentVariable token("YDB_ACCESS_TOKEN_CREDENTIALS", RootToken); + ASSERT_NO_FATAL_FAILURE(Connect("AuthMode=Environment;")); + ASSERT_NO_FATAL_FAILURE(SelectOne()); +} diff --git a/odbc/tests/integration/connection_api_it.cpp b/odbc/tests/integration/connection_api_it.cpp index aff067a9e2..4d029aeaa3 100644 --- a/odbc/tests/integration/connection_api_it.cpp +++ b/odbc/tests/integration/connection_api_it.cpp @@ -81,6 +81,89 @@ TEST(ConnectionApi, SQLDriverConnectInvalidConnString) { SQLFreeHandle(SQL_HANDLE_ENV, env); } +TEST(ConnectionApi, SQLDriverConnectIgnoresUnrecognizedAttributes) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLCHAR connectionString[] = + "Driver=" ODBC_DRIVER_PATH + ";Endpoint=localhost:2136;Database=/local;APP=PowerBI;WSID=desktop;Timeout=30;"; + const SQLRETURN rc = SQLDriverConnect( + dbc, nullptr, connectionString, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + ASSERT_EQ(rc, SQL_SUCCESS_WITH_INFO) << GetOdbcError(dbc, SQL_HANDLE_DBC); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), "01S00")); + + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, reinterpret_cast(const_cast("SELECT 1")), SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLDriverConnectValidatesAuthenticationSettings) { + SQLHENV env; + AllocEnv(&env); + + const struct { + const char* ConnectionString; + const char* SqlState; + } cases[] = { + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=None;", "28000"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;Token=a;UID=b;PWD=c;", "28000"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Static;UID=b;", "28000"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=Metadata;MetadataPort=70000;", "HY024"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=ServiceAccount;SaFile=/missing/sa.json;", "08001"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;AuthMode=OAuth2;OAuth2KeyFile=/missing/oauth2.json;", "08001"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;ClientCertificate=client.pem;", "08001"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=grpc://localhost:2136;Database=/local;CaFile=ca.pem;", "HY024"}, + {"Driver=" ODBC_DRIVER_PATH ";Endpoint=localhost:2136;Database=/local;RootCertificate=/missing/ca.pem;", "08001"}, + }; + + for (const auto& testCase : cases) { + SQLHDBC dbc; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + const SQLRETURN rc = SQLDriverConnect( + dbc, nullptr, reinterpret_cast(const_cast(testCase.ConnectionString)), SQL_NTS, + nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT); + ASSERT_EQ(rc, SQL_ERROR) << testCase.ConnectionString; + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(dbc, SQL_HANDLE_DBC), testCase.SqlState)) + << testCase.ConnectionString << ": " << GetOdbcError(dbc, SQL_HANDLE_DBC); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(ConnectionApi, SQLDriverConnectSupportsAliasesAndDsnOverlay) { + SQLHENV env; + SQLHDBC dbc; + AllocEnv(&env); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc), SQL_SUCCESS); + + SQLCHAR connectionString[] = + "DSN=YDB;Endpoint=grpc://127.0.0.1:2136;AuthMode=Token;AccessToken=ignored-by-anonymous-server;"; + CHECK_ODBC_OK(SQLDriverConnect( + dbc, nullptr, connectionString, SQL_NTS, nullptr, 0, nullptr, SQL_DRIVER_NOPROMPT), + dbc, SQL_HANDLE_DBC); + + SQLHSTMT stmt; + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + CHECK_ODBC_OK(SQLExecDirect(stmt, (SQLCHAR*)"SELECT 1", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + TEST(ConnectionApi, SQLConnectMissingDSN) { SQLHENV env; SQLHDBC dbc; diff --git a/odbc/tests/integration/statement_api_it.cpp b/odbc/tests/integration/statement_api_it.cpp index efa1bde2ed..9dbb1b188b 100644 --- a/odbc/tests/integration/statement_api_it.cpp +++ b/odbc/tests/integration/statement_api_it.cpp @@ -380,14 +380,134 @@ TEST(StatementApi, RowCount) { AllocEnvAndConnect(&env, &dbc); ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - CHECK_ODBC_OK(SQLExecDirect(stmt, - (SQLCHAR*)"SELECT * FROM AS_TABLE(ListMap(ListFromRange(1, 4), ($x) -> (AsStruct($x AS v))))", + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS row_count_test", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE row_count_test (id Int32, value Int32, PRIMARY KEY (id))", SQL_NTS), stmt, SQL_HANDLE_STMT); - - SQLLEN rowCount; + + SQLLEN rowCount = -2; CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); EXPECT_EQ(rowCount, -1); - + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPSERT INTO row_count_test (id, value) VALUES (1, 10), (2, 20), (3, 30)", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLLEN diagRowCount = -2; + CHECK_ODBC_OK(SQLGetDiagField(SQL_HANDLE_STMT, stmt, 0, SQL_DIAG_ROW_COUNT, + &diagRowCount, 0, nullptr), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 3); + EXPECT_EQ(diagRowCount, rowCount); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPDATE row_count_test SET value = value + 1 WHERE id <= 2", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 2); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"DELETE FROM row_count_test WHERE id = 3", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 1); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"UPDATE row_count_test SET value = 0 WHERE id = 100", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 0); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"PRAGMA TablePathPrefix = \"/local\";\n" + "UPDATE row_count_test SET value = value + 1 WHERE id = 1", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 1); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLPrepare(stmt, + (SQLCHAR*)"DECLARE $p1 AS Int32?;\n" + "UPDATE row_count_test SET value = value + 1 WHERE id = $p1", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER nativeId = 2; + SQLLEN nativeIdLength = 0; + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, &nativeId, 0, &nativeIdLength), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 1); + SQLFreeStmt(stmt, SQL_RESET_PARAMS); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"SELECT * FROM row_count_test", + SQL_NTS), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, -1); + + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE row_count_test", SQL_NTS); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(StatementApi, RowCountAggregatesParameterArrays) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE IF EXISTS row_count_param_test", SQL_NTS); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLExecDirect(stmt, + (SQLCHAR*)"CREATE TABLE row_count_param_test (id Int32, value Int32, PRIMARY KEY (id))", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLFreeStmt(stmt, SQL_CLOSE); + + CHECK_ODBC_OK(SQLPrepare(stmt, + (SQLCHAR*)"UPSERT INTO row_count_param_test (id, value) VALUES (?, ?)", + SQL_NTS), stmt, SQL_HANDLE_STMT); + SQLINTEGER ids[] = {1, 2, 3}; + SQLINTEGER values[] = {10, 20, 30}; + SQLLEN idLengths[] = {0, 0, 0}; + SQLLEN valueLengths[] = {0, 0, 0}; + SQLUSMALLINT operations[] = {SQL_PARAM_PROCEED, SQL_PARAM_IGNORE, SQL_PARAM_PROCEED}; + SQLUSMALLINT statuses[] = {SQL_PARAM_UNUSED, SQL_PARAM_UNUSED, SQL_PARAM_UNUSED}; + SQLULEN processed = 0; + + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAMSET_SIZE, + reinterpret_cast(3), 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_OPERATION_PTR, + operations, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAM_STATUS_PTR, + statuses, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_PARAMS_PROCESSED_PTR, + &processed, 0), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, ids, 0, idLengths), stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, + 0, 0, values, 0, valueLengths), stmt, SQL_HANDLE_STMT); + + CHECK_ODBC_OK(SQLExecute(stmt), stmt, SQL_HANDLE_STMT); + SQLLEN rowCount = -1; + CHECK_ODBC_OK(SQLRowCount(stmt, &rowCount), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(rowCount, 2); + EXPECT_EQ(processed, 3); + EXPECT_EQ(statuses[0], SQL_PARAM_SUCCESS); + EXPECT_EQ(statuses[1], SQL_PARAM_UNUSED); + EXPECT_EQ(statuses[2], SQL_PARAM_SUCCESS); + + SQLFreeStmt(stmt, SQL_CLOSE); + SQLExecDirect(stmt, (SQLCHAR*)"DROP TABLE row_count_param_test", SQL_NTS); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); diff --git a/odbc/tests/integration/test_utils.h b/odbc/tests/integration/test_utils.h index a14cbac5f3..b0eae27334 100644 --- a/odbc/tests/integration/test_utils.h +++ b/odbc/tests/integration/test_utils.h @@ -8,6 +8,7 @@ #include #include #include +#include inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { SQLCHAR sqlState[6] = {0}; @@ -26,8 +27,8 @@ inline std::string GetOdbcError(SQLHANDLE handle, SQLSMALLINT type) { inline const char* kConnStr = "Driver=" ODBC_DRIVER_PATH ";Server=localhost:2136;Database=/local;"; -inline bool SqlStatePrefix(const std::string& diag, const char* state5) { - return diag.size() >= 5 && std::strncmp(diag.c_str(), state5, 5) == 0; +inline bool SqlStatePrefix(std::string_view diag, std::string_view state) { + return diag.starts_with(state); } inline void AllocEnv(SQLHENV* env) { diff --git a/scripts/googleapis_deb/CMakeLists.txt b/scripts/googleapis_deb/CMakeLists.txt index 0c96c2358d..b0cf270066 100644 --- a/scripts/googleapis_deb/CMakeLists.txt +++ b/scripts/googleapis_deb/CMakeLists.txt @@ -48,6 +48,8 @@ endforeach() add_library(api-common-protos STATIC ${PROTO_SRCS} ${PROTO_HDRS}) add_library(yandex-googleapis-api-common-protos::api-common-protos ALIAS api-common-protos) +set_target_properties(api-common-protos PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(api-common-protos PUBLIC $ $