diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
commit | 3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch) | |
tree | b01f3923360c20a6a504aff42d45670c58af3ec5 /modules/setup/library/Setup/Utils | |
parent | Initial commit. (diff) | |
download | icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.tar.xz icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.zip |
Adding upstream version 2.12.1.upstream/2.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/setup/library/Setup/Utils')
-rw-r--r-- | modules/setup/library/Setup/Utils/DbTool.php | 950 | ||||
-rw-r--r-- | modules/setup/library/Setup/Utils/EnableModuleStep.php | 77 |
2 files changed, 1027 insertions, 0 deletions
diff --git a/modules/setup/library/Setup/Utils/DbTool.php b/modules/setup/library/Setup/Utils/DbTool.php new file mode 100644 index 0000000..7578462 --- /dev/null +++ b/modules/setup/library/Setup/Utils/DbTool.php @@ -0,0 +1,950 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Utils; + +use PDO; +use PDOException; +use LogicException; +use Zend_Db_Adapter_Pdo_Abstract; +use Zend_Db_Adapter_Pdo_Mysql; +use Zend_Db_Adapter_Pdo_Pgsql; +use Icinga\Util\File; +use Icinga\Exception\ConfigurationError; + +/** + * Utility class to ease working with databases when setting up Icinga Web 2 or one of its modules + */ +class DbTool +{ + /** + * The PDO database connection + * + * @var PDO + */ + protected $pdoConn; + + /** + * The Zend database adapter + * + * @var Zend_Db_Adapter_Pdo_Abstract + */ + protected $zendConn; + + /** + * The resource configuration + * + * @var array + */ + 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' => 12, + '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, + 'CREATE' => 13, + 'CONNECT' => 4, + 'TEMPORARY' => 4, + 'TEMP' => 4, + 'EXECUTE' => 32, + 'USAGE' => 33, + 'CREATEROLE' => 1 + ); + + /** + * Create a new DbTool + * + * @param array $config The resource configuration to use + */ + public function __construct(array $config) + { + if (! isset($config['port'])) { + // TODO: This is not quite correct, but works as it previously did. Previously empty values were not + // transformed no NULL (now they are) so if the port is now null, it's been the empty string. + $config['port'] = ''; + } + + $this->config = $config; + } + + /** + * Connect to the server + * + * @return $this + */ + public function connectToHost() + { + $this->assertHostAccess(); + + 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. (http://stackoverflow.com/q/4483139) + $this->connect('postgres'); + } else { + $this->connect(); + } + + return $this; + } + + /** + * Connect to the database + * + * @return $this + */ + public function connectToDb() + { + $this->assertHostAccess(); + $this->assertDatabaseAccess(); + $this->connect($this->config['dbname']); + return $this; + } + + /** + * Assert that all configuration values exist that are required to connect to a server + * + * @throws ConfigurationError + */ + protected function assertHostAccess() + { + if (! isset($this->config['db'])) { + throw new ConfigurationError('Can\'t connect to database server of unknown type'); + } elseif (! isset($this->config['host'])) { + throw new ConfigurationError('Can\'t connect to database server without a hostname or address'); + } elseif (! isset($this->config['port'])) { + throw new ConfigurationError('Can\'t connect to database server without a port'); + } elseif (! isset($this->config['username'])) { + throw new ConfigurationError('Can\'t connect to database server without a username'); + } elseif (! isset($this->config['password'])) { + throw new ConfigurationError('Can\'t connect to database server without a password'); + } + } + + /** + * Assert that all configuration values exist that are required to connect to a database + * + * @throws ConfigurationError + */ + protected function assertDatabaseAccess() + { + if (! isset($this->config['dbname'])) { + throw new ConfigurationError('Can\'t connect to database without a valid database name'); + } + } + + /** + * Assert that a connection with a database has been established + * + * @throws LogicException + */ + protected function assertConnectedToDb() + { + if ($this->zendConn === null) { + throw new LogicException('Not connected to database'); + } + } + + /** + * Return whether a connection with the server has been established + * + * @return bool + */ + public function isConnected() + { + return $this->pdoConn !== null; + } + + /** + * Establish a connection with the database or just the server by omitting the database name + * + * @param string $dbname The name of the database to connect to + */ + public function connect($dbname = null) + { + $this->pdoConnect($dbname); + if ($dbname !== null) { + $this->zendConnect($dbname); + $this->dbFromConfig = $dbname === $this->config['dbname']; + } + } + + /** + * Reestablish a connection with the database or just the server by omitting the database name + * + * @param string $dbname The name of the database to connect to + */ + public function reconnect($dbname = null) + { + $this->pdoConn = null; + $this->zendConn = null; + $this->connect($dbname); + } + + /** + * Initialize Zend database adapter + * + * @param string $dbname The name of the database to connect with + * + * @throws ConfigurationError In case the resource type is not a supported PDO driver name + */ + private function zendConnect($dbname) + { + if ($this->zendConn !== null) { + return; + } + + $config = array( + 'dbname' => $dbname, + 'host' => $this->config['host'], + 'port' => $this->config['port'], + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ); + + if ($this->config['db'] === 'mysql') { + if (isset($this->config['use_ssl']) && $this->config['use_ssl']) { + $this->config['driver_options'] = array(); + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config['ssl_key']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key']; + } + if ($this->config['ssl_cert']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert']; + } + if ($this->config['ssl_ca']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca']; + } + if ($this->config['ssl_capath']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath']; + } + if ($this->config['ssl_cipher']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher']; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config['ssl_do_not_verify_server_cert'] + ) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + $this->zendConn = new Zend_Db_Adapter_Pdo_Mysql($config); + } elseif ($this->config['db'] === 'pgsql') { + $this->zendConn = new Zend_Db_Adapter_Pdo_Pgsql($config); + } else { + throw new ConfigurationError( + 'Failed to connect to database. Unsupported PDO driver "%s"', + $this->config['db'] + ); + } + + $this->zendConn->getConnection(); // Force connection attempt + } + + /** + * Initialize PDO connection + * + * @param string $dbname The name of the database to connect with + */ + private function pdoConnect($dbname) + { + if ($this->pdoConn !== null) { + return; + } + + $driverOptions = array( + PDO::ATTR_TIMEOUT => 1, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + + if ($this->config['db'] === 'mysql' + && isset($this->config['use_ssl']) + && $this->config['use_ssl'] + ) { + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config['ssl_key']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key']; + } + if ($this->config['ssl_cert']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert']; + } + if ($this->config['ssl_ca']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca']; + } + if ($this->config['ssl_capath']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath']; + } + if ($this->config['ssl_cipher']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher']; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config['ssl_do_not_verify_server_cert'] + ) { + $driverOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + + $this->pdoConn = new PDO( + $this->buildDsn($this->config['db'], $dbname), + $this->config['username'], + $this->config['password'], + $driverOptions + ); + } + + /** + * Return a datasource name for the given database type and name + * + * @param string $dbtype + * @param string $dbname + * + * @return string + * + * @throws ConfigurationError In case the passed database type is not supported + */ + protected function buildDsn($dbtype, $dbname = null) + { + if ($dbtype === 'mysql') { + return 'mysql:host=' . $this->config['host'] . ';port=' . $this->config['port'] + . ($dbname !== null ? ';dbname=' . $dbname : ''); + } elseif ($dbtype === 'pgsql') { + return 'pgsql:host=' . $this->config['host'] . ';port=' . $this->config['port'] + . ($dbname !== null ? ';dbname=' . $dbname : ''); + } else { + throw new ConfigurationError( + 'Failed to build data source name. Unsupported PDO driver "%s"', + $dbtype + ); + } + } + + /** + * Try to connect to the server and throw an exception if this fails + * + * @throws PDOException In case an error occurs that does not indicate that authentication failed + */ + public function checkConnectivity() + { + try { + $this->connectToHost(); + } catch (PDOException $e) { + if ($this->config['db'] === 'mysql') { + $code = $e->getCode(); + /* + * 1040 .. Too many connections + * 1045 .. Access denied for user '%s'@'%s' (using password: %s) + * 1698 .. Access denied for user '%s'@'%s' + */ + if ($code !== 1040 && $code !== 1045 && $code !== 1698) { + throw $e; + } + } elseif ($this->config['db'] === 'pgsql') { + if (strpos($e->getMessage(), $this->config['username']) === false) { + throw $e; + } + } + } + } + + /** + * Return the given identifier escaped with backticks + * + * @param string $identifier The identifier to escape + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function quoteIdentifier($identifier) + { + if ($this->config['db'] === 'mysql') { + return '`' . str_replace('`', '``', $identifier) . '`'; + } elseif ($this->config['db'] === 'pgsql') { + return '"' . str_replace('"', '""', $identifier) . '"'; + } else { + throw new LogicException('Unable to quote identifier.'); + } + } + + /** + * Return the given table name with all wildcards being escaped + * + * @param string $tableName + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function escapeTableWildcards($tableName) + { + if ($this->config['db'] === 'mysql') { + return str_replace(array('_', '%'), array('\_', '\%'), $tableName); + } + + throw new LogicException('Unable to escape table wildcards.'); + } + + /** + * Return the given value escaped as string + * + * @param mixed $value The value to escape + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function quote($value) + { + $quoted = $this->pdoConn->quote($value); + if ($quoted === false) { + throw new LogicException(sprintf('Unable to quote value: %s', $value)); + } + + return $quoted; + } + + /** + * Execute a SQL statement and return the affected row count + * + * Use $params to use a prepared statement. + * + * @param string $statement The statement to execute + * @param array $params The params to bind + * + * @return int + */ + public function exec($statement, $params = array()) + { + if (empty($params)) { + return $this->pdoConn->exec($statement); + } + + $stmt = $this->pdoConn->prepare($statement); + $stmt->execute($params); + return $stmt->rowCount(); + } + + /** + * Execute a SQL statement and return the result + * + * Use $params to use a prepared statement. + * + * @param string $statement The statement to execute + * @param array $params The params to bind + * + * @return mixed + */ + public function query($statement, $params = array()) + { + if ($this->zendConn !== null) { + return $this->zendConn->query($statement, $params); + } + + if (empty($params)) { + return $this->pdoConn->query($statement); + } + + $stmt = $this->pdoConn->prepare($statement); + $stmt->execute($params); + return $stmt; + } + + /** + * Return the version of the server currently connected to + * + * @return string|null + */ + public function getServerVersion() + { + if ($this->config['db'] === 'mysql') { + return $this->query('show variables like "version"')->fetchColumn(1) ?: null; + } elseif ($this->config['db'] === 'pgsql') { + return $this->query('show server_version')->fetchColumn() ?: null; + } else { + throw new LogicException( + sprintf('Unable to fetch the server\'s version. Unsupported PDO driver "%s"', $this->config['db']) + ); + } + } + + /** + * Import the given SQL file + * + * @param string $filepath The file to import + */ + public function import($filepath) + { + $file = new File($filepath); + $content = join(PHP_EOL, iterator_to_array($file)); // There is no fread() before PHP 5.5 :( + + foreach (preg_split('@;(?! \\\\)@', $content) as $statement) { + if (($statement = trim($statement)) !== '') { + $this->exec($statement); + } + } + } + + /** + * 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, array $context = null, $username = null) + { + if ($this->config['db'] === 'mysql') { + 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()); + $quotedDbName = $this->quoteIdentifier($this->config['dbname']); + + $grant = 'GRANT %s'; + $on = ' ON %s.%s'; + $to = sprintf( + ' TO %s@%s', + $this->quoteIdentifier($username), + $this->quoteIdentifier($host) + ); + + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($tablePrivileges)) { + $tableGrant = sprintf($grant, join(',', $tablePrivileges)); + foreach ($context as $table) { + $this->exec($tableGrant . sprintf($on, $quotedDbName, $this->quoteIdentifier($table)) . $to); + } + } + + if (! empty($dbPrivileges)) { + $this->exec( + sprintf($grant, join(',', $dbPrivileges)) + . sprintf($on, $this->escapeTableWildcards($quotedDbName), '*') + . $to + ); + } + } elseif ($this->config['db'] === 'pgsql') { + $dbPrivileges = array(); + $schemaPrivileges = []; + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + + if ($this->pgsqlGrantContexts[$privilege] & static::GLOBAL_LEVEL) { + $schemaPrivileges[] = $privilege; + } + } + + if (! empty($schemaPrivileges)) { + // Allow the user to create,alter and use all attribute types in schema public + // such as creating and dropping custom data types (boolenum) + $this->exec(sprintf('GRANT %s ON SCHEMA public TO %s', implode(',', $schemaPrivileges), $username)); + } + + if (! empty($dbPrivileges)) { + $this->exec(sprintf( + 'GRANT %s ON DATABASE %s TO %s', + join(',', $dbPrivileges), + $this->config['dbname'], + $username + )); + } + + foreach ($context as $table) { + // PostgreSQL documentation says "You must own the table to use ALTER TABLE.", hence it isn't + // sufficient to just issue grants, as the user is still not allowed to alter that table. + $this->exec(sprintf('ALTER TABLE %s OWNER TO %s', $table, $username)); + } + } + } + + /** + * Return a list of all existing database tables + * + * @return array + */ + public function listTables() + { + $this->assertConnectedToDb(); + return $this->zendConn->listTables(); + } + + /** + * Return whether the given database login exists + * + * @param string $username The username to search + * + * @return ?bool + */ + public function hasLogin($username) + { + if ($this->config['db'] === 'mysql') { + $queryString = <<<EOD +SELECT 1 + FROM information_schema.user_privileges + WHERE grantee = REPLACE(CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'"), :current, :wanted) +EOD; + + $query = $this->query( + $queryString, + array( + ':current' => $this->config['username'], + ':wanted' => $username + ) + ); + return count($query->fetchAll()) > 0; + } elseif ($this->config['db'] === 'pgsql') { + $query = $this->query( + 'SELECT 1 FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1', + array(':ident' => $username) + ); + return count($query->fetchAll()) === 1; + } + } + + /** + * Add a new database login + * + * @param string $username The username of the new login + * @param string $password The password of the new login + */ + public function addLogin($username, $password) + { + if ($this->config['db'] === 'mysql') { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $this->exec( + 'CREATE USER :user@:host IDENTIFIED BY :passw', + array(':user' => $username, ':host' => $host, ':passw' => $password) + ); + } elseif ($this->config['db'] === 'pgsql') { + $this->exec(sprintf( + 'CREATE USER %s WITH PASSWORD %s', + $this->quoteIdentifier($username), + $this->quote($password) + )); + } + } + + /** + * Check whether the current user has 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 + */ + protected function checkMysqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $mysqlPrivileges = array_intersect($privileges, array_keys($this->mysqlGrantContexts)); + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $grantee = "'" . ($username === null ? $this->config['username'] : $username) . "'@'" . $host . "'"; + + if (isset($this->config['dbname'])) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach ($mysqlPrivileges as $privilege) { + if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } + if ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + $dbPrivilegesGranted = true; + $tablePrivilegesGranted = true; + + if (! empty($dbPrivileges)) { + $queryString = 'SELECT COUNT(*) as matches' + . ' FROM information_schema.schema_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND privilege_type IN (%s)' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''); + + $dbAndTableQuery = $this->query( + sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbPrivileges))), + array(':grantee' => $grantee, ':dbname' => $this->escapeTableWildcards($this->config['dbname'])) + ); + $grantedDbAndTablePrivileges = (int) $dbAndTableQuery->fetchObject()->matches; + if ($grantedDbAndTablePrivileges === count($dbPrivileges)) { + $tableExclusivePrivileges = array_diff($tablePrivileges, $dbPrivileges); + if (! empty($tableExclusivePrivileges)) { + $tablePrivileges = $tableExclusivePrivileges; + $tablePrivilegesGranted = false; + } + } else { + $tablePrivilegesGranted = false; + $dbExclusivePrivileges = array_diff($dbPrivileges, $tablePrivileges); + if (! empty($dbExclusivePrivileges)) { + $dbExclusiveQuery = $this->query( + sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbExclusivePrivileges))), + array( + ':grantee' => $grantee, + ':dbname' => $this->escapeTableWildcards($this->config['dbname']) + ) + ); + $dbPrivilegesGranted = (int) $dbExclusiveQuery->fetchObject()->matches === count( + $dbExclusivePrivileges + ); + } + } + } + + if (! $tablePrivilegesGranted && !empty($tablePrivileges)) { + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.table_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')' + . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $tablePrivileges)) . ')' + . ($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; + } + } + + $query = $this->query( + 'SELECT COUNT(*) as matches FROM information_schema.user_privileges WHERE grantee = :grantee' + . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $mysqlPrivileges)) . ')' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee) + ); + return (int) $query->fetchObject()->matches === count($mysqlPrivileges); + } + + /** + * Check whether the current user has the given privileges + * + * 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 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 checkPgsqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $privilegesGranted = true; + $owner = $username ?: $this->config['username']; + $isSuperUser = $this->query('select rolsuper from pg_roles where rolname = :user', [':user' => $owner]) + ->fetchColumn(); + + if ($this->dbFromConfig) { + $schemaPrivileges = []; + $dbPrivileges = array(); + if (! $isSuperUser) { + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + if ($this->pgsqlGrantContexts[$privilege] & static::GLOBAL_LEVEL) { + $schemaPrivileges[] = $privilege; + } + } + + if (! empty($schemaPrivileges)) { + foreach ($schemaPrivileges as $schemaPrivilege) { + $query = $this->query( + 'SELECT has_schema_privilege(:user, :schema, :privilege) AS db_privilege_granted', + [ + ':user' => $owner, + ':schema' => 'public', + ':privilege' => $schemaPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '') + ] + ); + + if (! $query->fetchObject()->db_privilege_granted) { + // The user doesn't fully have the provided privileges. + $privilegesGranted = false; + break; + } + } + } + + if ($privilegesGranted && ! empty($dbPrivileges)) { + foreach ($dbPrivileges as $dbPrivilege) { + $query = $this->query( + 'SELECT has_database_privilege(:user, :dbname, :privilege) AS db_privilege_granted', + array( + ':user' => $owner, + ':dbname' => $this->config['dbname'], + ':privilege' => $dbPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + if (! $query->fetchObject()->db_privilege_granted) { + // The user doesn't fully have the provided privileges. + $privilegesGranted = false; + break; + } + } + } + + if ($privilegesGranted && ! empty($context)) { + foreach (array_intersect($context, $this->listTables()) as $table) { + $query = $this->query( + 'SELECT tableowner FROM pg_catalog.pg_tables WHERE tablename = :tablename', + [':tablename' => $table] + ); + + if ($query->fetchColumn() !== $owner) { + $privilegesGranted = false; + break; + } + } + } + } + } 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 (in_array('CREATE', $privileges, true)) { + $query = $this->query( + 'select rolcreatedb from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted = $query->fetchColumn() !== false; + } + } + + if ($privilegesGranted && in_array('CREATEROLE', $privileges, true)) { + $query = $this->query( + 'select rolcreaterole from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted = $query->fetchColumn() !== false; + } + + if ($privilegesGranted && in_array('SUPER', $privileges, true)) { + $privilegesGranted = $isSuperUser === true; + } + + return $privilegesGranted; + } +} diff --git a/modules/setup/library/Setup/Utils/EnableModuleStep.php b/modules/setup/library/Setup/Utils/EnableModuleStep.php new file mode 100644 index 0000000..92af5b7 --- /dev/null +++ b/modules/setup/library/Setup/Utils/EnableModuleStep.php @@ -0,0 +1,77 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Utils; + +use Exception; +use Icinga\Application\Icinga; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class EnableModuleStep extends Step +{ + protected $modulePaths; + + protected $moduleNames; + + protected $errors; + + protected $warnings; + + public function __construct(array $moduleNames) + { + $this->moduleNames = $moduleNames; + + $this->modulePaths = array(); + if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) { + $this->modulePaths[] = $appModulePath; + } + } + + public function apply() + { + $moduleManager = Icinga::app()->getModuleManager(); + $moduleManager->detectInstalledModules($this->modulePaths); + + $success = true; + foreach ($this->moduleNames as $moduleName) { + try { + $moduleManager->enableModule($moduleName); + } catch (ConfigurationError $e) { + $this->warnings[$moduleName] = $e; + } catch (Exception $e) { + $this->errors[$moduleName] = $e; + $success = false; + } + } + + return $success; + } + + public function getSummary() + { + // Enabling a module is like a implicit action, which does not need to be shown to the user... + } + + public function getReport() + { + $okMessage = mt('setup', 'Module "%s" has been successfully enabled.'); + $failMessage = mt('setup', 'Module "%s" could not be enabled. An error occured:'); + + $report = array(); + foreach ($this->moduleNames as $moduleName) { + if (isset($this->errors[$moduleName])) { + $report[] = sprintf($failMessage, $moduleName); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->errors[$moduleName])); + } elseif (isset($this->warnings[$moduleName])) { + $report[] = sprintf($failMessage, $moduleName); + $report[] = sprintf(mt('setup', 'WARNING: %s'), $this->warnings[$moduleName]->getMessage()); + } else { + $report[] = sprintf($okMessage, $moduleName); + } + } + + return $report; + } +} |