From 586bc283db30018b7e9176b35b038372a30302b6 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Thu, 26 Jun 2025 12:24:04 +0200 Subject: [PATCH 1/5] Support private key --- .../src/ConnectionFactory.php | 58 +++++++++++++++---- .../tests/Functional/BaseCase.php | 4 +- .../Functional/ConnectionTestWithPassword.php | 36 ++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 packages/php-storage-driver-snowflake/tests/Functional/ConnectionTestWithPassword.php diff --git a/packages/php-storage-driver-snowflake/src/ConnectionFactory.php b/packages/php-storage-driver-snowflake/src/ConnectionFactory.php index 40ebb88d2..10bb8e64f 100644 --- a/packages/php-storage-driver-snowflake/src/ConnectionFactory.php +++ b/packages/php-storage-driver-snowflake/src/ConnectionFactory.php @@ -12,6 +12,30 @@ final class ConnectionFactory { + /** + * Check if a string is a valid RSA private key + */ + private static function isValidRsaPrivateKey(string $key): bool + { + // Remove any whitespace and check if it looks like a PEM encoded key + $key = trim($key); + if (!str_contains($key, '-----BEGIN') || !str_contains($key, 'PRIVATE KEY-----')) { + return false; + } + + // Try to get the private key details + $privateKey = openssl_pkey_get_private($key); + if ($privateKey === false) { + return false; + } + + // Get the details to verify it's an RSA key + $details = openssl_pkey_get_details($privateKey); + + // Check if it's an RSA key + return $details !== false && isset($details['key']) && $details['type'] === OPENSSL_KEYTYPE_RSA; + } + public static function createFromCredentials(GenericBackendCredentials $credentials): Connection { $meta = $credentials->getMeta(); @@ -22,15 +46,29 @@ public static function createFromCredentials(GenericBackendCredentials $credenti throw new Exception('SnowflakeCredentialsMeta is required.'); } - return SnowflakeConnectionFactory::getConnection( - $credentials->getHost(), - $credentials->getPrincipal(), - $credentials->getSecret(), - [ - 'port' => (string) $credentials->getPort(), - 'warehouse' => $meta->getWarehouse(), - 'database' => $meta->getDatabase(), - ], - ); + // Check if the secret is a valid RSA private key + $isRsaKey = self::isValidRsaPrivateKey($credentials->getSecret()); + + $connectionParams = [ + 'port' => (string) $credentials->getPort(), + 'warehouse' => $meta->getWarehouse(), + 'database' => $meta->getDatabase(), + ]; + + if ($isRsaKey) { + return SnowflakeConnectionFactory::getConnectionWithCert( + $credentials->getHost(), + $credentials->getPrincipal(), + $credentials->getSecret(), + $connectionParams, + ); + } else { + return SnowflakeConnectionFactory::getConnection( + $credentials->getHost(), + $credentials->getPrincipal(), + $credentials->getSecret(), + $connectionParams, + ); + } } } diff --git a/packages/php-storage-driver-snowflake/tests/Functional/BaseCase.php b/packages/php-storage-driver-snowflake/tests/Functional/BaseCase.php index c7f62d4d5..2948c16b2 100644 --- a/packages/php-storage-driver-snowflake/tests/Functional/BaseCase.php +++ b/packages/php-storage-driver-snowflake/tests/Functional/BaseCase.php @@ -18,10 +18,10 @@ abstract class BaseCase extends TestCase protected function getSnowflakeConnection(): Connection { - $this->connection = SnowflakeConnectionFactory::getConnection( + $this->connection = SnowflakeConnectionFactory::getConnectionWithCert( (string) getenv('SNOWFLAKE_HOST'), (string) getenv('SNOWFLAKE_USER'), - (string) getenv('SNOWFLAKE_PASSWORD'), + (string) getenv('SNOWFLAKE_PRIVATE_KEY'), [ 'port' => (string) getenv('SNOWFLAKE_PORT'), 'warehouse' => (string) getenv('SNOWFLAKE_WAREHOUSE'), diff --git a/packages/php-storage-driver-snowflake/tests/Functional/ConnectionTestWithPassword.php b/packages/php-storage-driver-snowflake/tests/Functional/ConnectionTestWithPassword.php new file mode 100644 index 000000000..a167562e0 --- /dev/null +++ b/packages/php-storage-driver-snowflake/tests/Functional/ConnectionTestWithPassword.php @@ -0,0 +1,36 @@ +getSnowflakeConnection(); + $connection->executeQuery('SELECT 1'); + } + + protected function getSnowflakeConnection(): Connection + { + $this->connection = SnowflakeConnectionFactory::getConnection( + (string) getenv('SNOWFLAKE_HOST'), + (string) getenv('SNOWFLAKE_USER'), + (string) getenv('SNOWFLAKE_PASSWORD'), + [ + 'port' => (string) getenv('SNOWFLAKE_PORT'), + 'warehouse' => (string) getenv('SNOWFLAKE_WAREHOUSE'), + 'database' => (string) getenv('SNOWFLAKE_DATABASE'), + ], + ); + + return $this->connection; + } +} From ab4bf605dfdd405a86fc09a2cc78f16b10c514d8 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Thu, 26 Jun 2025 12:30:47 +0200 Subject: [PATCH 2/5] Add tests --- .../Functional/ConnectionFactoryTest.php | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/php-storage-driver-snowflake/tests/Functional/ConnectionFactoryTest.php diff --git a/packages/php-storage-driver-snowflake/tests/Functional/ConnectionFactoryTest.php b/packages/php-storage-driver-snowflake/tests/Functional/ConnectionFactoryTest.php new file mode 100644 index 000000000..ab0232244 --- /dev/null +++ b/packages/php-storage-driver-snowflake/tests/Functional/ConnectionFactoryTest.php @@ -0,0 +1,64 @@ +setHost((string) getenv('SNOWFLAKE_HOST')); + $credentials->setPrincipal((string) getenv('SNOWFLAKE_USER')); + $credentials->setSecret((string) getenv('SNOWFLAKE_PASSWORD')); + $credentials->setPort((int) getenv('SNOWFLAKE_PORT')); + + $meta = new Any(); + $meta->pack( + (new SnowflakeCredentialsMeta()) + ->setWarehouse((string) getenv('SNOWFLAKE_WAREHOUSE')) + ->setDatabase((string) getenv('SNOWFLAKE_DATABASE')), + ); + $credentials->setMeta($meta); + + // Create connection + $connection = ConnectionFactory::createFromCredentials($credentials); + + // Test connection works + $result = $connection->executeQuery('SELECT 1 as TEST'); + $this->assertEquals(1, $result->fetchOne()); + } + + public function testCreateFromCredentialsWithPrivateKey(): void + { + // Create credentials with a key + $credentials = new GenericBackendCredentials(); + $credentials->setHost((string) getenv('SNOWFLAKE_HOST')); + $credentials->setPrincipal((string) getenv('SNOWFLAKE_USER')); + $credentials->setSecret((string) getenv('SNOWFLAKE_PRIVATE_KEY')); + $credentials->setPort((int) getenv('SNOWFLAKE_PORT')); + + $meta = new Any(); + $meta->pack( + (new SnowflakeCredentialsMeta()) + ->setWarehouse((string) getenv('SNOWFLAKE_WAREHOUSE')) + ->setDatabase((string) getenv('SNOWFLAKE_DATABASE')), + ); + $credentials->setMeta($meta); + + // Create connection + $connection = ConnectionFactory::createFromCredentials($credentials); + + // Test connection works + $result = $connection->executeQuery('SELECT 1 as TEST'); + $this->assertEquals(1, $result->fetchOne()); + } +} From 2ad93ac99d6b49a555d74db342c38d00481c30cb Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 27 Jun 2025 23:03:32 +0200 Subject: [PATCH 3/5] Document keypair --- packages/php-storage-driver-snowflake/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/php-storage-driver-snowflake/README.md b/packages/php-storage-driver-snowflake/README.md index 879eea304..cdb57f4bc 100644 --- a/packages/php-storage-driver-snowflake/README.md +++ b/packages/php-storage-driver-snowflake/README.md @@ -10,6 +10,14 @@ Keboola high level storage backend driver for Snowflake. ### Snowflake Prepare credentials for Snowflake access +Create RSA key pair for Snowflake user, you can use the following command to generate it: + +```bash +openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt +openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub +``` + +Then you can use the public key in the Snowflake user creation script below. ```snowflake CREATE ROLE "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE"; @@ -19,8 +27,10 @@ GRANT ALL PRIVILEGES ON DATABASE "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE" TO RO GRANT USAGE ON WAREHOUSE "DEV" TO ROLE "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE"; CREATE USER "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE" -PASSWORD = 'ewC@B3.6UyWVLxe*MZMdN7xYEnX6ZV_P' -DEFAULT_ROLE = "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE"; +PASSWORD = '' +DEFAULT_ROLE = "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE" +RSA_PUBLIC_KEY = '' +; GRANT ROLE "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE" TO USER "KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE"; ``` @@ -32,6 +42,7 @@ SNOWFLAKE_HOST: keboolaconnectiondev.us-east-1.snowflakecomputing.com SNOWFLAKE_PORT: 443 SNOWFLAKE_USER: KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }} +SNOWFLAKE_PRIVATE_KEY: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }} # note: it has to be full private key in PEM format, including the header and footer SNOWFLAKE_DATABASE: KEBOOLA_CI_PHP_STORAGE_DRIVER_SNOWFLAKE SNOWFLAKE_WAREHOUSE: DEV ``` From adad89d00868a339cec01b98a46d5a50681a8605 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Mon, 30 Jun 2025 10:31:56 +0200 Subject: [PATCH 4/5] Fix autoload --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f3c22b736..257c622cf 100755 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ "Keboola\\StorageDriver\\": "packages/php-storage-driver-common/generated/Keboola/StorageDriver", "Keboola\\StorageDriver\\Contract\\": "packages/php-storage-driver-common/contract/", "Keboola\\StorageDriver\\Shared\\": "packages/php-storage-driver-common/Shared/", + "Keboola\\StorageDriver\\Snowflake\\": "packages/php-storage-driver-snowflake/src/", "Keboola\\TableBackendUtils\\": "packages/php-table-backend-utils/src/" } }, @@ -66,7 +67,8 @@ "Tests\\Keboola\\Db\\ImportExportCommon\\": "packages/php-db-import-export/tests/Common", "Tests\\Keboola\\Db\\ImportExportFunctional\\": "packages/php-db-import-export/tests/functional/", "Tests\\Keboola\\Db\\ImportExportUnit\\": "packages/php-db-import-export/tests/unit", - "Tests\\Keboola\\TableBackendUtils\\": "packages/php-table-backend-utils/tests" + "Tests\\Keboola\\TableBackendUtils\\": "packages/php-table-backend-utils/tests", + "Keboola\\StorageDriver\\Snowflake\\Tests\\Functional\\": "packages/php-storage-driver-snowflake/tests/Functional" } }, "replace": { From 84cd8f2c94cb55d787f141ad5b385a9510dcd0a5 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Mon, 30 Jun 2025 14:17:17 +0200 Subject: [PATCH 5/5] Fix CR --- .../src/ConnectionFactory.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/php-storage-driver-snowflake/src/ConnectionFactory.php b/packages/php-storage-driver-snowflake/src/ConnectionFactory.php index 10bb8e64f..8fc9b2698 100644 --- a/packages/php-storage-driver-snowflake/src/ConnectionFactory.php +++ b/packages/php-storage-driver-snowflake/src/ConnectionFactory.php @@ -62,13 +62,13 @@ public static function createFromCredentials(GenericBackendCredentials $credenti $credentials->getSecret(), $connectionParams, ); - } else { - return SnowflakeConnectionFactory::getConnection( - $credentials->getHost(), - $credentials->getPrincipal(), - $credentials->getSecret(), - $connectionParams, - ); } + + return SnowflakeConnectionFactory::getConnection( + $credentials->getHost(), + $credentials->getPrincipal(), + $credentials->getSecret(), + $connectionParams, + ); } }