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": { 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 ``` diff --git a/packages/php-storage-driver-snowflake/src/ConnectionFactory.php b/packages/php-storage-driver-snowflake/src/ConnectionFactory.php index 40ebb88d2..8fc9b2698 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.'); } + // 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, + ); + } + return SnowflakeConnectionFactory::getConnection( $credentials->getHost(), $credentials->getPrincipal(), $credentials->getSecret(), - [ - 'port' => (string) $credentials->getPort(), - 'warehouse' => $meta->getWarehouse(), - 'database' => $meta->getDatabase(), - ], + $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/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()); + } +} 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; + } +}