From 3e271fd461371cf4a1e89f638a3c00ebd2c6a186 Mon Sep 17 00:00:00 2001 From: anas-srikou Date: Fri, 25 Aug 2023 13:14:46 +0900 Subject: [PATCH 1/6] Add key and passcode in the Model config params --- src/Models/SnowflakeDbConfig.php | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Models/SnowflakeDbConfig.php b/src/Models/SnowflakeDbConfig.php index bad6967..4ff80a8 100644 --- a/src/Models/SnowflakeDbConfig.php +++ b/src/Models/SnowflakeDbConfig.php @@ -11,15 +11,15 @@ */ class SnowflakeDbConfig extends BaseSqlDbConfig { - protected $appends = ['hostname', 'account', 'username', 'password', 'database', 'warehouse', 'schema', 'role']; + protected $appends = ['hostname', 'account', 'username', 'password', 'key', 'passcode', 'database', 'warehouse', 'schema', 'role']; - protected $encrypted = ['username', 'password']; + protected $encrypted = ['username', 'password', 'key', 'passcode']; protected $protected = ['password']; protected function getConnectionFields() { - return ['hostname', 'account', 'username', 'password', 'database', 'warehouse', 'schema', 'role']; + return ['hostname', 'account', 'username', 'password', 'key', 'passcode', 'database', 'warehouse', 'schema', 'role']; } public static function getDriverName() @@ -53,7 +53,20 @@ public static function getDefaultConnectionInfo() 'name' => 'password', 'label' => 'Password', 'type' => 'password', - 'description' => 'The password for the snowflake account user. This can be a lookup key.' + 'description' => 'The password for the snowflake account user. This can be a lookup key.' . + 'If you are using a private key, leave this blank.' + ], + [ + 'name' => 'key', + 'label' => 'key', + 'type' => 'file', + 'description' => 'Specifies the local path to the private key file you created.' + ], + [ + 'name' => 'passcode', + 'label' => 'passcode', + 'type' => 'string', + 'description' => 'Specifies the passcode to decode the private key file. Omit if not required.' ], [ 'name' => 'role', From 34423590f21f4f6b1270c1c2c53f2a73675130db Mon Sep 17 00:00:00 2001 From: anas-srikou Date: Fri, 25 Aug 2023 13:15:09 +0900 Subject: [PATCH 2/6] Update service provider to check for key and passcode --- src/ServiceProvider.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index fce5fc6..df5d448 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -70,6 +70,8 @@ protected function checkHeaders(&$config) $this->substituteConfig('warehouse', 'header', $config); $this->substituteConfig('username', 'header', $config); $this->substituteConfig('password', 'header', $config); + $this->substituteConfig('key', 'header', $config); + $this->substituteConfig('passcode', 'header', $config); $this->substituteConfig('role', 'header', $config); } @@ -82,6 +84,8 @@ protected function checkUrlParams(&$config) $this->substituteConfig('warehouse', 'url', $config); $this->substituteConfig('username', 'url', $config); $this->substituteConfig('password', 'url', $config); + $this->substituteConfig('key', 'header', $config); + $this->substituteConfig('passcode', 'header', $config); $this->substituteConfig('role', 'url', $config); } From 789754fe5525e8e39201d9ea314229c3059be6d1 Mon Sep 17 00:00:00 2001 From: anas-srikou Date: Fri, 25 Aug 2023 13:15:59 +0900 Subject: [PATCH 3/6] Update connector to use key file for authentication if exist --- .../Connectors/SnowflakeConnector.php | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Database/Connectors/SnowflakeConnector.php b/src/Database/Connectors/SnowflakeConnector.php index 5b456db..9daab68 100755 --- a/src/Database/Connectors/SnowflakeConnector.php +++ b/src/Database/Connectors/SnowflakeConnector.php @@ -27,6 +27,38 @@ public function connect(array $config) return $connection; } + public function createConnection($dsn, array $config, array $options) + { + [$username, $password] = [ + $config['username'] ?? null, $config['password'] ?? null, + ]; + + try { + if ($password === null && $config['key'] !== null) { + return $this->createConnectionWithKeyPairAuth( + $dsn, $username, $config, $options + ); + } + return $this->createPdoConnection( + $dsn, $username, $password, $options + ); + } catch (Exception $e) { + return $this->tryAgainIfCausedByLostConnection( + $e, $dsn, $username, $password, $options + ); + } + } + + protected function createConnectionWithKeyPairAuth($dsn, $username, $config, $options) + { + $pdo = new PDO($dsn, $username, ""); + foreach ($options as $key => $value) { + $pdo->setAttribute($key, $value); + } + + return $pdo; + } + /** * Create a new PDO connection instance. * @@ -46,7 +78,6 @@ protected function createPdoConnection($dsn, $username, $password, $options) return $pdo; } - /** * Create a DSN string from a configuration. * @@ -85,6 +116,14 @@ protected function getDsn(array $config) $dsn .= "role={$role};"; } + if (!empty($key)) { + $dsn .= "authenticator=SNOWFLAKE_JWT;priv_key_file={$key};"; + } + + if (!empty($passcode)) { + $dsn .= "priv_key_file_pwd={$passcode};"; + } + return $dsn; } } From ddf4a7571f8970efd51401f7acbf9fc120757e25 Mon Sep 17 00:00:00 2001 From: Kevin McGahey <36458555+thekevinm@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:05:59 -0800 Subject: [PATCH 4/6] Update SnowflakeConnector.php to include partner connection id --- src/Database/Connectors/SnowflakeConnector.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Connectors/SnowflakeConnector.php b/src/Database/Connectors/SnowflakeConnector.php index 9daab68..dfab336 100755 --- a/src/Database/Connectors/SnowflakeConnector.php +++ b/src/Database/Connectors/SnowflakeConnector.php @@ -124,6 +124,8 @@ protected function getDsn(array $config) $dsn .= "priv_key_file_pwd={$passcode};"; } + $dsn .= "application=DreamFactory_DreamFactory;"; + return $dsn; } } From 78e27a08b400bbd8994f98e9a4aa3899dd72da35 Mon Sep 17 00:00:00 2001 From: Riley Bolen Date: Mon, 29 Apr 2024 11:04:07 -0700 Subject: [PATCH 5/6] Add support for stored procedures in snowflake --- src/Database/Schema/SnowflakeSchema.php | 28 +++++++++++++++++++++++++ src/Services/SnowflakeDb.php | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/src/Database/Schema/SnowflakeSchema.php b/src/Database/Schema/SnowflakeSchema.php index d0d23b1..54bf29a 100644 --- a/src/Database/Schema/SnowflakeSchema.php +++ b/src/Database/Schema/SnowflakeSchema.php @@ -45,6 +45,34 @@ protected function getTableNames($schema = '') return $names; } + /** + * @inheritdoc + */ + public function getProcedureNames($schema = '') + { + $sql = 'SHOW PROCEDURES '; + + if (!empty($schema)) { + $sql .= ' IN ' . $this->quoteTableName($schema); + } + + $rows = $this->connection->select($sql); + + $names = []; + foreach ($rows as $row) { + $row = array_values((array)$row); + $schemaName = $schema; + $resourceName = $row[1]; + $internalName = $schemaName . '.' . $resourceName; + $name = $resourceName; + $quotedName = $this->quoteTableName($schemaName) . '.' . $this->quoteTableName($resourceName); + $settings = compact('schemaName', 'resourceName', 'name', 'internalName', 'quotedName'); + $names[strtolower($name)] = new ProcedureSchema($settings); + } + + return $names; + } + /** * @inheritdoc */ diff --git a/src/Services/SnowflakeDb.php b/src/Services/SnowflakeDb.php index 2398c09..071e82f 100644 --- a/src/Services/SnowflakeDb.php +++ b/src/Services/SnowflakeDb.php @@ -7,6 +7,7 @@ use DreamFactory\Core\Exceptions\InternalServerErrorException; use DreamFactory\Core\Resources\BaseRestResource; use DreamFactory\Core\SqlDb\Services\SqlDb; +use DreamFactory\Core\SqlDb\Resources\StoredProcedure; use Arr; /** @@ -83,6 +84,12 @@ public function getResourceHandlers() 'label' => 'Schema Table', ]; + $handlers[StoredProcedure::RESOURCE_NAME] = [ + 'name' => StoredProcedure::RESOURCE_NAME, + 'class_name' => StoredProcedure::class, + 'label' => 'Stored Procedure', + ]; + return $handlers; } From b978301a6bb359f439364a7c5a0bbe95f1d5082e Mon Sep 17 00:00:00 2001 From: Riley Bolen Date: Wed, 1 May 2024 09:34:49 -0700 Subject: [PATCH 6/6] Add support for snowflakes ILIKE operator in the filter field --- src/Enums/DbComparisonOperators.php | 30 ++++++ src/Resources/SnowflakeTable.php | 145 ++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/Enums/DbComparisonOperators.php diff --git a/src/Enums/DbComparisonOperators.php b/src/Enums/DbComparisonOperators.php new file mode 100644 index 0000000..287b004 --- /dev/null +++ b/src/Enums/DbComparisonOperators.php @@ -0,0 +1,30 @@ +parseFilterString($parts, $out_params, $fields_info, $in_params); + + return static::localizeOperator($logicalOp) . $parts; + } + } else { + // (a = 1) AND (b = 2) format or (a = 1)AND(b = 2) format + $filter = str_ireplace(')' . $logicalOp . '(', ') ' . $logicalOp . ' (', $filter); + $paddedOp = ') ' . $logicalOp . ' ('; + if (false !== $pos = stripos($filter, $paddedOp)) { + $left = trim(substr($filter, 0, $pos)) . ')'; // add back right ) + $right = '(' . trim(substr($filter, $pos + strlen($paddedOp))); // adding back left ( + $left = $this->parseFilterString($left, $out_params, $fields_info, $in_params); + $right = $this->parseFilterString($right, $out_params, $fields_info, $in_params); + + return $left . ' ' . static::localizeOperator($logicalOp) . ' ' . $right; + } + } + } + + $wrap = false; + if ((0 === strpos($filter, '(')) && ((strlen($filter) - 1) === strrpos($filter, ')'))) { + // remove unnecessary wrapping () + $filter = substr($filter, 1, -1); + $wrap = true; + } + + // Some scenarios leave extra parens dangling + $pure = trim($filter, '()'); + $pieces = explode($pure, $filter); + $leftParen = (!empty($pieces[0]) ? $pieces[0] : null); + $rightParen = (!empty($pieces[1]) ? $pieces[1] : null); + $filter = $pure; + + // the rest should be comparison operators + // Note: order matters here! + $sqlOperators = DbComparisonOperators::getParsingOrder(); + foreach ($sqlOperators as $sqlOp) { + $paddedOp = static::padOperator($sqlOp); + if (false !== $pos = stripos($filter, $paddedOp)) { + $field = trim(substr($filter, 0, $pos)); + $negate = false; + if (false !== strpos($field, ' ')) { + $parts = explode(' ', $field); + $partsCount = count($parts); + if (($partsCount > 1) && + (0 === strcasecmp($parts[$partsCount - 1], trim(DbLogicalOperators::NOT_STR))) + ) { + // negation on left side of operator + array_pop($parts); + $field = implode(' ', $parts); + $negate = true; + } + } + /** @type ColumnSchema $info */ + if (null === $info = array_get($fields_info, strtolower($field))) { + // This could be SQL injection attempt or bad field + throw new BadRequestException("Invalid or unparsable field in filter request: '$field'"); + } + + // make sure we haven't chopped off right side too much + $value = trim(substr($filter, $pos + strlen($paddedOp))); + if ((0 !== strpos($value, "'")) && + (0 !== $lpc = substr_count($value, '(')) && + ($lpc !== $rpc = substr_count($value, ')')) + ) { + // add back to value from right + $parenPad = str_repeat(')', $lpc - $rpc); + $value .= $parenPad; + $rightParen = preg_replace('/\)/', '', $rightParen, $lpc - $rpc); + } + if (DbComparisonOperators::requiresValueList($sqlOp)) { + if ((0 === strpos($value, '(')) && ((strlen($value) - 1) === strrpos($value, ')'))) { + // remove wrapping () + $value = substr($value, 1, -1); + $parsed = []; + foreach (explode(',', $value) as $each) { + $parsed[] = $this->parseFilterValue(trim($each), $info, $out_params, $in_params); + } + $value = '(' . implode(',', $parsed) . ')'; + } else { + throw new BadRequestException('Filter value lists must be wrapped in parentheses.'); + } + } elseif (DbComparisonOperators::requiresNoValue($sqlOp)) { + $value = null; + } else { + static::modifyValueByOperator($sqlOp, $value); + $value = $this->parseFilterValue($value, $info, $out_params, $in_params); + } + + $sqlOp = static::localizeOperator($sqlOp); + if ($negate) { + $sqlOp = DbLogicalOperators::NOT_STR . ' ' . $sqlOp; + } + + if ($function = $info->getDbFunction(DbFunctionUses::FILTER)) { + $out = $this->parent->getConnection()->raw($function); + } else { + $out = $info->quotedName; + } + $out .= " $sqlOp"; + $out .= (isset($value) ? " $value" : null); + if ($leftParen) { + $out = $leftParen . $out; + } + if ($rightParen) { + $out .= $rightParen; + } + + return ($wrap ? '(' . $out . ')' : $out); + } + } + + // This could be SQL injection attempt or unsupported filter arrangement + throw new BadRequestException('Invalid or unparsable filter request.'); + } + /** * {@inheritdoc} */