From 581935c26fab2d01c423821161a90e5a431b98e8 Mon Sep 17 00:00:00 2001
From: Johannes Meyer <johannes.meyer@netways.de>
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 = <<<EOD
+SELECT 1
+ FROM information_schema.user_privileges
+ WHERE grantee = REPLACE(CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'"), :current, :wanted)
+EOD;
 
-            return true;
+            $query = $this->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:
-     * <ul>
-     *  <li>The role can create new databases and the database does <b>not</b> yet exist </li>
-     *  <li>The database exists but the current role is the owner of it</li>
-     *  <li>The database exists but the role has superuser permissions</li>
-     *  <li>The role does not own the database, but has the necessary grants on it</li>
-     * </ul>
-     * 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;
     }
 }