From 581935c26fab2d01c423821161a90e5a431b98e8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 4 Nov 2014 13:51:15 +0100 Subject: [PATCH] Fix database setup and add support for the new schema refs #7163 --- .../forms/Setup/DatabaseCreationPage.php | 99 ++-- library/Icinga/Application/WebSetup.php | 58 ++- library/Icinga/Installation/DatabaseStep.php | 171 ++++--- library/Icinga/Web/Setup/DbTool.php | 478 +++++++++++++----- 4 files changed, 527 insertions(+), 279 deletions(-) diff --git a/application/forms/Setup/DatabaseCreationPage.php b/application/forms/Setup/DatabaseCreationPage.php index bbbb80324..57303a604 100644 --- a/application/forms/Setup/DatabaseCreationPage.php +++ b/application/forms/Setup/DatabaseCreationPage.php @@ -22,11 +22,18 @@ class DatabaseCreationPage extends Form protected $config; /** - * The required database privileges + * The required privileges to setup the database * * @var array */ - protected $databasePrivileges; + protected $databaseSetupPrivileges; + + /** + * The required privileges to operate the database + * + * @var array + */ + protected $databaseUsagePrivileges; /** * Initialize this page @@ -50,15 +57,28 @@ class DatabaseCreationPage extends Form } /** - * Set the required database privileges + * Set the required privileges to setup the database * - * @param array $privileges The required privileges + * @param array $privileges The privileges * * @return self */ - public function setDatabasePrivileges(array $privileges) + public function setDatabaseSetupPrivileges(array $privileges) { - $this->databasePrivileges = $privileges; + $this->databaseSetupPrivileges = $privileges; + return $this; + } + + /** + * Set the required privileges to operate the database + * + * @param array $privileges The privileges + * + * @return self + */ + public function setDatabaseUsagePrivileges(array $privileges) + { + $this->databaseUsagePrivileges = $privileges; return $this; } @@ -130,57 +150,42 @@ class DatabaseCreationPage extends Form return true; } - $this->config['username'] = $this->getValue('username'); - $this->config['password'] = $this->getValue('password'); - $db = new DbTool($this->config); - $database = $this->config['dbname']; - $dbtype = $this->config['db']; + $config = $this->config; + $config['username'] = $this->getValue('username'); + $config['password'] = $this->getValue('password'); + $db = new DbTool($config); try { - $error = false; - $msg = ''; - if ($dbtype === 'pgsql') { - $db->connectToHost(); - if ( - false === $db->checkPgsqlGrantOption($this->databasePrivileges, $database, 'account') && - false === $db->checkPgsqlGrantOption($this->databasePrivileges, $database, 'preference') - ) { - $error = true; - $msg = sprintf(t('The role does not seem to have permission to create ' . - ' or to grant access to the database "%s" and the tables "account", "preference".'), $database); - } - } else if ($dbtype === 'mysql') { - $db->connectToHost(); - if (false === $db->checkMysqlGrantOption($this->databasePrivileges)) { - $error = true; - $msg = sprintf(t('The user does not seem to have permission to create ' . - ' or to grant access to the database %s.'), $database); - } - } - if ($error) { - $this->addSkipValidationCheckbox(); - $this->addError(t( - 'The provided credentials do not have the required access rights to create the database schema: ' - ) . $msg); - return false; - } - } catch (PDOException $e) { + $db->connectToDb(); // Are we able to login on the database? + } catch (PDOException $_) { try { - $db->connectToHost(); - if (false === $db->checkPrivileges($this->databasePrivileges)) { - $this->addError( - t('The provided credentials cannot be used to create the database and/or the user.') - ); - $this->addSkipValidationCheckbox(); - return false; - } + $db->connectToHost(); // Are we able to login on the server? } catch (PDOException $e) { + // We are NOT able to login on the server.. $this->addError($e->getMessage()); $this->addSkipValidationCheckbox(); return false; } } + // In case we are connected the credentials filled into this + // form need to be granted to create databases, users... + if (false === $db->checkPrivileges($this->databaseSetupPrivileges)) { + $this->addError(t('The provided credentials cannot be used to create the database and/or the user.')); + $this->addSkipValidationCheckbox(); + return false; + } + + // ...and to grant all required usage privileges to others + if (false === $db->isGrantable($this->databaseUsagePrivileges)) { + $this->addError(sprintf( + t('The provided credentials cannot be used to grant all required privileges to the login "%s".'), + $this->config['username'] + )); + $this->addSkipValidationCheckbox(); + return false; + } + return true; } diff --git a/library/Icinga/Application/WebSetup.php b/library/Icinga/Application/WebSetup.php index 2e561407f..a2fa80c35 100644 --- a/library/Icinga/Application/WebSetup.php +++ b/library/Icinga/Application/WebSetup.php @@ -44,14 +44,37 @@ class WebSetup extends Wizard implements SetupWizard */ protected $databaseSetupPrivileges = array( 'CREATE', + 'ALTER', + 'REFERENCES', + 'CREATE USER', // MySQL + 'CREATEROLE' // PostgreSQL + ); + + /** + * The privileges required by Icinga Web 2 to operate the database + * + * @var array + */ + protected $databaseUsagePrivileges = array( 'SELECT', 'INSERT', 'UPDATE', 'DELETE', - 'REFERENCES', 'EXECUTE', - 'CREATE TEMPORARY TABLES', - 'CREATE USER' + 'TEMPORARY', // PostgreSql + 'CREATE TEMPORARY TABLES' // MySQL + ); + + /** + * The database tables operated by Icinga Web 2 + * + * @var array + */ + protected $databaseTables = array( + 'icingaweb_group', + 'icingaweb_group_membership', + 'icingaweb_user', + 'icingaweb_user_preference' ); /** @@ -110,7 +133,8 @@ class WebSetup extends Wizard implements SetupWizard $page->setResourceConfig($this->getPageData('setup_ldap_resource')); } } elseif ($page->getName() === 'setup_database_creation') { - $page->setDatabasePrivileges($this->databaseSetupPrivileges); + $page->setDatabaseSetupPrivileges($this->databaseSetupPrivileges); + $page->setDatabaseUsagePrivileges($this->databaseUsagePrivileges); $page->setResourceConfig($this->getPageData('setup_db_resource')); } elseif ($page->getName() === 'setup_summary') { $page->setSubjectTitle('Icinga Web 2'); @@ -171,18 +195,24 @@ class WebSetup extends Wizard implements SetupWizard $db = new DbTool($config); try { - $db->connectToDb(); - if (array_search('account', $db->listTables()) === false) { - $skip = $db->checkPrivileges($this->databaseSetupPrivileges); + $db->connectToDb(); // Are we able to login on the database? + if (array_search(key($this->databaseTables), $db->listTables()) === false) { + // In case the database schema does not yet exist the user + // needs the privileges to create and setup the database + $skip = $db->checkPrivileges($this->databaseSetupPrivileges, $this->databaseTables); } else { - $skip = true; + // In case the database schema exists the user needs the required privileges + // to operate the database, if those are missing we ask for another user + $skip = $db->checkPrivileges($this->databaseUsagePrivileges, $this->databaseTables); } - } catch (PDOException $e) { + } catch (PDOException $_) { try { - $db->connectToHost(); - $skip = $db->checkPrivileges($this->databaseSetupPrivileges); - } catch (PDOException $e) { - // skip should already be false, nothing to do + $db->connectToHost(); // Are we able to login on the server? + // It is not possible to reliably determine whether a database exists or not if a user can't + // log in to the database, so we just require the user to be able to create the database + $skip = $db->checkPrivileges($this->databaseSetupPrivileges, $this->databaseTables); + } catch (PDOException $_) { + // We are NOT able to login on the server.. } } } else { @@ -255,6 +285,8 @@ class WebSetup extends Wizard implements SetupWizard ) { $installer->addStep( new DatabaseStep(array( + 'tables' => $this->databaseTables, + 'privileges' => $this->databaseUsagePrivileges, 'resourceConfig' => $pageData['setup_db_resource'], 'adminName' => isset($pageData['setup_database_creation']['username']) ? $pageData['setup_database_creation']['username'] diff --git a/library/Icinga/Installation/DatabaseStep.php b/library/Icinga/Installation/DatabaseStep.php index 2b220baf6..db502e146 100644 --- a/library/Icinga/Installation/DatabaseStep.php +++ b/library/Icinga/Installation/DatabaseStep.php @@ -9,7 +9,6 @@ use PDOException; use Icinga\Web\Setup\Step; use Icinga\Web\Setup\DbTool; use Icinga\Application\Icinga; -use Icinga\Application\Platform; use Icinga\Exception\InstallException; class DatabaseStep extends Step @@ -61,44 +60,40 @@ class DatabaseStep extends Step t('Successfully connected to existing database "%s"...'), $this->data['resourceConfig']['dbname'] ); - } catch (PDOException $e) { + } catch (PDOException $_) { $db->connectToHost(); $this->log(t('Creating new database "%s"...'), $this->data['resourceConfig']['dbname']); $db->exec('CREATE DATABASE ' . $db->quoteIdentifier($this->data['resourceConfig']['dbname'])); $db->reconnect($this->data['resourceConfig']['dbname']); } - if ($db->hasLogin( - $this->data['resourceConfig']['username'], - $this->data['resourceConfig']['password'] - )) { + if (array_search(key($this->data['tables']), $db->listTables()) !== false) { + $this->log(t('Database schema already exists...')); + } else { + $this->log(t('Creating database schema...')); + $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/mysql.schema.sql'); + } + + if ($db->hasLogin($this->data['resourceConfig']['username'])) { $this->log(t('Login "%s" already exists...'), $this->data['resourceConfig']['username']); } else { $this->log(t('Creating login "%s"...'), $this->data['resourceConfig']['username']); $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']); } - if (array_search('account', $db->listTables()) !== false) { - $this->log(t('Database schema already exists...')); - } else { - $this->log(t('Creating database schema...')); - $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/mysql.sql'); - } - - $privileges = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'EXECUTE', 'CREATE TEMPORARY TABLES'); - if ($db->checkMysqlGrantOption(array_merge($privileges))) { - $this->log(t('Granting required privileges to login "%s"...'), $this->data['resourceConfig']['username']); - $db->exec(sprintf( - "GRANT %s ON %s.* TO %s@'%%'", - join(',', $privileges), - $db->quoteIdentifier($this->data['resourceConfig']['dbname']), - $db->quoteIdentifier($this->data['resourceConfig']['username']) - )); - } else { + $username = $this->data['resourceConfig']['username']; + if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) { $this->log( t('Required privileges were already granted to login "%s".'), $this->data['resourceConfig']['username'] ); + } else { + $this->log(t('Granting required privileges to login "%s"...'), $this->data['resourceConfig']['username']); + $db->grantPrivileges( + $this->data['privileges'], + $this->data['tables'], + $this->data['resourceConfig']['username'] + ); } } @@ -110,52 +105,43 @@ class DatabaseStep extends Step t('Successfully connected to existing database "%s"...'), $this->data['resourceConfig']['dbname'] ); - } catch (PDOException $e) { + } catch (PDOException $_) { $db->connectToHost(); $this->log(t('Creating new database "%s"...'), $this->data['resourceConfig']['dbname']); - $db->exec('CREATE DATABASE ' . $db->quoteIdentifier($this->data['resourceConfig']['dbname'])); + $db->exec(sprintf( + "CREATE DATABASE %s WITH ENCODING 'UTF-8'", + $db->quoteIdentifier($this->data['resourceConfig']['dbname']) + )); $db->reconnect($this->data['resourceConfig']['dbname']); } - if ($db->hasLogin( - $this->data['resourceConfig']['username'], - $this->data['resourceConfig']['password'] - )) { + if (array_search(key($this->data['tables']), $db->listTables()) !== false) { + $this->log(t('Database schema already exists...')); + } else { + $this->log(t('Creating database schema...')); + $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/pgsql.schema.sql'); + } + + if ($db->hasLogin($this->data['resourceConfig']['username'])) { $this->log(t('Login "%s" already exists...'), $this->data['resourceConfig']['username']); } else { $this->log(t('Creating login "%s"...'), $this->data['resourceConfig']['username']); $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']); } - if (array_search('account', $db->listTables()) !== false) { - $this->log(t('Database schema already exists...')); - } else { - $this->log(t('Creating database schema...')); - $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/pgsql.sql'); - } - - $privileges = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'REFERENCES'); - if ($db->checkPgsqlGrantOption( - $privileges, - $this->data['resourceConfig']['dbname'], - 'account' - )) { - $this->log(t('Granting required privileges to login "%s"...'), $this->data['resourceConfig']['username']); - $db->exec(sprintf( - "GRANT %s ON TABLE account TO %s", - join(',', $privileges), - $db->quoteIdentifier($this->data['resourceConfig']['username']) - )); - $db->exec(sprintf( - "GRANT %s ON TABLE preference TO %s", - join(',', $privileges), - $db->quoteIdentifier($this->data['resourceConfig']['username']) - )); - } else { + $username = $this->data['resourceConfig']['username']; + if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) { $this->log( t('Required privileges were already granted to login "%s".'), $this->data['resourceConfig']['username'] ); + } else { + $this->log(t('Granting required privileges to login "%s"...'), $this->data['resourceConfig']['username']); + $db->grantPrivileges( + $this->data['privileges'], + $this->data['tables'], + $this->data['resourceConfig']['username'] + ); } } @@ -173,48 +159,71 @@ class DatabaseStep extends Step try { $db->connectToDb(); - if (array_search('account', $db->listTables()) === false) { - $message = sprintf( - t( - 'The database user "%s" will be used to setup the missing' - . ' schema required by Icinga Web 2 in database "%s".' - ), - $resourceConfig['username'], - $resourceConfig['dbname'] - ); + if (array_search(key($this->data['tables']), $db->listTables()) === false) { + if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) { + $message = sprintf( + t( + 'The database user "%s" will be used to setup the missing schema required by Icinga' + . ' Web 2 in database "%s" and to grant access to it to a new login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } else { + $message = sprintf( + t( + 'The database user "%s" will be used to setup the missing' + . ' schema required by Icinga Web 2 in database "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'] + ); + } } else { $message = sprintf( t('The database "%s" already seems to be fully set up. No action required.'), $resourceConfig['dbname'] ); } - } catch (PDOException $e) { + } catch (PDOException $_) { try { $db->connectToHost(); - if ($db->hasLogin( - $this->data['resourceConfig']['username'], - $this->data['resourceConfig']['password'] - )) { + if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) { + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $message = sprintf( + t( + 'The database user "%s" will be used to create the missing database' + . ' "%s" with the schema required by Icinga Web 2 and to grant' + . ' access to it to an existing login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } else { + $message = sprintf( + t( + 'The database user "%s" will be used to create the missing database' + . ' "%s" with the schema required by Icinga Web 2 and to grant' + . ' access to it to a new login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } + } else { $message = sprintf( t( - 'The database user "%s" will be used to create the missing ' - . 'database "%s" with the schema required by Icinga Web 2.' + 'The database user "%s" will be used to create the missing' + . ' database "%s" with the schema required by Icinga Web 2.' ), $resourceConfig['username'], $resourceConfig['dbname'] ); - } else { - $message = sprintf( - t( - 'The database user "%s" will be used to create the missing database "%s" ' - . 'with the schema required by Icinga Web 2 and a new login called "%s".' - ), - $resourceConfig['username'], - $resourceConfig['dbname'], - $this->data['resourceConfig']['username'] - ); } - } catch (Exception $ex) { + } catch (Exception $_) { $message = t( 'No connection to database host possible. You\'ll need to setup the' . ' database with the schema required by Icinga Web 2 manually.' diff --git a/library/Icinga/Web/Setup/DbTool.php b/library/Icinga/Web/Setup/DbTool.php index e3248a26a..7417786eb 100644 --- a/library/Icinga/Web/Setup/DbTool.php +++ b/library/Icinga/Web/Setup/DbTool.php @@ -38,6 +38,83 @@ class DbTool */ protected $config; + /** + * Whether we are connected to the database from the resource configuration + * + * @var bool + */ + protected $dbFromConfig = false; + + /** + * GRANT privilege level identifiers + */ + const GLOBAL_LEVEL = 1; + const PROCEDURE_LEVEL = 2; + const DATABASE_LEVEL = 4; + const TABLE_LEVEL = 8; + const COLUMN_LEVEL = 16; + const FUNCTION_LEVEL = 32; + + /** + * All MySQL GRANT privileges with their respective level identifiers + * + * @var array + */ + protected $mysqlGrantContexts = array( + 'ALL' => 31, + 'ALL PRIVILEGES' => 31, + 'ALTER' => 13, + 'ALTER ROUTINE' => 7, + 'CREATE' => 13, + 'CREATE ROUTINE' => 5, + 'CREATE TEMPORARY TABLES' => 5, + 'CREATE USER' => 1, + 'CREATE VIEW' => 13, + 'DELETE' => 13, + 'DROP' => 13, + 'EXECUTE' => 5, // MySQL reference states this also supports database level, 5.1.73 not though + 'FILE' => 1, + 'GRANT OPTION' => 15, + 'INDEX' => 13, + 'INSERT' => 29, + 'LOCK TABLES' => 5, + 'PROCESS' => 1, + 'REFERENCES' => 0, + 'RELOAD' => 1, + 'REPLICATION CLIENT' => 1, + 'REPLICATION SLAVE' => 1, + 'SELECT' => 29, + 'SHOW DATABASES' => 1, + 'SHOW VIEW' => 13, + 'SHUTDOWN' => 1, + 'SUPER' => 1, + 'UPDATE' => 29 + ); + + /** + * All PostgreSQL GRANT privileges with their respective level identifiers + * + * @var array + */ + protected $pgsqlGrantContexts = array( + 'ALL' => 63, + 'ALL PRIVILEGES' => 63, + 'SELECT' => 24, + 'INSERT' => 24, + 'UPDATE' => 24, + 'DELETE' => 8, + 'TRUNCATE' => 8, + 'REFERENCES' => 24, + 'TRIGGER' => 8, + 'CREATE' => 12, + 'CONNECT' => 4, + 'TEMPORARY' => 4, + 'TEMP' => 4, + 'EXECUTE' => 32, + 'USAGE' => 33, + 'CREATEROLE' => 1 + ); + /** * Create a new DbTool * @@ -62,11 +139,12 @@ class DbTool // the current user name as default database in cases none is provided. If // that database doesn't exist (which might be the case here) it will error. // Therefore, we specify the maintenance database 'postgres' as database, which - // is most probably present and public. + // is most probably present and public. (http://stackoverflow.com/q/4483139) $this->connect('postgres'); } else { $this->connect(); } + return $this; } @@ -137,6 +215,7 @@ class DbTool $this->_pdoConnect($dbname); if ($dbname !== null) { $this->_zendConnect($dbname); + $this->dbFromConfig = $dbname === $this->config['dbname']; } } @@ -282,12 +361,12 @@ class DbTool */ public function quote($value) { - $value = $this->pdoConn->quote($value); - if ($value === false) { - throw new LogicException('Unable to quote value'); + $quoted = $this->pdoConn->quote($value); + if ($quoted === false) { + throw new LogicException(sprintf('Unable to quote value: %s', $value)); } - return $value; + return $quoted; } /** @@ -346,7 +425,7 @@ class DbTool $file = new File($filepath); $content = join(PHP_EOL, iterator_to_array($file)); // There is no fread() before PHP 5.5 :( - foreach (explode(';', $content) as $statement) { + foreach (preg_split('@;(?! \\\\)@', $content) as $statement) { if (($statement = trim($statement)) !== '') { $this->exec($statement); } @@ -357,15 +436,108 @@ class DbTool * Return whether the given privileges were granted * * @param array $privileges An array of strings with the required privilege names + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name for which to check the privileges, + * if NULL the current login is used * * @return bool */ - public function checkPrivileges(array $privileges) + public function checkPrivileges(array $privileges, array $context = null, $username = null) { if ($this->config['db'] === 'mysql') { - return $this->checkMysqlPriv($privileges); - } else { - return $this->checkPgsqlPriv($privileges, $table); + return $this->checkMysqlPrivileges($privileges, false, $context, $username); + } elseif ($this->config['db'] === 'pgsql') { + return $this->checkPgsqlPrivileges($privileges, false, $context, $username); + } + } + + /** + * Return whether the given privileges are grantable to other users + * + * @param array $privileges The privileges that should be grantable + * + * @return bool + */ + public function isGrantable($privileges) + { + if ($this->config['db'] === 'mysql') { + return $this->checkMysqlPrivileges($privileges, true); + } elseif ($this->config['db'] === 'pgsql') { + return $this->checkPgsqlPrivileges($privileges, true); + } + } + + /** + * Grant all given privileges to the given user + * + * @param array $privileges The privilege names to grant + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The username to grant the privileges to + */ + public function grantPrivileges(array $privileges, array $context, $username) + { + if ($this->config['db'] === 'mysql') { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $queryString = sprintf( + 'GRANT %%s ON %s.%%s TO %s@%s', + $this->quoteIdentifier($this->config['dbname']), + $this->quoteIdentifier($username), + $this->quoteIdentifier($host) + ); + + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) { + if (false === empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (false === empty($tablePrivileges)) { + foreach ($context as $table) { + $this->exec( + sprintf($queryString, join(',', $tablePrivileges), $this->quoteIdentifier($table)) + ); + } + } + + if (false === empty($dbPrivileges)) { + $this->exec(sprintf($queryString, join(',', $dbPrivileges), '*')); + } + } elseif ($this->config['db'] === 'pgsql') { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (false === empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (false === empty($dbPrivileges)) { + $this->exec(sprintf( + 'GRANT %s ON DATABASE %s TO %s', + join(',', $dbPrivileges), + $this->config['dbname'], + $username + )); + } + + if (false === empty($tablePrivileges)) { + foreach ($context as $table) { + $this->exec(sprintf( + 'GRANT %s ON TABLE %s TO %s', + join(',', $tablePrivileges), + $table, + $username + )); + } + } } } @@ -384,33 +556,33 @@ class DbTool * Return whether the given database login exists * * @param string $username The username to search - * @param string $password The password for user $username, required in case it's a MySQL database * * @return bool */ - public function hasLogin($username, $password = null) + public function hasLogin($username) { if ($this->config['db'] === 'mysql') { - // probe login by trial and error since we don't know our host name or it may be globbed - try { - $probeConf = $this->config; - $probeConf['username'] = $username; - $probeConf['password'] = $password; - $probe = new DbTool($probeConf); - $probe->connectToHost(); - } catch (PDOException $e) { - return false; - } + $queryString = <<query( + $queryString, + array( + ':current' => $this->config['username'], + ':wanted' => $username + ) + ); + return count($query->fetchAll()) > 0; } elseif ($this->config['db'] === 'pgsql') { - $rowCount = $this->exec( - 'SELECT usename FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1', + $query = $this->query( + 'SELECT 1 FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1', array(':ident' => $username) ); + return count($query->fetchAll()) === 1; } - - return $rowCount === 1; } /** @@ -437,139 +609,169 @@ class DbTool } /** - * Check whether the current role has GRANT permissions + * Check whether the current user has the given privileges * - * @param array $privileges - * @param $database + * @param array $privileges The privilege names + * @param bool $requireGrants Only return true when all privileges can be granted to others + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name to which the passed privileges need to be granted + * + * @return bool */ - public function checkMysqlGrantOption(array $privileges) - { - return $this->checkMysqlPriv($privileges, true); - } + protected function checkMysqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $grantee = "'" . ($username === null ? $this->config['username'] : $username) . "'@'" . $host . "'"; + $privilegeCondition = 'privilege_type IN (' . join(',', array_map(array($this, 'quote'), $privileges)) . ')'; - /** - * Check whether the current user has the given global privileges - * - * @param array $privileges The privilege names - * @param boolean $requireGrants Only return true when all privileges can be granted to others - * - * @return bool - */ - public function checkMysqlPriv(array $privileges, $requireGrants = false) - { - $cnt = count($privileges); - if ($cnt <= 0) { - return true; - } - $grantOption = ''; - if ($requireGrants) { - $grantOption = ' AND IS_GRANTABLE = \'YES\''; - } - $rows = $this->exec( - 'SELECT PRIVILEGE_TYPE FROM information_schema.user_privileges ' . - ' WHERE GRANTEE = CONCAT("\'", REPLACE(CURRENT_USER(), \'@\', "\'@\'"), "\'") ' . - ' AND PRIVILEGE_TYPE IN (?' . str_repeat(',?', $cnt - 1) . ') ' . $grantOption . ';', - $privileges - ); + if (isset($this->config['dbname'])) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) { + if (false === empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } - return $cnt === $rows; - } + $dbPrivilegesGranted = true; + if (false === empty($dbPrivileges)) { + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.schema_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND ' . $privilegeCondition + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee, ':dbname' => $this->config['dbname']) + ); + $dbPrivilegesGranted = (int) $query->fetchObject()->matches === count($dbPrivileges); + } - /** - * Check whether the current role has GRANT permissions for the given database name - * - * For Postgres, this will be assumed as true when: - * - * A more fine-grained check of schema, table and columns permissions in the database - * will not happen. - * - * @param array $privileges - * @param $database The database - * @param $table The optional table - * - * @return bool - */ - public function checkPgsqlGrantOption(array $privileges, $database, $table = null) - { - if ($this->checkPgsqlPriv(array('SUPER'))) { - // superuser - return true; - } - $create = $this->checkPgsqlPriv(array('CREATE', 'CREATE USER')); - $owner = $this->query(sprintf( - 'SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_database WHERE datname = %s', - $this->quote($database) - ))->fetchColumn(); - if ($owner !== false) { - if ($owner !== $this->config['username']) { - // database already exists and the user is not owner of the database - return $this->checkPgsqlPriv($privileges, $table, true); - } else { - // database already exists and the user is owner of the database + $tablePrivilegesGranted = true; + if (false === empty($tablePrivileges)) { + $tableCondition = 'table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')'; + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.table_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND ' . $tableCondition + . ' AND ' . $privilegeCondition + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee, ':dbname' => $this->config['dbname']) + ); + $expectedAmountOfMatches = count($context) * count($tablePrivileges); + $tablePrivilegesGranted = (int) $query->fetchObject()->matches === $expectedAmountOfMatches; + } + + if ($dbPrivilegesGranted && $tablePrivilegesGranted) { return true; } } - // database does not exist, permission depends on createdb and createrole permissions - return $create; + + $query = $this->query( + 'SELECT COUNT(*) as matches FROM information_schema.user_privileges WHERE grantee = :grantee' + . ' AND ' . $privilegeCondition . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee) + ); + return (int) $query->fetchObject()->matches === count($privileges); } /** - * Check whether the current role has the given privileges + * Check whether the current user has the given privileges * - * NOTE: The only global role privileges in Postgres are SUPER (superuser), CREATE and CREATE USER - * (databases and roles), all others will be ignored in case no table was given + * Note that database and table specific privileges (i.e. not SUPER, CREATE and CREATEROLE) are ignored + * in case no connection to the database defined in the resource configuration has been established * - * @param array $privileges The privileges to check - * @param $table The optional schema to use, defaults to 'public' - * @param $withGrant Whether we also require the grant option on the given privileges + * @param array $privileges The privilege names + * @param bool $requireGrants Only return true when all privileges can be granted to others + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name to which the passed privileges need to be granted * - * @return bool + * @return bool */ - public function checkPgsqlPriv(array $privileges, $table = null, $withGrantOption = false) - { - if (isset($table)) { - $queries = array(); - foreach ($privileges as $privilege) { - if (false === array_search($privilege, array('CREATE USER', 'CREATE', 'SUPER'))) { - $queries[] = sprintf ( - 'has_table_privilege(%s, %s)', - $this->quote($table), - $this->quote($privilege . ($withGrantOption ? ' WITH GRANT OPTION' : '')) - ) . ' AS ' . $this->quoteIdentifier($privilege); + public function checkPgsqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $privilegesGranted = true; + if ($this->dbFromConfig) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (false === empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; } } - $ret = $this->query('SELECT ' . join (', ', $queries) . ';')->fetch(); - if (false === $ret || false !== array_search(false, $ret)) { - return false; + + if (false === empty($dbPrivileges)) { + $query = $this->query( + 'SELECT has_database_privilege(:user, :dbname, :privileges) AS db_privileges_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':dbname' => $this->config['dbname'], + ':privileges' => join(',', $dbPrivileges) . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + $privilegesGranted &= $query->fetchObject()->db_privileges_granted; } - } - if (false !== array_search('CREATE USER', $privileges)) { - $query = $this->query('select rolcreaterole from pg_roles where rolname = current_user;'); - $createrole = $query->fetchColumn(); - if (false === $createrole) { - return false; + + if (false === empty($tablePrivileges)) { + foreach (array_intersect($context, $this->listTables()) as $table) { + $query = $this->query( + 'SELECT has_table_privilege(:user, :table, :privileges) AS table_privileges_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':table' => $table, + ':privileges' => join(',', $tablePrivileges) . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + $privilegesGranted &= $query->fetchObject()->table_privileges_granted; + } } + } else { + // In case we cannot check whether the user got the required db-/table-privileges due to not being + // connected to the database defined in the resource configuration it is safe to just ignore them + // as the chances are very high that the database is created later causing the current user being + // the owner with ALL privileges. (Which in turn can be granted to others.) } - if (false !== array_search('CREATE', $privileges)) { - $query = $this->query('select rolcreatedb from pg_roles where rolname = current_user;'); - $createdb = $query->fetchColumn(); - if (false === $createdb) { - return false; - } + if (array_search('CREATE', $privileges) !== false) { + $query = $this->query( + 'select rolcreatedb from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; } - if (false !== array_search('SUPER', $privileges)) { - $query = $this->query('select rolsuper from pg_roles where rolname = current_user;'); - $super = $query->fetchColumn(); - if (false === $super) { - return false; - } + + if (array_search('CREATEROLE', $privileges) !== false) { + $query = $this->query( + 'select rolcreaterole from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; } - return true; + + if (array_search('SUPER', $privileges) !== false) { + $query = $this->query( + 'select rolsuper from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + + return $privilegesGranted; } }