Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature keypair auth #8

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
43 changes: 42 additions & 1 deletion src/Database/Connectors/SnowflakeConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -46,7 +78,6 @@ protected function createPdoConnection($dsn, $username, $password, $options)
return $pdo;
}


/**
* Create a DSN string from a configuration.
*
Expand Down Expand Up @@ -85,6 +116,16 @@ 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};";
}

$dsn .= "application=DreamFactory_DreamFactory;";

return $dsn;
}
}
28 changes: 28 additions & 0 deletions src/Database/Schema/SnowflakeSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
30 changes: 30 additions & 0 deletions src/Enums/DbComparisonOperators.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace DreamFactory\Core\Snowflake\Enums;

use DreamFactory\Core\Enums\DbComparisonOperators as BaseDbComparisonOperators;


/**
* DbComparisonOperators
* DB server-side filter comparison operator string constants
*/
class DbComparisonOperators extends BaseDbComparisonOperators
{
//*************************************************************************
// Constants
//*************************************************************************

/**
* @var string
*/
const ILIKE = 'ILIKE';

public static function getParsingOrder()
{
$baseParsingOrder = parent::getParsingOrder();
$likePos = array_search(static::LIKE, $baseParsingOrder);
array_splice($baseParsingOrder, $likePos, 0, static::ILIKE);
return $baseParsingOrder;
}
}
21 changes: 17 additions & 4 deletions src/Models/SnowflakeDbConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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',
Expand Down
145 changes: 145 additions & 0 deletions src/Resources/SnowflakeTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
namespace DreamFactory\Core\Snowflake\Resources;

use DB;
use DreamFactory\Core\Database\Schema\ColumnSchema;
use DreamFactory\Core\Database\Enums\DbFunctionUses;
use DreamFactory\Core\Enums\ApiOptions;
use DreamFactory\Core\Enums\DbLogicalOperators;
use DreamFactory\Core\Exceptions\BadRequestException;
use DreamFactory\Core\Exceptions\BatchException;
use DreamFactory\Core\Snowflake\Enums\DbComparisonOperators;
use DreamFactory\Core\SqlDb\Resources\Table;
use Arr;

Expand Down Expand Up @@ -85,6 +89,147 @@ public function createRecords($table, $records, $extras = [])
return $out;
}

/**
* @param string $filter
* @param array $out_params
* @param ColumnSchema[] $fields_info
* @param array $in_params
*
* @return string
* @throws \DreamFactory\Core\Exceptions\BadRequestException
* @throws \Exception
*/
protected function parseFilterString($filter, array &$out_params, $fields_info, array $in_params = [])
{
if (empty($filter)) {
return null;
}

$filter = trim($filter);
// todo use smarter regex
// handle logical operators first
$logicalOperators = DbLogicalOperators::getDefinedConstants();
foreach ($logicalOperators as $logicalOp) {
if (DbLogicalOperators::NOT_STR === $logicalOp) {
// NOT(a = 1) or NOT (a = 1)format
if ((0 === stripos($filter, $logicalOp . ' (')) || (0 === stripos($filter, $logicalOp . '('))) {
$parts = trim(substr($filter, 3));
$parts = $this->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}
*/
Expand Down
4 changes: 4 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down
7 changes: 7 additions & 0 deletions src/Services/SnowflakeDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}

Expand Down