diff --git a/application/forms/Setup/DatabaseCreationPage.php b/application/forms/Setup/DatabaseCreationPage.php index b8e5adf6c..bbbb80324 100644 --- a/application/forms/Setup/DatabaseCreationPage.php +++ b/application/forms/Setup/DatabaseCreationPage.php @@ -94,7 +94,6 @@ class DatabaseCreationPage extends Form 'password', 'password', array( - 'required' => false === $skipValidation, 'label' => t('Password'), 'description' => t('The password for the database user defined above') ) @@ -134,14 +133,35 @@ class DatabaseCreationPage extends Form $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']; try { - $db->connectToDb(); - if (false === $db->checkPrivileges($this->databasePrivileges)) { - $this->addError( - t('The provided credentials do not have the required access rights to create the database schema.') - ); + $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) { diff --git a/library/Icinga/Application/Installation/DatabaseStep.php b/library/Icinga/Application/Installation/DatabaseStep.php index bad915498..5a7d7d458 100644 --- a/library/Icinga/Application/Installation/DatabaseStep.php +++ b/library/Icinga/Application/Installation/DatabaseStep.php @@ -68,7 +68,10 @@ class DatabaseStep extends Step $db->reconnect($this->data['resourceConfig']['dbname']); } - if ($db->hasLogin($this->data['resourceConfig']['username'])) { + if ($db->hasLogin( + $this->data['resourceConfig']['username'], + $this->data['resourceConfig']['password'] + )) { $this->log(t('Login "%s" already exists...'), $this->data['resourceConfig']['username']); } else { $this->log(t('Creating login "%s"...'), $this->data['resourceConfig']['username']); @@ -83,14 +86,13 @@ class DatabaseStep extends Step } $privileges = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'EXECUTE', 'CREATE TEMPORARY TABLES'); - if ($db->checkPrivileges(array_merge($privileges, array('GRANT OPTION')))) { + 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@%s", + "GRANT %s ON %s.* TO %s@'%%'", join(',', $privileges), $db->quoteIdentifier($this->data['resourceConfig']['dbname']), - $db->quoteIdentifier($this->data['resourceConfig']['username']), - $db->quoteIdentifier(Platform::getFqdn()) + $db->quoteIdentifier($this->data['resourceConfig']['username']) )); } else { $this->log( @@ -115,7 +117,10 @@ class DatabaseStep extends Step $db->reconnect($this->data['resourceConfig']['dbname']); } - if ($db->hasLogin($this->data['resourceConfig']['username'])) { + if ($db->hasLogin( + $this->data['resourceConfig']['username'], + $this->data['resourceConfig']['password'] + )) { $this->log(t('Login "%s" already exists...'), $this->data['resourceConfig']['username']); } else { $this->log(t('Creating login "%s"...'), $this->data['resourceConfig']['username']); @@ -129,8 +134,12 @@ class DatabaseStep extends Step $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/pgsql.sql'); } - $privileges = array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); - if ($db->checkPrivileges(array_merge($privileges, array('GRANT OPTION')))) { + $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", @@ -182,7 +191,10 @@ class DatabaseStep extends Step } catch (PDOException $e) { try { $db->connectToHost(); - if ($db->hasLogin($this->data['resourceConfig']['username'])) { + if ($db->hasLogin( + $this->data['resourceConfig']['username'], + $this->data['resourceConfig']['password'] + )) { $message = sprintf( t( 'The database user "%s" will be used to create the missing ' diff --git a/library/Icinga/Application/WebSetup.php b/library/Icinga/Application/WebSetup.php index c14526296..f21b3ef81 100644 --- a/library/Icinga/Application/WebSetup.php +++ b/library/Icinga/Application/WebSetup.php @@ -39,21 +39,18 @@ class WebSetup extends Wizard implements SetupWizard /** * The privileges required by Icinga Web 2 to setup the database * - * @todo This list is not intended to be exhaustive nor being correct. (Driver compatibilitiy, Necessity, ...) - * * @var array */ protected $databaseSetupPrivileges = array( - 'USAGE', 'CREATE', - 'ALTER', + 'SELECT', 'INSERT', 'UPDATE', 'DELETE', - 'TRUNCATE', 'REFERENCES', - 'CREATE USER', - 'GRANT OPTION' + 'EXECUTE', + 'CREATE TEMPORARY TABLES', + 'CREATE USER' ); /** diff --git a/library/Icinga/Web/Setup/DbTool.php b/library/Icinga/Web/Setup/DbTool.php index 97975f2e4..650cd7bda 100644 --- a/library/Icinga/Web/Setup/DbTool.php +++ b/library/Icinga/Web/Setup/DbTool.php @@ -4,6 +4,7 @@ namespace Icinga\Web\Setup; +use Icinga\Exception\ProgrammingError; use PDO; use PDOException; use LogicException; @@ -57,7 +58,18 @@ class DbTool public function connectToHost() { $this->assertHostAccess(); - $this->connect(); + + if ($this->config['db'] == 'pgsql') { + // PostgreSQL requires us to specify a database on each connection and will use + // 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. + $this->connect('postgres'); + + } else { + $this->connect(); + } return $this; } @@ -351,9 +363,13 @@ class DbTool * * @return bool */ - public function checkPrivileges(array $privileges) + public function checkPrivileges(array $privileges, $table = null) { - return true; // TODO(7163): Implement privilege checks + if ($this->config['db'] === 'mysql') { + return $this->checkMysqlPriv($privileges); + } else { + return $this->checkPgsqlPriv($privileges, $table); + } } /** @@ -371,16 +387,30 @@ class DbTool * Return whether the given database login exists * * @param string $username The username to search + * @param string $password The username to login * * @return bool + * @throws PDOException When logging in is not possible due to an Exception not related + * to authentication */ - public function hasLogin($username) + public function hasLogin($username, $password) { if ($this->config['db'] === 'mysql') { - $rowCount = $this->exec( - 'SELECT grantee FROM information_schema.user_privileges WHERE grantee = :ident LIMIT 1', - array(':ident' => "'" . $username . "'@'" . Platform::getFqdn() . "'") - ); + // 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) { + $code = $e->getCode(); + if ($code === 1045 || $code === 1044) { + return false; + } + throw $e; + } + return true; } elseif ($this->config['db'] === 'pgsql') { $rowCount = $this->exec( 'SELECT usename FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1', @@ -402,14 +432,155 @@ class DbTool if ($this->config['db'] === 'mysql') { $this->exec( 'CREATE USER :user@:host IDENTIFIED BY :passw', - array(':user' => $username, ':host' => Platform::getFqdn(), ':passw' => $password) + array(':user' => $username, ':host' => '%', ':passw' => $password) ); + return true; } elseif ($this->config['db'] === 'pgsql') { $this->exec(sprintf( 'CREATE USER %s WITH PASSWORD %s', $this->quoteIdentifier($username), $this->quote($password) )); + return true; } } + + /** + * Check whether the current role has GRANT permissions + * + * @param array $privileges + * @param $database + */ + public function checkMysqlGrantOption(array $privileges) + { + return $this->checkMysqlPriv($privileges, true); + } + + /** + * 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 + ); + + return $cnt === $rows; + } + + /** + * Check whether the current role has GRANT permissions for the given database name + * + * For Postgres, this will be assumed as true when: + *