From 8058eb021516bbc15c77e6fcebdad709f9c9ed0f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 21 Apr 2015 12:32:18 +0200 Subject: [PATCH 001/239] Move UserGroupBackend class to Icinga\Authentication\UserGroup refs #8826 --- library/Icinga/Authentication/Backend/DbUserGroupBackend.php | 2 +- library/Icinga/Authentication/Backend/IniUserGroupBackend.php | 2 +- library/Icinga/Authentication/Manager.php | 1 + .../Icinga/Authentication/{ => UserGroup}/UserGroupBackend.php | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename library/Icinga/Authentication/{ => UserGroup}/UserGroupBackend.php (98%) diff --git a/library/Icinga/Authentication/Backend/DbUserGroupBackend.php b/library/Icinga/Authentication/Backend/DbUserGroupBackend.php index d2230bf04..9b744257e 100644 --- a/library/Icinga/Authentication/Backend/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/Backend/DbUserGroupBackend.php @@ -3,7 +3,7 @@ namespace Icinga\Authentication\Backend; -use Icinga\Authentication\UserGroupBackend; +use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Data\Db\DbConnection; use Icinga\User; diff --git a/library/Icinga/Authentication/Backend/IniUserGroupBackend.php b/library/Icinga/Authentication/Backend/IniUserGroupBackend.php index b7f366511..ff5cebefe 100644 --- a/library/Icinga/Authentication/Backend/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/Backend/IniUserGroupBackend.php @@ -4,7 +4,7 @@ namespace Icinga\Authentication\Backend; use Icinga\Application\Config; -use Icinga\Authentication\UserGroupBackend; +use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Exception\ConfigurationError; use Icinga\User; use Icinga\Util\String; diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index 993475636..9d86b6ad6 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -4,6 +4,7 @@ namespace Icinga\Authentication; use Exception; +use Icinga\Authentication\Usergroup\UserGroupBackend; use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Exception\NotReadableError; diff --git a/library/Icinga/Authentication/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php similarity index 98% rename from library/Icinga/Authentication/UserGroupBackend.php rename to library/Icinga/Authentication/UserGroup/UserGroupBackend.php index f1289220d..9535d2be0 100644 --- a/library/Icinga/Authentication/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -1,7 +1,7 @@ Date: Tue, 21 Apr 2015 12:38:57 +0200 Subject: [PATCH 002/239] Move concrete UserGroupBackend classes to Icinga\Authentication\UserGroup refs #8826 --- .../{Backend => UserGroup}/DbUserGroupBackend.php | 3 +-- .../{Backend => UserGroup}/IniUserGroupBackend.php | 3 +-- library/Icinga/Authentication/UserGroup/UserGroupBackend.php | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) rename library/Icinga/Authentication/{Backend => UserGroup}/DbUserGroupBackend.php (94%) rename library/Icinga/Authentication/{Backend => UserGroup}/IniUserGroupBackend.php (94%) diff --git a/library/Icinga/Authentication/Backend/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php similarity index 94% rename from library/Icinga/Authentication/Backend/DbUserGroupBackend.php rename to library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index 9b744257e..a5a2a539a 100644 --- a/library/Icinga/Authentication/Backend/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -1,9 +1,8 @@ Date: Tue, 21 Apr 2015 12:42:21 +0200 Subject: [PATCH 003/239] Move UserGroupBackend to Icinga\Authentication\User refs #8826 --- library/Icinga/Authentication/AuthChain.php | 1 + library/Icinga/Authentication/Backend/DbUserBackend.php | 2 +- library/Icinga/Authentication/Backend/ExternalBackend.php | 2 +- library/Icinga/Authentication/Backend/LdapUserBackend.php | 2 +- library/Icinga/Authentication/{ => User}/UserBackend.php | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) rename library/Icinga/Authentication/{ => User}/UserBackend.php (99%) diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php index 9b9f873c5..167661f39 100644 --- a/library/Icinga/Authentication/AuthChain.php +++ b/library/Icinga/Authentication/AuthChain.php @@ -5,6 +5,7 @@ namespace Icinga\Authentication; use Iterator; use Icinga\Data\ConfigObject; +use Icinga\Authentication\User\UserBackend; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/Backend/DbUserBackend.php index 12fa0392f..de3489fc3 100644 --- a/library/Icinga/Authentication/Backend/DbUserBackend.php +++ b/library/Icinga/Authentication/Backend/DbUserBackend.php @@ -4,7 +4,7 @@ namespace Icinga\Authentication\Backend; use PDO; -use Icinga\Authentication\UserBackend; +use Icinga\Authentication\User\UserBackend; use Icinga\Data\Db\DbConnection; use Icinga\User; use Icinga\Exception\AuthenticationException; diff --git a/library/Icinga/Authentication/Backend/ExternalBackend.php b/library/Icinga/Authentication/Backend/ExternalBackend.php index fb73c55e8..612a66a52 100644 --- a/library/Icinga/Authentication/Backend/ExternalBackend.php +++ b/library/Icinga/Authentication/Backend/ExternalBackend.php @@ -3,7 +3,7 @@ namespace Icinga\Authentication\Backend; -use Icinga\Authentication\UserBackend; +use Icinga\Authentication\User\UserBackend; use Icinga\Data\ConfigObject; use Icinga\User; diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index a392fa682..d9e19588e 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -4,7 +4,7 @@ namespace Icinga\Authentication\Backend; use Icinga\User; -use Icinga\Authentication\UserBackend; +use Icinga\Authentication\User\UserBackend; use Icinga\Protocol\Ldap\Query; use Icinga\Protocol\Ldap\Connection; use Icinga\Exception\AuthenticationException; diff --git a/library/Icinga/Authentication/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php similarity index 99% rename from library/Icinga/Authentication/UserBackend.php rename to library/Icinga/Authentication/User/UserBackend.php index 8e35660df..bee481be6 100644 --- a/library/Icinga/Authentication/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -1,7 +1,7 @@ Date: Tue, 21 Apr 2015 12:51:31 +0200 Subject: [PATCH 004/239] Move concrete UserBackend classes to Icinga\Authentication\User refs #8826 --- application/controllers/AuthenticationController.php | 2 +- application/forms/Config/Authentication/DbBackendForm.php | 2 +- application/forms/Config/Authentication/LdapBackendForm.php | 2 +- .../Icinga/Authentication/{Backend => User}/DbUserBackend.php | 3 +-- .../Authentication/{Backend => User}/ExternalBackend.php | 3 +-- .../Authentication/{Backend => User}/LdapUserBackend.php | 3 +-- library/Icinga/Authentication/User/UserBackend.php | 3 --- modules/setup/application/forms/AdminAccountPage.php | 4 ++-- modules/setup/library/Setup/Steps/AuthenticationStep.php | 2 +- 9 files changed, 9 insertions(+), 15 deletions(-) rename library/Icinga/Authentication/{Backend => User}/DbUserBackend.php (98%) rename library/Icinga/Authentication/{Backend => User}/ExternalBackend.php (95%) rename library/Icinga/Authentication/{Backend => User}/LdapUserBackend.php (98%) diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index 7d492f8c1..4d6fe9928 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -7,7 +7,7 @@ use Icinga\Application\Config; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Authentication\AuthChain; -use Icinga\Authentication\Backend\ExternalBackend; +use Icinga\Authentication\User\ExternalBackend; use Icinga\Exception\AuthenticationException; use Icinga\Exception\ConfigurationError; use Icinga\Exception\NotReadableError; diff --git a/application/forms/Config/Authentication/DbBackendForm.php b/application/forms/Config/Authentication/DbBackendForm.php index e96b9637c..98d955f4a 100644 --- a/application/forms/Config/Authentication/DbBackendForm.php +++ b/application/forms/Config/Authentication/DbBackendForm.php @@ -7,7 +7,7 @@ use Exception; use Icinga\Web\Form; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; -use Icinga\Authentication\Backend\DbUserBackend; +use Icinga\Authentication\User\DbUserBackend; /** * Form class for adding/modifying database authentication backends diff --git a/application/forms/Config/Authentication/LdapBackendForm.php b/application/forms/Config/Authentication/LdapBackendForm.php index 562526788..867f5658c 100644 --- a/application/forms/Config/Authentication/LdapBackendForm.php +++ b/application/forms/Config/Authentication/LdapBackendForm.php @@ -8,7 +8,7 @@ use Icinga\Web\Form; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\AuthenticationException; -use Icinga\Authentication\Backend\LdapUserBackend; +use Icinga\Authentication\User\LdapUserBackend; /** * Form class for adding/modifying LDAP authentication backends diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php similarity index 98% rename from library/Icinga/Authentication/Backend/DbUserBackend.php rename to library/Icinga/Authentication/User/DbUserBackend.php index de3489fc3..e572927d8 100644 --- a/library/Icinga/Authentication/Backend/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -1,10 +1,9 @@ Date: Tue, 21 Apr 2015 13:14:27 +0200 Subject: [PATCH 005/239] UserBackend: Drop abstract method hasUser refs #8826 --- library/Icinga/Authentication/User/UserBackend.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 7d5ebd83f..9842505b0 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -144,15 +144,6 @@ abstract class UserBackend implements Countable return $backend; } - /** - * Test whether the given user exists - * - * @param User $user - * - * @return bool - */ - abstract public function hasUser(User $user); - /** * Authenticate * From 11f522d9292bad2135fca4efd04c55eeeceba188 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 21 Apr 2015 13:14:50 +0200 Subject: [PATCH 006/239] DbUserBackend: Drop redundant method hasUser refs #8826 --- .../Authentication/User/DbUserBackend.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index e572927d8..2286f4e93 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -39,23 +39,6 @@ class DbUserBackend extends UserBackend $this->conn = $conn; } - /** - * Test whether the given user exists - * - * @param User $user - * - * @return bool - */ - public function hasUser(User $user) - { - $select = new Zend_Db_Select($this->conn->getDbAdapter()); - $row = $select->from('icingaweb_user', array(new Zend_Db_Expr(1))) - ->where('name = ?', $user->getUsername()) - ->query()->fetchObject(); - - return ($row !== false) ? true : false; - } - /** * Add a new user * From 60a86546147c761dd6b0b230f9bbcce4011168e3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 21 Apr 2015 13:15:06 +0200 Subject: [PATCH 007/239] ExternalBackend: Drop redundant method hasUser refs #8826 --- .../Authentication/User/ExternalBackend.php | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/library/Icinga/Authentication/User/ExternalBackend.php b/library/Icinga/Authentication/User/ExternalBackend.php index 4861ff7de..4f9dd47b3 100644 --- a/library/Icinga/Authentication/User/ExternalBackend.php +++ b/library/Icinga/Authentication/User/ExternalBackend.php @@ -40,33 +40,6 @@ class ExternalBackend extends UserBackend return 1; } - /** - * Test whether the given user exists - * - * @param User $user - * - * @return bool - */ - public function hasUser(User $user) - { - if (isset($_SERVER['REMOTE_USER'])) { - $username = $_SERVER['REMOTE_USER']; - $user->setRemoteUserInformation($username, 'REMOTE_USER'); - if ($this->stripUsernameRegexp) { - $stripped = preg_replace($this->stripUsernameRegexp, '', $username); - if ($stripped !== false) { - // TODO(el): PHP issues a warning when PHP cannot compile the regular expression. Should we log an - // additional message in that case? - $username = $stripped; - } - } - $user->setUsername($username); - return true; - } - - return false; - } - /** * Authenticate * @@ -77,6 +50,23 @@ class ExternalBackend extends UserBackend */ public function authenticate(User $user, $password = null) { - return $this->hasUser($user); + if (isset($_SERVER['REMOTE_USER'])) { + $username = $_SERVER['REMOTE_USER']; + $user->setRemoteUserInformation($username, 'REMOTE_USER'); + + if ($this->stripUsernameRegexp) { + $stripped = preg_replace($this->stripUsernameRegexp, '', $username); + if ($stripped !== false) { + // TODO(el): PHP issues a warning when PHP cannot compile the regular expression. Should we log an + // additional message in that case? + $username = $stripped; + } + } + + $user->setUsername($username); + return true; + } + + return false; } } From 319ca3625c8f6b78fe0f82d67d66b85291c75914 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 21 Apr 2015 13:15:40 +0200 Subject: [PATCH 008/239] LdapUserBackend: Drop redundant method hasUser refs #8826 --- .../Authentication/User/LdapUserBackend.php | 28 +++---------------- library/Icinga/Protocol/Ldap/Connection.php | 2 +- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 86dd247fc..8bc8f0222 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -185,25 +185,6 @@ class LdapUserBackend extends UserBackend return $groups; } - /** - * Return whether the given user exists - * - * @param User $user - * - * @return bool - */ - public function hasUser(User $user) - { - $username = $user->getUsername(); - $entry = $this->selectUser($username)->fetchOne(); - - if (is_array($entry)) { - return in_array(strtolower($username), array_map('strtolower', $entry)); - } - - return strtolower($entry) === strtolower($username); - } - /** * Return whether the given user credentials are valid * @@ -229,17 +210,16 @@ class LdapUserBackend extends UserBackend } } - if (! $this->hasUser($user)) { - return false; - } - try { $userDn = $this->conn->fetchDN($this->selectUser($user->getUsername())); + if ($userDn === null) { + return false; + } + $authenticated = $this->conn->testCredentials( $userDn, $password ); - if ($authenticated) { $groups = $this->getGroups($userDn); if ($groups !== null) { diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 687f30e62..992a2f553 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -201,7 +201,7 @@ class Connection public function fetchDN(Query $query, $fields = array()) { $rows = $this->fetchAll($query, $fields); - if (count($rows) !== 1) { + if (count($rows) > 1) { throw new LdapException( 'Cannot fetch single DN for %s', $query->create() From 97caeb27f7f4dfc5c160861dec90a7f82f15f21d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 21 Apr 2015 13:59:35 +0200 Subject: [PATCH 009/239] UserBackend: Add missing and fix existing method documentation refs #8826 --- .../Authentication/User/UserBackend.php | 42 ++++++++++++------- library/Icinga/Data/ResourceFactory.php | 9 ++-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 9842505b0..dc88d83d4 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -9,19 +9,22 @@ use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; use Icinga\User; +/** + * Base class for concrete user backends + */ abstract class UserBackend implements Countable { /** - * Name of the backend + * The name of this backend * * @var string */ protected $name; /** - * Setter for the backend's name + * Set this backend's name * - * @param string $name + * @param string $name * * @return $this */ @@ -32,20 +35,31 @@ abstract class UserBackend implements Countable } /** - * Getter for the backend's name + * Return this backend's name * - * @return string + * @return string */ public function getName() { return $this->name; } + /** + * Create and return a UserBackend with the given name and given configuration applied to it + * + * @param string $name + * @param ConfigObject $backendConfig + * + * @return UserBackend + * + * @throws ConfigurationError + */ public static function create($name, ConfigObject $backendConfig) { if ($backendConfig->name !== null) { $name = $backendConfig->name; } + if (isset($backendConfig->class)) { // Use a custom backend class, this is only useful for testing if (!class_exists($backendConfig->class)) { @@ -58,32 +72,27 @@ abstract class UserBackend implements Countable } return new $backendConfig->class($backendConfig); } - if (($backendType = $backendConfig->backend) === null) { + + if (! ($backendType = strtolower($backendConfig->backend))) { throw new ConfigurationError( 'Authentication configuration for backend "%s" is missing the backend directive', $name ); } - $backendType = strtolower($backendType); if ($backendType === 'external') { $backend = new ExternalBackend($backendConfig); $backend->setName($name); return $backend; } + if ($backendConfig->resource === null) { throw new ConfigurationError( 'Authentication configuration for backend "%s" is missing the resource directive', $name ); } - try { - $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource); - } catch (ProgrammingError $e) { - throw new ConfigurationError( - 'Resources not set up. Please contact your Icinga Web administrator' - ); - } - $resource = ResourceFactory::createResource($resourceConfig); + $resource = ResourceFactory::createResource(ResourceFactory::getResourceConfig($backendConfig->resource)); + switch ($backendType) { case 'db': $backend = new DbUserBackend($resource); @@ -140,12 +149,13 @@ abstract class UserBackend implements Countable $backendType ); } + $backend->setName($name); return $backend; } /** - * Authenticate + * Authenticate the given user * * @param User $user * @param string $password diff --git a/library/Icinga/Data/ResourceFactory.php b/library/Icinga/Data/ResourceFactory.php index 249a70958..add35baac 100644 --- a/library/Icinga/Data/ResourceFactory.php +++ b/library/Icinga/Data/ResourceFactory.php @@ -4,7 +4,6 @@ namespace Icinga\Data; use Icinga\Application\Config; -use Icinga\Exception\ProgrammingError; use Icinga\Util\ConfigAwareFactory; use Icinga\Exception\ConfigurationError; use Icinga\Data\Db\DbConnection; @@ -70,13 +69,13 @@ class ResourceFactory implements ConfigAwareFactory /** * Check if the existing resources are set. If not, throw an error. * - * @throws ProgrammingError + * @throws ConfigurationError */ private static function assertResourcesExist() { - if (!isset(self::$resources)) { - throw new ProgrammingError( - 'The ResourceFactory must be initialised by setting a config, before it can be used' + if (self::$resources === null) { + throw new ConfigurationError( + 'Resources not set up. Please contact your Icinga Web administrator' ); } } From b45e576722aea29d81654d903ff8c84203adb35f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 21 Apr 2015 14:15:43 +0200 Subject: [PATCH 010/239] UserBackend: Remove testing only related code There are no tests for this class at all. --- library/Icinga/Authentication/User/UserBackend.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index dc88d83d4..0f25e565f 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -60,19 +60,6 @@ abstract class UserBackend implements Countable $name = $backendConfig->name; } - if (isset($backendConfig->class)) { - // Use a custom backend class, this is only useful for testing - if (!class_exists($backendConfig->class)) { - throw new ConfigurationError( - 'Authentication configuration for backend "%s" defines an invalid backend class.' - . ' Backend class "%s" not found', - $name, - $backendConfig->class - ); - } - return new $backendConfig->class($backendConfig); - } - if (! ($backendType = strtolower($backendConfig->backend))) { throw new ConfigurationError( 'Authentication configuration for backend "%s" is missing the backend directive', From 33628cbf04e5e401bcc5e5eae288eb7fe983d3a1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 09:06:26 +0200 Subject: [PATCH 011/239] Icinga\Application\Modules\Module: Add missing documentation --- library/Icinga/Application/Modules/Module.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index c1fd84513..80bbcb3e8 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -179,6 +179,8 @@ class Module protected $paneItems = array(); /** + * A set of objects representing a searchUrl configuration + * * @var array */ protected $searchUrls = array(); @@ -201,6 +203,11 @@ class Module $this->searchUrls[] = $searchUrl; } + /** + * Return this module's search urls + * + * @return array + */ public function getSearchUrls() { $this->launchConfigScript(); From 847c02ed8e4f560a9cb532a9ca538662068f8ed1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 09:07:58 +0200 Subject: [PATCH 012/239] UserBackend: Add support for custom authentication backends refs #8826 refs #8877 --- library/Icinga/Application/Modules/Module.php | 32 +++++ .../Authentication/User/UserBackend.php | 112 ++++++++++++++++-- 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 80bbcb3e8..a575b9b9a 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -185,6 +185,13 @@ class Module */ protected $searchUrls = array(); + /** + * This module's user backends providing several authentication mechanisms + * + * @var array + */ + protected $userBackends = array(); + /** * Provide a search URL * @@ -709,6 +716,17 @@ class Module return new $this->setupWizard; } + /** + * Return this module's user backends + * + * @return array + */ + public function getUserBackends() + { + $this->launchConfigScript(); + return $this->userBackends; + } + /** * Provide a named permission * @@ -784,6 +802,20 @@ class Module return $this; } + /** + * Provide a user backend capable of authenticating users + * + * @param string $identifier The identifier of the new backend type + * @param string $className The name of the class + * + * @return $this + */ + protected function provideUserBackend($identifier, $className) + { + $this->userBackends[strtolower($identifier)] = $className; + return $this; + } + /** * Register new namespaces on the autoloader * diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 0f25e565f..68ca10157 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -4,6 +4,8 @@ namespace Icinga\Authentication\User; use Countable; +use Icinga\Application\Logger; +use Icinga\Application\Icinga; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; @@ -14,6 +16,25 @@ use Icinga\User; */ abstract class UserBackend implements Countable { + /** + * The default user backend types provided by Icinga Web 2 + * + * @var array + */ + private static $defaultBackends = array( // I would have liked it if I were able to declare this as constant :'( + 'external', + 'db', + 'ldap', + 'msldap' + ); + + /** + * The registered custom user backends with their identifier as key and class name as value + * + * @var array + */ + protected static $customBackends; + /** * The name of this backend * @@ -44,6 +65,75 @@ abstract class UserBackend implements Countable return $this->name; } + /** + * Fetch all custom user backends from all loaded modules + */ + public static function loadCustomUserBackends() + { + if (static::$customBackends !== null) { + return; + } + + static::$customBackends = array(); + $providedBy = array(); + foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { + foreach ($module->getUserBackends() as $identifier => $className) { + if (array_key_exists($identifier, $providedBy)) { + Logger::warning( + 'Cannot register UserBackend of type "%s" provided by module "%s".' + . ' The type is already provided by module "%s"', + $identifier, + $module->getName(), + $providedBy[$identifier] + ); + } elseif (in_array($identifier, static::$defaultBackends)) { + Logger::warning( + 'Cannot register UserBackend of type "%s" provided by module "%s".' + . ' The type is a default type provided by Icinga Web 2', + $identifier, + $module->getName() + ); + } else { + $providedBy[$identifier] = $module->getName(); + static::$customBackends[$identifier] = $className; + } + } + } + } + + /** + * Validate and return the class for the given custom user backend + * + * @param string $identifier The identifier of the custom user backend + * + * @return string|null The name of the class or null in case there was no + * backend found with the given identifier + * + * @throws ConfigurationError In case the class could not be successfully validated + */ + protected static function getCustomUserBackend($identifier) + { + static::loadCustomUserBackends(); + if (array_key_exists($identifier, static::$customBackends)) { + $className = static::$customBackends[$identifier]; + if (! class_exists($className)) { + throw new ConfigurationError( + 'Cannot utilize UserBackend of type "%s". Class "%s" does not exist', + $identifier, + $className + ); + } elseif (! is_subclass_of($className, __CLASS__)) { + throw new ConfigurationError( + 'Cannot utilize UserBackend of type "%s". Class "%s" is not a sub-type of UserBackend', + $identifier, + $className + ); + } + + return $className; + } + } + /** * Create and return a UserBackend with the given name and given configuration applied to it * @@ -71,6 +161,21 @@ abstract class UserBackend implements Countable $backend->setName($name); return $backend; } + if (in_array($backendType, static::$defaultBackends)) { + // The default backend check is the first one because of performance reasons: + // Do not attempt to load a custom user backend unless it's actually required + } elseif (($customClass = static::getCustomUserBackend($backendType)) !== null) { + $backend = new $customClass($backendConfig); + $backend->setName($name); + return $backend; + } else { + throw new ConfigurationError( + 'Authentication configuration for backend "%s" defines an invalid backend type.' + . ' Backend type "%s" is not supported', + $name, + $backendType + ); + } if ($backendConfig->resource === null) { throw new ConfigurationError( @@ -128,13 +233,6 @@ abstract class UserBackend implements Countable $groupOptions ); break; - default: - throw new ConfigurationError( - 'Authentication configuration for backend "%s" defines an invalid backend type.' - . ' Backend type "%s" is not supported', - $name, - $backendType - ); } $backend->setName($name); From c9dcddb1343f9bd2190863020d5eeabea03385ac Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 09:35:06 +0200 Subject: [PATCH 013/239] UserGroupBackend: Add missing and fix existing method documentation --- .../UserGroup/UserGroupBackend.php | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php index 29ea7094a..1cc7abec5 100644 --- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -6,7 +6,6 @@ namespace Icinga\Authentication\UserGroup; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; -use Icinga\Exception\IcingaException; use Icinga\User; /** @@ -15,16 +14,16 @@ use Icinga\User; abstract class UserGroupBackend { /** - * Name of the backend + * The name of this backend * * @var string */ protected $name; /** - * Set the backend name + * Set this backend's name * - * @param string $name + * @param string $name * * @return $this */ @@ -35,9 +34,9 @@ abstract class UserGroupBackend } /** - * Get the backend name + * Return this backend's name * - * @return string + * @return string */ public function getName() { @@ -45,42 +44,36 @@ abstract class UserGroupBackend } /** - * Create a user group backend + * Create and return a UserGroupBackend with the given name and given configuration applied to it * * @param string $name * @param ConfigObject $backendConfig * - * @return DbUserGroupBackend|IniUserGroupBackend - * @throws ConfigurationError If the backend configuration is invalid + * @return UserGroupBackend + * + * @throws ConfigurationError */ public static function create($name, ConfigObject $backendConfig) { if ($backendConfig->name !== null) { $name = $backendConfig->name; } - if (($backendType = $backendConfig->backend) === null) { + + if (! ($backendType = strtolower($backendConfig->backend))) { throw new ConfigurationError( - 'Configuration for user group backend \'%s\' is missing the \'backend\' directive', + 'Configuration for user group backend "%s" is missing the \'backend\' directive', $name ); } - $backendType = strtolower($backendType); - if (($resourceName = $backendConfig->resource) === null) { + + if ($backendConfig->resource === null) { throw new ConfigurationError( - 'Configuration for user group backend \'%s\' is missing the \'resource\' directive', + 'Configuration for user group backend "%s" is missing the \'resource\' directive', $name ); } - $resourceName = strtolower($resourceName); - try { - $resource = ResourceFactory::create($resourceName); - } catch (IcingaException $e) { - throw new ConfigurationError( - 'Can\'t create user group backend \'%s\'. An exception was thrown: %s', - $resourceName, - $e - ); - } + $resource = ResourceFactory::create($backendConfig->resource); + switch ($backendType) { case 'db': $backend = new DbUserGroupBackend($resource); @@ -90,11 +83,13 @@ abstract class UserGroupBackend break; default: throw new ConfigurationError( - 'Can\'t create user group backend \'%s\'. Invalid backend type \'%s\'.', + 'Configuration for user group backend "%s" defines an invalid backend type.' + . ' Backend type "%s" is not supported', $name, $backendType ); } + $backend->setName($name); return $backend; } @@ -102,7 +97,7 @@ abstract class UserGroupBackend /** * Get the groups the given user is a member of * - * @param User $user + * @param User $user * * @return array */ From a1d8ed6e8fb12fa66efa539474d9831d01830821 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 09:35:41 +0200 Subject: [PATCH 014/239] UserBackend: Utilize ResourceFactory::create --- library/Icinga/Authentication/User/UserBackend.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 68ca10157..40d76f398 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -183,7 +183,7 @@ abstract class UserBackend implements Countable $name ); } - $resource = ResourceFactory::createResource(ResourceFactory::getResourceConfig($backendConfig->resource)); + $resource = ResourceFactory::create($backendConfig->resource); switch ($backendType) { case 'db': From a2cd5d63f157174bb65879fea1330435b335522f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 09:36:45 +0200 Subject: [PATCH 015/239] UserBackend: Wrap config directives as part of errors in single quotes --- library/Icinga/Authentication/User/UserBackend.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 40d76f398..89bd1f56e 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -152,7 +152,7 @@ abstract class UserBackend implements Countable if (! ($backendType = strtolower($backendConfig->backend))) { throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the backend directive', + 'Authentication configuration for backend "%s" is missing the \'backend\' directive', $name ); } @@ -179,7 +179,7 @@ abstract class UserBackend implements Countable if ($backendConfig->resource === null) { throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the resource directive', + 'Authentication configuration for backend "%s" is missing the \'resource\' directive', $name ); } @@ -208,13 +208,14 @@ abstract class UserBackend implements Countable case 'ldap': if ($backendConfig->user_class === null) { throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the user_class directive', + 'Authentication configuration for backend "%s" is missing the \'user_class\' directive', $name ); } if ($backendConfig->user_name_attribute === null) { throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the user_name_attribute directive', + 'Authentication configuration for backend "%s" is' + . ' missing the \'user_name_attribute\' directive', $name ); } From 7960e911a661984a732ae8364ffe0508c89d29ca Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 09:52:08 +0200 Subject: [PATCH 016/239] UserGroupBackend: Add support for custom backends to fetch user groups refs #8826 refs #9122 --- library/Icinga/Application/Modules/Module.php | 32 +++++ .../UserGroup/UserGroupBackend.php | 110 ++++++++++++++++-- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index a575b9b9a..516e6ad0f 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -192,6 +192,13 @@ class Module */ protected $userBackends = array(); + /** + * This module's user group backends + * + * @var array + */ + protected $userGroupBackends = array(); + /** * Provide a search URL * @@ -727,6 +734,17 @@ class Module return $this->userBackends; } + /** + * Return this module's user group backends + * + * @return array + */ + public function getUserGroupBackends() + { + $this->launchConfigScript(); + return $this->userGroupBackends; + } + /** * Provide a named permission * @@ -816,6 +834,20 @@ class Module return $this; } + /** + * Provide a user group backend + * + * @param string $identifier The identifier of the new backend type + * @param string $className The name of the class + * + * @return $this + */ + protected function provideUserGroupBackend($identifier, $className) + { + $this->userGroupBackends[strtolower($identifier)] = $className; + return $this; + } + /** * Register new namespaces on the autoloader * diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php index 1cc7abec5..410b5114d 100644 --- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -3,6 +3,8 @@ namespace Icinga\Authentication\UserGroup; +use Icinga\Application\Logger; +use Icinga\Application\Icinga; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; @@ -13,6 +15,23 @@ use Icinga\User; */ abstract class UserGroupBackend { + /** + * The default user group backend types provided by Icinga Web 2 + * + * @var array + */ + private static $defaultBackends = array( // I would have liked it if I were able to declare this as constant :'( + 'db', + 'ini' + ); + + /** + * The registered custom user group backends with their identifier as key and class name as value + * + * @var array + */ + protected static $customBackends; + /** * The name of this backend * @@ -43,6 +62,75 @@ abstract class UserGroupBackend return $this->name; } + /** + * Fetch all custom user group backends from all loaded modules + */ + public static function loadCustomUserGroupBackends() + { + if (static::$customBackends !== null) { + return; + } + + static::$customBackends = array(); + $providedBy = array(); + foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { + foreach ($module->getUserGroupBackends() as $identifier => $className) { + if (array_key_exists($identifier, $providedBy)) { + Logger::warning( + 'Cannot register UserGroupBackend of type "%s" provided by module "%s".' + . ' The type is already provided by module "%s"', + $identifier, + $module->getName(), + $providedBy[$identifier] + ); + } elseif (in_array($identifier, static::$defaultBackends)) { + Logger::warning( + 'Cannot register UserGroupBackend of type "%s" provided by module "%s".' + . ' The type is a default type provided by Icinga Web 2', + $identifier, + $module->getName() + ); + } else { + $providedBy[$identifier] = $module->getName(); + static::$customBackends[$identifier] = $className; + } + } + } + } + + /** + * Validate and return the class for the given custom user group backend + * + * @param string $identifier The identifier of the custom user group backend + * + * @return string|null The name of the class or null in case there was no + * backend found with the given identifier + * + * @throws ConfigurationError In case the class could not be successfully validated + */ + protected static function getCustomUserGroupBackend($identifier) + { + static::loadCustomUserGroupBackends(); + if (array_key_exists($identifier, static::$customBackends)) { + $className = static::$customBackends[$identifier]; + if (! class_exists($className)) { + throw new ConfigurationError( + 'Cannot utilize UserGroupBackend of type "%s". Class "%s" does not exist', + $identifier, + $className + ); + } elseif (! is_subclass_of($className, __CLASS__)) { + throw new ConfigurationError( + 'Cannot utilize UserGroupBackend of type "%s". Class "%s" is not a sub-type of UserGroupBackend', + $identifier, + $className + ); + } + + return $className; + } + } + /** * Create and return a UserGroupBackend with the given name and given configuration applied to it * @@ -65,6 +153,21 @@ abstract class UserGroupBackend $name ); } + if (in_array($backendType, static::$defaultBackends)) { + // The default backend check is the first one because of performance reasons: + // Do not attempt to load a custom user group backend unless it's actually required + } elseif (($customClass = static::getCustomUserGroupBackend($backendType)) !== null) { + $backend = new $customClass($backendConfig); + $backend->setName($name); + return $backend; + } else { + throw new ConfigurationError( + 'Configuration for user group backend "%s" defines an invalid backend type.' + . ' Backend type "%s" is not supported', + $name, + $backendType + ); + } if ($backendConfig->resource === null) { throw new ConfigurationError( @@ -81,13 +184,6 @@ abstract class UserGroupBackend case 'ini': $backend = new IniUserGroupBackend($resource); break; - default: - throw new ConfigurationError( - 'Configuration for user group backend "%s" defines an invalid backend type.' - . ' Backend type "%s" is not supported', - $name, - $backendType - ); } $backend->setName($name); From adae7b34c1cb5ca03d07ee9de2baa68ae9db094a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 22 Apr 2015 10:36:37 +0200 Subject: [PATCH 017/239] Fix DbBackendFormTest and LdapBackendFormTest refs #8826 --- .../forms/Config/Authentication/DbBackendFormTest.php | 4 ++-- .../forms/Config/Authentication/LdapBackendFormTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php b/test/php/application/forms/Config/Authentication/DbBackendFormTest.php index b7d1ea3c1..010118544 100644 --- a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php +++ b/test/php/application/forms/Config/Authentication/DbBackendFormTest.php @@ -27,7 +27,7 @@ class DbBackendFormTest extends BaseTestCase public function testValidBackendIsValid() { $this->setUpResourceFactoryMock(); - Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend') + Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend') ->shouldReceive('count') ->andReturn(2); @@ -52,7 +52,7 @@ class DbBackendFormTest extends BaseTestCase public function testInvalidBackendIsNotValid() { $this->setUpResourceFactoryMock(); - Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend') + Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend') ->shouldReceive('count') ->andReturn(0); diff --git a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php b/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php index d31033ba9..d933005b7 100644 --- a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php +++ b/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php @@ -28,7 +28,7 @@ class LdapBackendFormTest extends BaseTestCase public function testValidBackendIsValid() { $this->setUpResourceFactoryMock(); - Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend') + Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend') ->shouldReceive('assertAuthenticationPossible')->andReturnNull(); $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]'); @@ -52,7 +52,7 @@ class LdapBackendFormTest extends BaseTestCase public function testInvalidBackendIsNotValid() { $this->setUpResourceFactoryMock(); - Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend') + Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend') ->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException); $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]'); From 3da144f1995623769a809007c4e3d3cce9b00527 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 28 Apr 2015 15:57:33 +0200 Subject: [PATCH 018/239] Revert "Sort LDAP user list" This reverts commit d4dc0177c0880ca3324b6854b219cef2d2ce9300. --- library/Icinga/Protocol/Ldap/Connection.php | 2 +- modules/setup/application/forms/AdminAccountPage.php | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 992a2f553..829545bd8 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -426,7 +426,7 @@ class Connection ldap_control_paged_result($this->ds, 0); } - return $entries; + return $entries; // TODO(7693): Sort entries post-processed } protected function cleanupAttributes($attrs) diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php index 7462b18df..9b1ed06fc 100644 --- a/modules/setup/application/forms/AdminAccountPage.php +++ b/modules/setup/application/forms/AdminAccountPage.php @@ -285,9 +285,7 @@ class AdminAccountPage extends Form } try { - $users = $backend->listUsers(); - natsort ($users); - return $users; + return $backend->listUsers(); } catch (Exception $e) { // No need to handle anything special here. Error means no users found. return array(); From ca5ef2da2bb9ce21701a32a668c149c0608fc8bb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:07:50 +0200 Subject: [PATCH 019/239] Merge Queryable into QueryInterface A *Query*Interface describes an object as being queryable, now. refs #8826 --- library/Icinga/Data/QueryInterface.php | 2 +- library/Icinga/Data/SimpleQuery.php | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Data/QueryInterface.php b/library/Icinga/Data/QueryInterface.php index 4d43d1059..7f82c8c09 100644 --- a/library/Icinga/Data/QueryInterface.php +++ b/library/Icinga/Data/QueryInterface.php @@ -5,4 +5,4 @@ namespace Icinga\Data; use Countable; -interface QueryInterface extends Browsable, Fetchable, Filterable, Limitable, Sortable, Countable {}; +interface QueryInterface extends Queryable, Browsable, Fetchable, Filterable, Limitable, Sortable, Countable {}; diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index 16f82cbe4..ea8eca3d2 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -3,14 +3,13 @@ namespace Icinga\Data; +use Zend_Paginator; use Icinga\Application\Icinga; use Icinga\Data\Filter\Filter; -use Icinga\Web\Paginator\Adapter\QueryAdapter; -use Zend_Paginator; -use Exception; use Icinga\Exception\IcingaException; +use Icinga\Web\Paginator\Adapter\QueryAdapter; -class SimpleQuery implements QueryInterface, Queryable +class SimpleQuery implements QueryInterface { /** * Query data source From e7789ed6403d7e338ac128fc0b80f72c94d9f12a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:12:43 +0200 Subject: [PATCH 020/239] SimpleQuery: Rename unused property `table' to `target' `target' is already in use, but was not declared. `table' was declared but not used anywhere. So `table' is now `target'. --- library/Icinga/Data/SimpleQuery.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index ea8eca3d2..a069e03cd 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -19,9 +19,11 @@ class SimpleQuery implements QueryInterface protected $ds; /** - * The table you are going to query + * The target you are going to query + * + * @var mixed */ - protected $table; + protected $target; /** * The columns you asked for @@ -101,11 +103,14 @@ class SimpleQuery implements QueryInterface } /** - * Choose a table and the colums you are interested in + * Choose a table and the columns you are interested in * - * Query will return all available columns if none are given here + * Query will return all available columns if none are given here. * - * @return $this + * @param mixed $target + * @param array $fields + * + * @return $this */ public function from($target, array $fields = null) { From ecd059dec543795529539fd0aa4db2d928d6bcb7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:13:38 +0200 Subject: [PATCH 021/239] DbConnection: select() returns a DbQuery, not a Query --- library/Icinga/Data/Db/DbConnection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index d1817d24a..83c74fb8e 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -72,7 +72,7 @@ class DbConnection implements Selectable /** * Provide a query on this connection * - * @return Query + * @return DbQuery */ public function select() { From 7178026b8bdf06700a6fecb06f0953a2a6dd9616 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:15:20 +0200 Subject: [PATCH 022/239] Ldap\Connection: Implement interface Selectable refs #8826 --- library/Icinga/Protocol/Ldap/Connection.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 829545bd8..de7d7b628 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -3,12 +3,13 @@ namespace Icinga\Protocol\Ldap; -use Icinga\Exception\ProgrammingError; -use Icinga\Protocol\Ldap\Exception as LdapException; -use Icinga\Application\Platform; use Icinga\Application\Config; use Icinga\Application\Logger; +use Icinga\Application\Platform; use Icinga\Data\ConfigObject; +use Icinga\Data\Selectable; +use Icinga\Exception\ProgrammingError; +use Icinga\Protocol\Ldap\Exception as LdapException; /** * Backend class managing all the LDAP stuff for you. @@ -24,7 +25,7 @@ use Icinga\Data\ConfigObject; * )); * */ -class Connection +class Connection implements Selectable { const LDAP_NO_SUCH_OBJECT = 32; const LDAP_SIZELIMIT_EXCEEDED = 4; @@ -122,6 +123,11 @@ class Connection return $this->root; } + /** + * Provide a query on this connection + * + * @return Query + */ public function select() { return new Query($this); From 99213432f51aeb22d69a22886f72f5964a13e9c3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:16:16 +0200 Subject: [PATCH 023/239] Ldap\Connection: Rename fetchDN() to fetchDn() We're using CamelCase names for methods. --- library/Icinga/Protocol/Ldap/Connection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index de7d7b628..f7567b6e7 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -204,7 +204,7 @@ class Connection implements Selectable * @return string Returns the distinguished name, or false when the given query yields no results * @throws LdapException When the query result is empty and contains no DN to fetch */ - public function fetchDN(Query $query, $fields = array()) + public function fetchDn(Query $query, $fields = array()) { $rows = $this->fetchAll($query, $fields); if (count($rows) > 1) { From 5baa0590b16db240028ebd577c4b87345e783aa9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:26:27 +0200 Subject: [PATCH 024/239] Ldap\Query: Extend SimpleQuery and add missing documentation refs #8826 refs #8955 --- library/Icinga/Protocol/Ldap/Connection.php | 40 ++- library/Icinga/Protocol/Ldap/Query.php | 340 ++++++------------ .../Icinga/Protocol/Ldap/QueryTest.php | 63 +--- 3 files changed, 135 insertions(+), 308 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index f7567b6e7..6193d630f 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -210,7 +210,7 @@ class Connection implements Selectable if (count($rows) > 1) { throw new LdapException( 'Cannot fetch single DN for %s', - $query->create() + $query ); } return key($rows); @@ -272,16 +272,20 @@ class Connection implements Selectable * @return array The matched entries * @throws LdapException */ - protected function runQuery(Query $query, $fields = array()) + protected function runQuery(Query $query, array $fields = null) { $limit = $query->getLimit(); $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; + if (empty($fields)) { + $fields = $query->getColumns(); + } + $results = @ldap_search( $this->ds, - $query->hasBase() ? $query->getBase() : $this->root_dn, - $query->create(), - empty($fields) ? $query->listFields() : $fields, + $query->getBase() ?: $this->root_dn, + (string) $query, + array_values($fields), 0, // Attributes and values $limit ? $offset + $limit : 0 ); @@ -292,8 +296,8 @@ class Connection implements Selectable throw new LdapException( 'LDAP query "%s" (base %s) failed. Error: %s', - $query->create(), - $query->hasBase() ? $query->getBase() : $this->root_dn, + $query, + $query->getBase() ?: $this->root_dn, ldap_error($this->ds) ); } elseif (ldap_count_entries($this->ds, $results) === 0) { @@ -336,28 +340,29 @@ class Connection implements Selectable * * @param Query $query The query to execute * @param array $fields The fields that will be fetched from the matches - * @param int $page_size The maximum page size, defaults to Connection::PAGE_SIZE + * @param int $pageSize The maximum page size, defaults to Connection::PAGE_SIZE * * @return array The matched entries * @throws LdapException * @throws ProgrammingError When executed without available page controls (check with pageControlAvailable() ) */ - protected function runPagedQuery(Query $query, $fields = array(), $pageSize = null) + protected function runPagedQuery(Query $query, array $fields = null, $pageSize = null) { if (! $this->pageControlAvailable($query)) { throw new ProgrammingError('LDAP: Page control not available.'); } + if (! isset($pageSize)) { $pageSize = static::PAGE_SIZE; } $limit = $query->getLimit(); $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; - $queryString = $query->create(); - $base = $query->hasBase() ? $query->getBase() : $this->root_dn; + $queryString = (string) $query; + $base = $query->getBase() ?: $this->root_dn; if (empty($fields)) { - $fields = $query->listFields(); + $fields = $query->getColumns(); } $count = 0; @@ -368,7 +373,14 @@ class Connection implements Selectable // possibillity server to return an answer in case the pagination extension is missing. ldap_control_paged_result($this->ds, $pageSize, false, $cookie); - $results = @ldap_search($this->ds, $base, $queryString, $fields, 0, $limit ? $offset + $limit : 0); + $results = @ldap_search( + $this->ds, + $base, + $queryString, + array_values($fields), + 0, // Attributes and values + $limit ? $offset + $limit : 0 + ); if ($results === false) { if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { break; @@ -426,7 +438,7 @@ class Connection implements Selectable // pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by // the server: https://www.ietf.org/rfc/rfc2696.txt ldap_control_paged_result($this->ds, 0, false, $cookie); - ldap_search($this->ds, $base, $queryString, $fields); // Returns no entries, due to the page size + ldap_search($this->ds, $base, $queryString); // Returns no entries, due to the page size } else { // Reset the paged search request so that subsequent requests succeed ldap_control_paged_result($this->ds, 0); diff --git a/library/Icinga/Protocol/Ldap/Query.php b/library/Icinga/Protocol/Ldap/Query.php index f82a40511..d923c8ebd 100644 --- a/library/Icinga/Protocol/Ldap/Query.php +++ b/library/Icinga/Protocol/Ldap/Query.php @@ -3,151 +3,151 @@ namespace Icinga\Protocol\Ldap; +use Icinga\Data\SimpleQuery; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\NotImplementedError; + /** - * Search class - * - * @package Icinga\Protocol\Ldap + * LDAP query class */ -/** - * Search abstraction class - * - * Usage example: - * - * - * $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga'); - * - * - * @copyright Copyright (c) 2013 Icinga-Web Team - * @author Icinga-Web Team - * @package Icinga\Protocol\Ldap - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License - */ -class Query +class Query extends SimpleQuery { - protected $connection; - protected $filters = array(); - protected $fields = array(); - protected $limit_count = 0; - protected $limit_offset = 0; - protected $sort_columns = array(); - protected $count; - protected $base; - protected $usePagedResults = true; + /** + * This query's filters + * + * Currently just a basic key/value pair based array. Can be removed once Icinga\Data\Filter is supported. + * + * @var array + */ + protected $filters; /** - * Constructor + * The base dn being used for this query * - * @param Connection LDAP Connection object - * @return void + * @var string */ - public function __construct(Connection $connection) + protected $base; + + /** + * Whether this query is permitted to utilize paged results + * + * @var bool + */ + protected $usePagedResults; + + /** + * Initialize this query + */ + protected function init() { - $this->connection = $connection; + $this->filters = array(); + $this->usePagedResults = true; } + /** + * Set the base dn to be used for this query + * + * @param string $base + * + * @return $this + */ public function setBase($base) { $this->base = $base; return $this; } - public function hasBase() - { - return $this->base !== null; - } - + /** + * Return the base dn being used for this query + * + * @return string + */ public function getBase() { return $this->base; } + /** + * Set whether this query is permitted to utilize paged results + * + * @param bool $state + * + * @return $this + */ public function setUsePagedResults($state = true) { $this->usePagedResults = (bool) $state; return $this; } + /** + * Return whether this query is permitted to utilize paged results + * + * @return bool + */ public function getUsePagedResults() { return $this->usePagedResults; } /** - * Count result set, ignoring limits + * Choose an objectClass and the columns you are interested in * - * @return int + * {@inheritdoc} This creates an objectClass filter. */ - public function count() + public function from($target, array $fields = null) { - if ($this->count === null) { - $this->count = $this->connection->count($this); - } - return $this->count; + $this->filters['objectClass'] = $target; + return parent::from($target, $fields); } /** - * Count result set, ignoring limits + * Add a new filter to the query * - * @return int + * @param string $condition Column to search in + * @param mixed $value Value to look for (asterisk wildcards are allowed) + * + * @return $this */ - public function limit($count = null, $offset = null) + public function where($condition, $value = null) { - if (! preg_match('~^\d+~', $count . $offset)) { - throw new Exception( - 'Got invalid limit: %s, %s', - $count, - $offset - ); + // TODO: Adjust this once support for Icinga\Data\Filter is available + if ($condition instanceof Expression) { + $this->filters[] = $condition; + } else { + $this->filters[$condition] = $value; } - $this->limit_count = (int) $count; - $this->limit_offset = (int) $offset; + return $this; } - /** - * Whether a limit has been set - * - * @return boolean - */ - public function hasLimit() + public function getFilter() { - return $this->limit_count > 0; + throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); } - /** - * Whether an offset (limit) has been set - * - * @return boolean - */ - public function hasOffset() + public function applyFilter(Filter $filter) { - return $this->limit_offset > 0; + throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); } - /** - * Retrieve result limit - * - * @return int - */ - public function getLimit() + public function addFilter(Filter $filter) { - return $this->limit_count; + throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); } - /** - * Retrieve result offset - * - * @return int - */ - public function getOffset() + public function setFilter(Filter $filter) { - return $this->limit_offset; + throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); } /** * Fetch result as tree * - * @return Node + * @return Root + * + * @todo This is untested waste, not being used anywhere and ignores the query's order and base dn. + * Evaluate whether it's reasonable to properly implement and test it. */ public function fetchTree() { @@ -179,157 +179,32 @@ class Query } /** - * Fetch result as an array of objects + * Fetch the distinguished name of the first result * - * @return array + * @return string|false The distinguished name or false in case it's not possible to fetch a result + * + * @throws Exception In case the query returns multiple results + * (i.e. it's not possible to fetch a unique DN) */ - public function fetchAll() + public function fetchDn() { - return $this->connection->fetchAll($this); + return $this->ds->fetchDn($this); } /** - * Fetch first result row + * Return the LDAP filter to be applied on this query * - * @return object + * @return string + * + * @throws Exception In case the objectClass filter does not exist */ - public function fetchRow() + protected function renderFilter() { - return $this->connection->fetchRow($this); - } - - /** - * Fetch first column value from first result row - * - * @return mixed - */ - public function fetchOne() - { - return $this->connection->fetchOne($this); - } - - /** - * Fetch a key/value list, first column is key, second is value - * - * @return array - */ - public function fetchPairs() - { - // STILL TODO!! - return $this->connection->fetchPairs($this); - } - - /** - * Where to select (which fields) from - * - * This creates an objectClass filter - * - * @return Query - */ - public function from($objectClass, $fields = array()) - { - $this->filters['objectClass'] = $objectClass; - $this->fields = $fields; - return $this; - } - - /** - * Add a new filter to the query - * - * @param string Column to search in - * @param string Filter text (asterisks are allowed) - * @return Query - */ - public function where($key, $val) - { - $this->filters[$key] = $val; - return $this; - } - - /** - * Sort by given column - * - * TODO: Sort direction is not implemented yet - * - * @param string Order column - * @param string Order direction - * @return Query - */ - public function order($column, $direction = 'ASC') - { - $this->sort_columns[] = array($column, $direction); - return $this; - } - - /** - * Retrieve a list of the desired fields - * - * @return array - */ - public function listFields() - { - return $this->fields; - } - - /** - * Retrieve a list containing current sort columns - * - * @return array - */ - public function getSortColumns() - { - return $this->sort_columns; - } - - /** - * Return a pagination adapter for the current query - * - * @return \Zend_Paginator - */ - public function paginate($limit = null, $page = null) - { - if ($page === null || $limit === null) { - $request = \Zend_Controller_Front::getInstance()->getRequest(); - if ($page === null) { - $page = $request->getParam('page', 0); - } - if ($limit === null) { - $limit = $request->getParam('limit', 20); - } - } - $paginator = new \Zend_Paginator( - // TODO: Adapter doesn't fit yet: - new \Icinga\Web\Paginator\Adapter\QueryAdapter($this) - ); - $paginator->setItemCountPerPage($limit); - $paginator->setCurrentPageNumber($page); - return $paginator; - } - - /** - * Add a filter expression to this query - * - * @param Expression $expression - * - * @return Query - */ - public function addFilter(Expression $expression) - { - $this->filters[] = $expression; - return $this; - } - - /** - * Returns the LDAP filter that will be applied - * - * @string - */ - public function create() - { - $parts = array(); - if (! isset($this->filters['objectClass']) || $this->filters['objectClass'] === null) { + if (! isset($this->filters['objectClass'])) { throw new Exception('Object class is mandatory'); } + + $parts = array(); foreach ($this->filters as $key => $value) { if ($value instanceof Expression) { $parts[] = (string) $value; @@ -341,6 +216,7 @@ class Query ); } } + if (count($parts) > 1) { return '(&(' . implode(')(', $parts) . '))'; } else { @@ -348,17 +224,13 @@ class Query } } + /** + * Return the LDAP filter to be applied on this query + * + * @return string + */ public function __toString() { - return $this->create(); - } - - /** - * Descructor - */ - public function __destruct() - { - // To be on the safe side: - unset($this->connection); + return $this->renderFilter(); } } diff --git a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php index 589dc8f36..44e47df78 100644 --- a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php +++ b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php @@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase return $select; } - public function testLimit() - { - $select = $this->prepareSelect(); - $this->assertEquals(10, $select->getLimit()); - $this->assertEquals(4, $select->getOffset()); - } - - public function testHasLimit() - { - $select = $this->emptySelect(); - $this->assertFalse($select->hasLimit()); - $select = $this->prepareSelect(); - $this->assertTrue($select->hasLimit()); - } - - public function testHasOffset() - { - $select = $this->emptySelect(); - $this->assertFalse($select->hasOffset()); - $select = $this->prepareSelect(); - $this->assertTrue($select->hasOffset()); - } - - public function testGetLimit() - { - $select = $this->prepareSelect(); - $this->assertEquals(10, $select->getLimit()); - } - - public function testGetOffset() - { - $select = $this->prepareSelect(); - $this->assertEquals(10, $select->getLimit()); - } - public function testFetchTree() { $this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP'); } - public function testFrom() - { - return $this->testListFields(); - } - public function testWhere() { $this->markTestIncomplete('testWhere is not implemented yet'); @@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase public function testOrder() { - $select = $this->emptySelect()->order('bla'); - // tested by testGetSortColumns + $this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete'); } - public function testListFields() - { - $select = $this->prepareSelect(); - $this->assertEquals( - array('testIntColumn', 'testStringColumn'), - $select->listFields() - ); - } - - public function testGetSortColumns() - { - $select = $this->prepareSelect(); - $cols = $select->getSortColumns(); - $this->assertEquals('testIntColumn', $cols[0][0]); - } - - public function testCreateQuery() + public function testRenderFilter() { $select = $this->prepareSelect(); $res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))'; - $this->assertEquals($res, $select->create()); + $this->assertEquals($res, (string) $select); } } From 664017573f9aba9914e963140e617658f585d514 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:32:03 +0200 Subject: [PATCH 025/239] Ldap\Connection: Add query alias support refs #8826 --- library/Icinga/Protocol/Ldap/Connection.php | 91 +++++++++++++-------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 6193d630f..0472d5079 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -315,7 +315,7 @@ class Connection implements Selectable $count += 1; if ($offset === 0 || $offset < $count) { $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( - ldap_get_attributes($this->ds, $entry) + ldap_get_attributes($this->ds, $entry), array_flip($fields) ); } } while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); @@ -412,7 +412,7 @@ class Connection implements Selectable $count += 1; if ($offset === 0 || $offset < $count) { $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( - ldap_get_attributes($this->ds, $entry) + ldap_get_attributes($this->ds, $entry), array_flip($fields) ); } } while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); @@ -447,20 +447,48 @@ class Connection implements Selectable return $entries; // TODO(7693): Sort entries post-processed } - protected function cleanupAttributes($attrs) + protected function cleanupAttributes($attributes, array $requestedFields) { - $clean = (object) array(); - for ($i = 0; $i < $attrs['count']; $i++) { - $attr_name = $attrs[$i]; - if ($attrs[$attr_name]['count'] === 1) { - $clean->$attr_name = $attrs[$attr_name][0]; + // In case the result contains attributes with a differing case than the requested fields, it is + // necessary to create another array to map attributes case insensitively to their requested counterparts. + // This does also apply the virtual alias handling. (Since an LDAP server does not handle such) + $loweredFieldMap = array(); + foreach ($requestedFields as $name => $alias) { + $loweredFieldMap[strtolower($name)] = is_string($alias) ? $alias : $name; + } + + $cleanedAttributes = array(); + for ($i = 0; $i < $attributes['count']; $i++) { + $attribute_name = $attributes[$i]; + if ($attributes[$attribute_name]['count'] === 1) { + $attribute_value = $attributes[$attribute_name][0]; } else { - for ($j = 0; $j < $attrs[$attr_name]['count']; $j++) { - $clean->{$attr_name}[] = $attrs[$attr_name][$j]; + $attribute_value = array(); + for ($j = 0; $j < $attributes[$attribute_name]['count']; $j++) { + $attribute_value[] = $attributes[$attribute_name][$j]; } } + + $requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)]) + ? $loweredFieldMap[strtolower($attribute_name)] + : $attribute_name; + $cleanedAttributes[$requestedAttributeName] = $attribute_value; } - return $clean; + + // The result may not contain all requested fields, so populate the cleaned + // result with the missing fields and their value being set to null + foreach ($requestedFields as $name => $alias) { + if (! is_string($alias)) { + $alias = $name; + } + + if (! array_key_exists($alias, $cleanedAttributes)) { + $cleanedAttributes[$alias] = null; + Logger::debug('LDAP query result does not provide the requested field "%s"', $name); + } + } + + return (object) $cleanedAttributes; } public function testCredentials($username, $password) @@ -608,30 +636,22 @@ class Connection implements Selectable */ protected function discoverCapabilities($ds) { - $query = $this->select()->from( - '*', - array( - 'defaultNamingContext', - 'namingContexts', - 'vendorName', - 'vendorVersion', - 'supportedSaslMechanisms', - 'dnsHostName', - 'schemaNamingContext', - 'supportedLDAPVersion', // => array(3, 2) - 'supportedCapabilities', - 'supportedControl', - 'supportedExtension', - '+' - ) - ); - $result = @ldap_read( - $ds, - '', - $query->create(), - $query->listFields() + $fields = array( + 'defaultNamingContext', + 'namingContexts', + 'vendorName', + 'vendorVersion', + 'supportedSaslMechanisms', + 'dnsHostName', + 'schemaNamingContext', + 'supportedLDAPVersion', // => array(3, 2) + 'supportedCapabilities', + 'supportedControl', + 'supportedExtension', + '+' ); + $result = @ldap_read($ds, '', (string) $this->select()->from('*', $fields), $fields); if (! $result) { throw new LdapException( 'Capability query failed (%s:%d): %s. Check if hostname and port of the' @@ -641,6 +661,7 @@ class Connection implements Selectable ldap_error($ds) ); } + $entry = ldap_first_entry($ds, $result); if ($entry === false) { throw new LdapException( @@ -651,9 +672,7 @@ class Connection implements Selectable ); } - $ldapAttributes = ldap_get_attributes($ds, $entry); - $result = $this->cleanupAttributes($ldapAttributes); - return new Capability($result); + return new Capability($this->cleanupAttributes(ldap_get_attributes($ds, $entry), array_flip($fields))); } /** From 6612e4c1ae79c2bc3a5cd08e1594fc152107618e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:34:39 +0200 Subject: [PATCH 026/239] SimpleQuery: Make compare() alias aware refs #8826 refs #7693 --- library/Icinga/Data/SimpleQuery.php | 64 +++++++++++++++++++---------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index a069e03cd..1507ba589 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -43,6 +43,15 @@ class SimpleQuery implements QueryInterface */ protected $columns = array(); + /** + * The columns and their aliases flipped in order to handle aliased sort columns + * + * Supposed to be used and populated by $this->compare *only*. + * + * @var array + */ + protected $flippedColumns; + /** * The columns you're using to sort the query result * @@ -219,32 +228,42 @@ class SimpleQuery implements QueryInterface return $this; } - public function compare($a, $b, $col_num = 0) + /** + * Compare $a with $b based on this query's sort rules and column aliases + * + * @param object $a + * @param object $b + * @param int $orderIndex + * + * @return int + */ + public function compare($a, $b, $orderIndex = 0) { - // Last column to sort reached, rows are considered being equal - if (! array_key_exists($col_num, $this->order)) { - return 0; - } - $col = $this->order[$col_num][0]; - $dir = $this->order[$col_num][1]; -// TODO: throw Exception if column is missing - //$res = strnatcmp(strtolower($a->$col), strtolower($b->$col)); - $res = @strcmp(strtolower($a->$col), strtolower($b->$col)); - if ($res === 0) { -// return $this->compare($a, $b, $col_num++); - - if (array_key_exists(++$col_num, $this->order)) { - return $this->compare($a, $b, $col_num); - } else { - return 0; - } - + if (! array_key_exists($orderIndex, $this->order)) { + return 0; // Last column to sort reached, rows are considered being equal } - if ($dir === self::SORT_ASC) { - return $res; + if ($this->flippedColumns === null) { + $this->flippedColumns = array_flip($this->columns); + } + + $column = $this->order[$orderIndex][0]; + if (array_key_exists($column, $this->flippedColumns)) { + $column = $this->flippedColumns[$column]; + } + + // TODO: throw Exception if column is missing + //$res = strnatcmp(strtolower($a->$column), strtolower($b->$column)); + $result = @strcmp(strtolower($a->$column), strtolower($b->$column)); + if ($result === 0) { + return $this->compare($a, $b, $orderIndex); + } + + $direction = $this->order[$orderIndex][1]; + if ($direction === self::SORT_ASC) { + return $result; } else { - return $res * -1; + return $result * -1; } } @@ -426,6 +445,7 @@ class SimpleQuery implements QueryInterface public function columns(array $columns) { $this->columns = $columns; + $this->flippedColumns = null; // Reset, due to updated columns return $this; } From 7b7a7c9299eca3f6615158b6a5f73e125ba562a8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:36:38 +0200 Subject: [PATCH 027/239] Ldap\Connection: Add proper order support Will now utilize SimpleQuery::compare() to provide support for multiple order columns. refs #8826 refs #7693 --- library/Icinga/Protocol/Ldap/Connection.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 0472d5079..edda85dcf 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -304,10 +304,6 @@ class Connection implements Selectable return array(); } - foreach ($query->getSortColumns() as $col) { - ldap_sort($this->ds, $results, $col[0]); - } - $count = 0; $entries = array(); $entry = ldap_first_entry($this->ds, $results); @@ -320,6 +316,10 @@ class Connection implements Selectable } } while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); + if ($query->hasOrder()) { + uasort($entries, array($query, 'compare')); + } + ldap_free_result($results); return $entries; } @@ -444,7 +444,11 @@ class Connection implements Selectable ldap_control_paged_result($this->ds, 0); } - return $entries; // TODO(7693): Sort entries post-processed + if ($query->hasOrder()) { + uasort($entries, array($query, 'compare')); + } + + return $entries; } protected function cleanupAttributes($attributes, array $requestedFields) From 3b93b84ecf66acddfd70dc20807e5641aa47b6f3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:37:48 +0200 Subject: [PATCH 028/239] Introduce class Icinga\Repository\Repository refs #8826 --- library/Icinga/Repository/Repository.php | 353 +++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 library/Icinga/Repository/Repository.php diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php new file mode 100644 index 000000000..e1b39e1c8 --- /dev/null +++ b/library/Icinga/Repository/Repository.php @@ -0,0 +1,353 @@ + + *
  • Concrete implementations need to initialize Repository::$queryColumns
  • + *
  • The datasource passed to a repository must implement the Selectable interface
  • + *
  • The datasource must yield an instance of QueryInterface when its select() method is called
  • + * + */ +abstract class Repository implements Selectable +{ + /** + * The name of this repository + * + * @var string + */ + protected $name; + + /** + * The datasource being used + * + * @var Selectable + */ + protected $ds; + + /** + * The base table name this repository is responsible for + * + * This will be automatically set to the first key of $queryColumns if not explicitly set. + * + * @var mixed + */ + protected $baseTable; + + /** + * The query columns being provided + * + * This must be overwritten by concrete repository implementations, in the following format + *
    
    +     *  array(
    +     *      'baseTable' => array(
    +     *          'column1',
    +     *          'alias1' => 'column2',
    +     *          'alias2' => 'column3'
    +     *      )
    +     *  )
    +     * 
    
    +     *
    +     * @var array
    +     */
    +    protected $queryColumns;
    +
    +    /**
    +     * The columns (or aliases) which are not permitted to be queried. (by design)
    +     *
    +     * @var array   An array of strings
    +     */
    +    protected $filterColumns;
    +
    +    /**
    +     * The default sort rules to be applied on a query
    +     *
    +     * This may be overwritten by concrete repository implementations, in the following format
    +     * 
    
    +     *  array(
    +     *      'alias_or_column_name' => array(
    +     *          'order'     => 'asc'
    +     *      ),
    +     *      'alias_or_column_name' => array(
    +     *          'columns'   => array(
    +     *              'once_more_the_alias_or_column_name_as_in_the_parent_key',
    +     *              'an_additional_alias_or_column_name_with_a_specific_direction asc'
    +     *          ),
    +     *          'order'     => 'desc'
    +     *      ),
    +     *      'alias_or_column_name' => array(
    +     *          'columns'   => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column')
    +     *          // Ascendant sort by default
    +     *      )
    +     *  )
    +     * 
    
    +     * Note that it's mandatory to supply the alias name in case there is one.
    +     *
    +     * @var array
    +     */
    +    protected $sortRules;
    +
    +    /**
    +     * An array to map table names to aliases
    +     *
    +     * @var array
    +     */
    +    protected $aliasTableMap;
    +
    +    /**
    +     * A flattened array to map query columns to aliases
    +     *
    +     * @var array
    +     */
    +    protected $aliasColumnMap;
    +
    +    /**
    +     * Create a new repository object
    +     *
    +     * @param   Selectable  $ds     The datasource to use
    +     */
    +    public function __construct(Selectable $ds)
    +    {
    +        $this->ds = $ds;
    +        $this->aliasTableMap = array();
    +        $this->aliasColumnMap = array();
    +
    +        $this->init();
    +
    +        if ($this->filterColumns === null) {
    +            $this->filterColumns = $this->getFilterColumns();
    +        }
    +
    +        if ($this->sortRules === null) {
    +            $this->sortRules = $this->getSortRules();
    +        }
    +    }
    +
    +    /**
    +     * Initialize this repository
    +     *
    +     * Supposed to be overwritten by concrete repository implementations.
    +     */
    +    protected function init()
    +    {
    +
    +    }
    +
    +    /**
    +     * Set this repository's name
    +     *
    +     * @param   string  $name
    +     *
    +     * @return  $this
    +     */
    +    public function setName($name)
    +    {
    +        $this->name = $name;
    +        return $this;
    +    }
    +
    +    /**
    +     * Return this repository's name
    +     *
    +     * In case no name has been explicitly set yet, the class name is returned.
    +     *
    +     * @return  string
    +     */
    +    public function getName()
    +    {
    +        return $this->name ?: __CLASS__;
    +    }
    +
    +    /**
    +     * Return the datasource being used
    +     *
    +     * @return  Selectable
    +     */
    +    public function getDataSource()
    +    {
    +        return $this->ds;
    +    }
    +
    +    /**
    +     * Return the base table name this repository is responsible for
    +     *
    +     * @return  mixed
    +     *
    +     * @throws  ProgrammingError    In case no base table name has been set and
    +     *                               $this->queryColumns does not provide one either
    +     */
    +    public function getBaseTable()
    +    {
    +        if ($this->baseTable === null) {
    +            $queryColumns = $this->queryColumns; // Copy because of reset()
    +            reset($queryColumns);
    +            $this->baseTable = key($queryColumns);
    +            if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
    +                throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable);
    +            }
    +        }
    +
    +        return $this->baseTable;
    +    }
    +
    +    /**
    +     * Return the columns (or aliases) which are not permitted to be queried
    +     *
    +     * @return  array
    +     */
    +    public function getFilterColumns()
    +    {
    +        if ($this->filterColumns !== null) {
    +            return $this->filterColumns;
    +        }
    +
    +        return array();
    +    }
    +
    +    /**
    +     * Return the default sort rules to be applied on a query
    +     *
    +     * @return  array
    +     */
    +    public function getSortRules()
    +    {
    +        if ($this->sortRules !== null) {
    +            return $this->sortRules;
    +        }
    +
    +        return array();
    +    }
    +
    +    /**
    +     * Return a new query for the given columns
    +     *
    +     * @param   array   $columns    The desired columns, if null all columns will be queried
    +     *
    +     * @return  RepositoryQuery
    +     *
    +     * @throws  ProgrammingError    In case $this->queryColumns has not been initialized yet
    +     */
    +    public function select(array $columns = null)
    +    {
    +        if (empty($this->queryColumns)) {
    +            throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
    +        }
    +
    +        $this->initializeAliasMaps();
    +
    +        $query = new RepositoryQuery($this);
    +        $query->from($this->getBaseTable(), $columns);
    +        return $query;
    +    }
    +
    +    /**
    +     * Initialize $this->aliasTableMap and $this->aliasColumnMap
    +     */
    +    protected function initializeAliasMaps()
    +    {
    +        if (! empty($this->aliasColumnMap)) {
    +            return;
    +        }
    +
    +        foreach ($this->queryColumns as $table => $columns) {
    +            foreach ($columns as $alias => $column) {
    +                if (! is_string($alias)) {
    +                    $this->aliasTableMap[$column] = $table;
    +                    $this->aliasColumnMap[$column] = $column;
    +                } else {
    +                    $this->aliasTableMap[$alias] = $table;
    +                    $this->aliasColumnMap[$alias] = preg_replace('~\n\s*~', ' ', $column);
    +                }
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Return this repository's query columns mapped to their respective aliases
    +     *
    +     * @return  array
    +     */
    +    public function requireAllQueryColumns()
    +    {
    +        $map = array();
    +        foreach ($this->aliasColumnMap as $alias => $_) {
    +            if ($this->hasQueryColumn($alias)) {
    +                // Just in case $this->requireQueryColumn has been overwritten and there is some magic going on
    +                $map[$alias] = $this->requireQueryColumn($alias);
    +            }
    +        }
    +
    +        return $map;
    +    }
    +
    +    /**
    +     * Return whether the given column name or alias is a valid query column
    +     *
    +     * @param   string  $name   The column name or alias to check
    +     *
    +     * @return  bool
    +     */
    +    public function hasQueryColumn($name)
    +    {
    +        return array_key_exists($name, $this->aliasColumnMap) && !in_array($name, $this->filterColumns);
    +    }
    +
    +    /**
    +     * Validate that the given column is a valid query target and return it or the actual name if it's an alias
    +     *
    +     * @param   string  $name       The name or alias of the column to validate
    +     *
    +     * @return  string              The given column's name
    +     *
    +     * @throws  QueryException      In case the given column is not a valid query column
    +     */
    +    public function requireQueryColumn($name)
    +    {
    +        if (in_array($name, $this->filterColumns)) {
    +            throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
    +        }
    +        if (! array_key_exists($name, $this->aliasColumnMap)) {
    +            throw new QueryException(t('Query column "%s" not found'), $name);
    +        }
    +
    +        return $this->aliasColumnMap[$name];
    +    }
    +
    +    /**
    +     * Return whether the given column name or alias is a valid filter column
    +     *
    +     * @param   string  $name   The column name or alias to check
    +     *
    +     * @return  bool
    +     */
    +    public function hasFilterColumn($name)
    +    {
    +        return array_key_exists($name, $this->aliasColumnMap);
    +    }
    +
    +    /**
    +     * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
    +     *
    +     * @param   string  $name       The name or alias of the column to validate
    +     *
    +     * @return  string              The given column's name
    +     *
    +     * @throws  QueryException      In case the given column is not a valid filter column
    +     */
    +    public function requireFilterColumn($name)
    +    {
    +        if (! array_key_exists($name, $this->aliasColumnMap)) {
    +            throw new QueryException(t('Filter column "%s" not found'), $name);
    +        }
    +
    +        return $this->aliasColumnMap[$name];
    +    }
    +}
    
    From fa1906ee7d1366f33656017bc6894f7101b91071 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 4 May 2015 11:38:21 +0200
    Subject: [PATCH 029/239] Introduce class Icinga\Repository\RepositoryQuery
    
    refs #8826
    ---
     library/Icinga/Repository/RepositoryQuery.php | 453 ++++++++++++++++++
     1 file changed, 453 insertions(+)
     create mode 100644 library/Icinga/Repository/RepositoryQuery.php
    
    diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
    new file mode 100644
    index 000000000..df5d17cd6
    --- /dev/null
    +++ b/library/Icinga/Repository/RepositoryQuery.php
    @@ -0,0 +1,453 @@
    +repository = $repository;
    +        $this->query = $repository->getDataSource()->select();
    +    }
    +
    +    /**
    +     * Return the real query being used
    +     *
    +     * @return  QueryInterface
    +     */
    +    public function getQuery()
    +    {
    +        return $this->query;
    +    }
    +
    +    /**
    +     * Set where to fetch which columns
    +     *
    +     * This notifies the repository about each desired query column.
    +     *
    +     * @param   mixed   $target     The type and purpose of this parameter depends on this query's repository
    +     * @param   array   $columns    If null or an empty array, all columns will be fetched
    +     *
    +     * @return  $this
    +     */
    +    public function from($target, array $columns = null)
    +    {
    +        $this->query->from($target, $this->prepareQueryColumns($columns));
    +        return $this;
    +    }
    +
    +    /**
    +     * Set which columns to fetch
    +     *
    +     * This notifies the repository about each desired query column.
    +     *
    +     * @param   array   $columns    If null or an empty array, all columns will be fetched
    +     *
    +     * @return  $this
    +     */
    +    public function columns(array $columns)
    +    {
    +        $this->query->columns($this->prepareQueryColumns($columns));
    +        return $this;
    +    }
    +
    +    /**
    +     * Resolve the given columns supposed to be fetched
    +     *
    +     * This notifies the repository about each desired query column.
    +     *
    +     * @param   array   $desiredColumns     Pass null or an empty array to require all query columns
    +     *
    +     * @return  array                       The desired columns indexed by their respective alias
    +     */
    +    protected function prepareQueryColumns(array $desiredColumns = null)
    +    {
    +        if (empty($desiredColumns)) {
    +            $columns = $this->repository->requireAllQueryColumns();
    +        } else {
    +            $columns = array();
    +            foreach ($desiredColumns as $customAlias => $columnAlias) {
    +                $resolvedColumn = $this->repository->requireQueryColumn($columnAlias);
    +                if ($resolvedColumn !== $columnAlias) {
    +                    $columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
    +                } elseif (is_string($customAlias)) {
    +                    $columns[$customAlias] = $columnAlias;
    +                } else {
    +                    $columns[] = $columnAlias;
    +                }
    +            }
    +        }
    +
    +        return $columns;
    +    }
    +
    +    /**
    +     * Filter this query using the given column and value
    +     *
    +     * This notifies the repository about the required filter column.
    +     *
    +     * @param   string  $column
    +     * @param   mixed   $value
    +     *
    +     * @return  $this
    +     */
    +    public function where($column, $value = null)
    +    {
    +        $this->query->where($this->repository->requireFilterColumn($column), $value);
    +        return $this;
    +    }
    +
    +    /**
    +     * Add an additional filter expression to this query
    +     *
    +     * This notifies the repository about each required filter column.
    +     *
    +     * @param   Filter  $filter
    +     *
    +     * @return  $this
    +     */
    +    public function applyFilter(Filter $filter)
    +    {
    +        return $this->addFilter($filter);
    +    }
    +
    +    /**
    +     * Set a filter for this query
    +     *
    +     * This notifies the repository about each required filter column.
    +     *
    +     * @param   Filter  $filter
    +     *
    +     * @return  $this
    +     */
    +    public function setFilter(Filter $filter)
    +    {
    +        $this->requireFilterColumns($filter);
    +        $this->query->setFilter($filter);
    +        return $this;
    +    }
    +
    +    /**
    +     * Add an additional filter expression to this query
    +     *
    +     * This notifies the repository about each required filter column.
    +     *
    +     * @param   Filter  $filter
    +     *
    +     * @return  $this
    +     */
    +    public function addFilter(Filter $filter)
    +    {
    +        $this->requireFilterColumns($filter);
    +        $this->query->addFilter($filter);
    +        return $this;
    +    }
    +
    +    /*+
    +     * Recurse the given filter and notify the repository about each required filter column
    +     */
    +    protected function requireFilterColumns(Filter $filter)
    +    {
    +        if ($filter->isExpression()) {
    +            $filter->setColumn($this->repository->requireFilterColumn($filter->getColumn()));
    +        } elseif ($filter->isChain()) {
    +            foreach ($filter->filters() as $chainOrExpression) {
    +                $this->requireFilterColumns($chainOrExpression);
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Return the current filter
    +     *
    +     * @return  Filter
    +     */
    +    public function getFilter()
    +    {
    +        return $this->query->getFilter();
    +    }
    +
    +    /**
    +     * Add a sort rule for this query
    +     *
    +     * If called without a specific column, the repository's defaul sort rules will be applied.
    +     * This notifies the repository about each column being required as filter column.
    +     *
    +     * @param   string  $field      The name of the column by which to sort the query's result
    +     * @param   string  $direction  The direction to use when sorting (asc or desc, default is asc)
    +     *
    +     * @return  $this
    +     */
    +    public function order($field = null, $direction = null)
    +    {
    +        $sortRules = $this->repository->getSortRules();
    +        if ($field === null) {
    +            // Use first available sort rule as default
    +            if (empty($sortRules)) {
    +                // Return early in case of no sort defaults and no given $field
    +                return $this;
    +            }
    +
    +            $sortColumns = reset($sortRules);
    +            if (! array_key_exists('columns', $sortColumns)) {
    +                $sortColumns['columns'] = array(key($sortRules));
    +            }
    +            if ($direction !== null || !array_key_exists('order', $sortColumns)) {
    +                $sortColumns['order'] = $direction ?: static::SORT_ASC;
    +            }
    +        } elseif (array_key_exists($field, $sortRules)) {
    +            $sortColumns = $sortRules[$field];
    +            if (! array_key_exists('columns', $sortColumns)) {
    +                $sortColumns['columns'] = array($field);
    +            }
    +            if ($direction !== null || !array_key_exists('order', $sortColumns)) {
    +                $sortColumns['order'] = $direction ?: static::SORT_ASC;
    +            }
    +        } else {
    +            $sortColumns = array(
    +                'columns'   => array($field),
    +                'order'     => $direction
    +            );
    +        };
    +
    +        $baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;
    +
    +        foreach ($sortColumns['columns'] as $column) {
    +            list($column, $specificDirection) = $this->splitOrder($column);
    +
    +            try {
    +                $this->query->order(
    +                    $this->repository->requireFilterColumn($column),
    +                    $direction ? $baseDirection : ($specificDirection ?: $baseDirection)
    +                );
    +            } catch (QueryException $_) {
    +                Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
    +            }
    +        }
    +
    +        return $this;
    +    }
    +
    +    /**
    +     * Extract and return the name and direction of the given sort column definition
    +     *
    +     * @param   string  $field
    +     *
    +     * @return  array               An array of two items: $columnName, $direction
    +     */
    +    protected function splitOrder($field)
    +    {
    +        $columnAndDirection = explode(' ', $field, 2);
    +        if (count($columnAndDirection) === 1) {
    +            $column = $field;
    +            $direction = null;
    +        } else {
    +            $column = $columnAndDirection[0];
    +            $direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
    +                ? static::SORT_DESC
    +                : static::SORT_ASC;
    +        }
    +
    +        return array($column, $direction);
    +    }
    +
    +    /**
    +     * Return whether any sort rules were applied to this query
    +     *
    +     * @return  bool
    +     */
    +    public function hasOrder()
    +    {
    +        return $this->query->hasOrder();
    +    }
    +
    +    /**
    +     * Return the sort rules applied to this query
    +     *
    +     * @return  array
    +     */
    +    public function getOrder()
    +    {
    +        return $this->query->getOrder();
    +    }
    +
    +    /**
    +     * Limit this query's results
    +     *
    +     * @param   int     $count      When to stop returning results
    +     * @param   int     $offset     When to start returning results
    +     *
    +     * @return  $this
    +     */
    +    public function limit($count = null, $offset = null)
    +    {
    +        $this->query->limit($count, $offset);
    +        return $this;
    +    }
    +
    +    /**
    +     * Return whether this query does not return all available entries from its result
    +     *
    +     * @return  bool
    +     */
    +    public function hasLimit()
    +    {
    +        return $this->query->hasLimit();
    +    }
    +
    +    /**
    +     * Return the limit when to stop returning results
    +     *
    +     * @return  int
    +     */
    +    public function getLimit()
    +    {
    +        return $this->query->getLimit();
    +    }
    +
    +    /**
    +     * Return whether this query does not start returning results at the very first entry
    +     *
    +     * @return  bool
    +     */
    +    public function hasOffset()
    +    {
    +        return $this->query->hasOffset();
    +    }
    +
    +    /**
    +     * Return the offset when to start returning results
    +     *
    +     * @return  int
    +     */
    +    public function getOffset()
    +    {
    +        return $this->query->getOffset();
    +    }
    +
    +    /**
    +     * Return a paginator object for this query
    +     *
    +     * If not given, $itemsPerPage and $pageNumber will be set to their URL parameter counterparts.
    +     *
    +     * @param   int     $itemsPerPage   Number of items per page
    +     * @param   int     $pageNumber     Current page number
    +     *
    +     * @return  Zend_Paginator
    +     */
    +    public function paginate($itemsPerPage = null, $pageNumber = null)
    +    {
    +        return $this->query->paginate($itemsPerPage, $pageNumber);
    +    }
    +
    +    /**
    +     * Fetch and return the first column of this query's first row
    +     *
    +     * @return  mixed
    +     */
    +    public function fetchOne()
    +    {
    +        if (! $this->hasOrder()) {
    +            $this->order();
    +        }
    +
    +        return $this->query->fetchOne();
    +    }
    +
    +    /**
    +     * Fetch and return the first row of this query's result
    +     *
    +     * @return  object
    +     */
    +    public function fetchRow()
    +    {
    +        if (! $this->hasOrder()) {
    +            $this->order();
    +        }
    +
    +        return $this->query->fetchRow();
    +    }
    +
    +    /**
    +     * Fetch and return a column of all rows of the result set as an array
    +     *
    +     * @param   int     $columnIndex    Index of the column to fetch
    +     *
    +     * @return  array
    +     */
    +    public function fetchColumn($columnIndex = 0)
    +    {
    +        if (! $this->hasOrder()) {
    +            $this->order();
    +        }
    +
    +        return $this->query->fetchColumn($columnIndex);
    +    }
    +
    +    /**
    +     * Fetch and return all rows of this query's result as a flattened key/value based array
    +     *
    +     * @return  array
    +     */
    +    public function fetchPairs()
    +    {
    +        if (! $this->hasOrder()) {
    +            $this->order();
    +        }
    +
    +        return $this->query->fetchPairs();
    +    }
    +
    +    /**
    +     * Fetch and return all results of this query
    +     *
    +     * @return  array
    +     */
    +    public function fetchAll()
    +    {
    +        if (! $this->hasOrder()) {
    +            $this->order();
    +        }
    +
    +        return $this->query->fetchAll();
    +    }
    +
    +    /**
    +     * Count all results of this query
    +     *
    +     * @return  int
    +     */
    +    public function count()
    +    {
    +        return $this->query->count();
    +    }
    +}
    
    From 870e75c99c811da73791582e3ab7129ee4b4ad20 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 4 May 2015 11:39:12 +0200
    Subject: [PATCH 030/239] Introduce class Icinga\Repository\DbRepository
    
    refs #8826
    ---
     library/Icinga/Repository/DbRepository.php | 64 ++++++++++++++++++++++
     1 file changed, 64 insertions(+)
     create mode 100644 library/Icinga/Repository/DbRepository.php
    
    diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
    new file mode 100644
    index 000000000..8ba43efaf
    --- /dev/null
    +++ b/library/Icinga/Repository/DbRepository.php
    @@ -0,0 +1,64 @@
    +
    + *  
  • Automatic table prefix handling
  • + * + */ +abstract class DbRepository extends Repository +{ + /** + * Return the base table name this repository is responsible for + * + * This prepends the datasource's table prefix, if available and required. + * + * @return mixed + * + * @throws ProgrammingError In case no base table name has been set and + * $this->queryColumns does not provide one either + */ + public function getBaseTable() + { + return $this->prependTablePrefix(parent::getBaseTable()); + } + + /** + * Return the given table with the datasource's prefix being prepended + * + * @param array|string $table + * + * @return array|string + * + * @throws IcingaException In case $table is not of a supported type + */ + protected function prependTablePrefix($table) + { + $prefix = $this->ds->getTablePrefix(); + if (! $prefix) { + return $table; + } + + if (is_array($table)) { + foreach ($table as & $tableName) { + if (strpos($tableName, $prefix) === false) { + $tableName = $prefix . $tableName; + } + } + } elseif (is_string($table)) { + $table = (strpos($table, $prefix) === false ? $prefix : '') . $table; + } else { + throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table)); + } + + return $table; + } +} From 68657c02ee1db0af30b4915c1911de249d4430ab Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:40:17 +0200 Subject: [PATCH 031/239] Introduce interface Icinga\Authentication\User\UserBackendInterface refs #8826 --- .../User/UserBackendInterface.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 library/Icinga/Authentication/User/UserBackendInterface.php diff --git a/library/Icinga/Authentication/User/UserBackendInterface.php b/library/Icinga/Authentication/User/UserBackendInterface.php new file mode 100644 index 000000000..cfb2e3753 --- /dev/null +++ b/library/Icinga/Authentication/User/UserBackendInterface.php @@ -0,0 +1,41 @@ + Date: Mon, 4 May 2015 11:43:53 +0200 Subject: [PATCH 032/239] Make class UserBackend being just a factory for user backends refs #8826 --- .../Authentication/User/UserBackend.php | 88 +++++-------------- 1 file changed, 24 insertions(+), 64 deletions(-) diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 89bd1f56e..8bda07323 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -3,25 +3,23 @@ namespace Icinga\Authentication\User; -use Countable; use Icinga\Application\Logger; use Icinga\Application\Icinga; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; -use Icinga\User; /** - * Base class for concrete user backends + * Factory for user backends */ -abstract class UserBackend implements Countable +class UserBackend { /** * The default user backend types provided by Icinga Web 2 * * @var array */ - private static $defaultBackends = array( // I would have liked it if I were able to declare this as constant :'( + protected static $defaultBackends = array( 'external', 'db', 'ldap', @@ -36,39 +34,9 @@ abstract class UserBackend implements Countable protected static $customBackends; /** - * The name of this backend - * - * @var string + * Register all custom user backends from all loaded modules */ - protected $name; - - /** - * Set this backend's name - * - * @param string $name - * - * @return $this - */ - public function setName($name) - { - $this->name = $name; - return $this; - } - - /** - * Return this backend's name - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Fetch all custom user backends from all loaded modules - */ - public static function loadCustomUserBackends() + protected static function registerCustomUserBackends() { if (static::$customBackends !== null) { return; @@ -80,7 +48,7 @@ abstract class UserBackend implements Countable foreach ($module->getUserBackends() as $identifier => $className) { if (array_key_exists($identifier, $providedBy)) { Logger::warning( - 'Cannot register UserBackend of type "%s" provided by module "%s".' + 'Cannot register user backend of type "%s" provided by module "%s".' . ' The type is already provided by module "%s"', $identifier, $module->getName(), @@ -88,7 +56,7 @@ abstract class UserBackend implements Countable ); } elseif (in_array($identifier, static::$defaultBackends)) { Logger::warning( - 'Cannot register UserBackend of type "%s" provided by module "%s".' + 'Cannot register user backend of type "%s" provided by module "%s".' . ' The type is a default type provided by Icinga Web 2', $identifier, $module->getName() @@ -102,29 +70,23 @@ abstract class UserBackend implements Countable } /** - * Validate and return the class for the given custom user backend + * Return the class for the given custom user backend * * @param string $identifier The identifier of the custom user backend * * @return string|null The name of the class or null in case there was no * backend found with the given identifier * - * @throws ConfigurationError In case the class could not be successfully validated + * @throws ConfigurationError In case the class associated to the given identifier does not exist */ protected static function getCustomUserBackend($identifier) { - static::loadCustomUserBackends(); + static::registerCustomUserBackends(); if (array_key_exists($identifier, static::$customBackends)) { $className = static::$customBackends[$identifier]; if (! class_exists($className)) { throw new ConfigurationError( - 'Cannot utilize UserBackend of type "%s". Class "%s" does not exist', - $identifier, - $className - ); - } elseif (! is_subclass_of($className, __CLASS__)) { - throw new ConfigurationError( - 'Cannot utilize UserBackend of type "%s". Class "%s" is not a sub-type of UserBackend', + 'Cannot utilize user backend of type "%s". Class "%s" does not exist', $identifier, $className ); @@ -135,12 +97,12 @@ abstract class UserBackend implements Countable } /** - * Create and return a UserBackend with the given name and given configuration applied to it + * Create and return a user backend with the given name and given configuration applied to it * * @param string $name * @param ConfigObject $backendConfig * - * @return UserBackend + * @return UserBackendInterface * * @throws ConfigurationError */ @@ -152,7 +114,7 @@ abstract class UserBackend implements Countable if (! ($backendType = strtolower($backendConfig->backend))) { throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the \'backend\' directive', + 'Authentication configuration for user backend "%s" is missing the \'backend\' directive', $name ); } @@ -166,11 +128,19 @@ abstract class UserBackend implements Countable // Do not attempt to load a custom user backend unless it's actually required } elseif (($customClass = static::getCustomUserBackend($backendType)) !== null) { $backend = new $customClass($backendConfig); + if (! is_a($backend, 'Icinga\Authentication\User\UserBackendInterface')) { + throw new ConfigurationError( + 'Cannot utilize user backend of type "%s". Class "%s" does not implement UserBackendInterface', + $backendType, + $customClass + ); + } + $backend->setName($name); return $backend; } else { throw new ConfigurationError( - 'Authentication configuration for backend "%s" defines an invalid backend type.' + 'Authentication configuration for user backend "%s" defines an invalid backend type.' . ' Backend type "%s" is not supported', $name, $backendType @@ -179,7 +149,7 @@ abstract class UserBackend implements Countable if ($backendConfig->resource === null) { throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the \'resource\' directive', + 'Authentication configuration for user backend "%s" is missing the \'resource\' directive', $name ); } @@ -239,14 +209,4 @@ abstract class UserBackend implements Countable $backend->setName($name); return $backend; } - - /** - * Authenticate the given user - * - * @param User $user - * @param string $password - * - * @return bool - */ - abstract public function authenticate(User $user, $password); } From 7b41fc020a1662973b0dc1b7f72576747eef3ab0 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 11:44:41 +0200 Subject: [PATCH 033/239] AuthChain: Yield UserBackendInterface instead of UserBackend refs #8826 --- library/Icinga/Authentication/AuthChain.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php index 167661f39..7354825a2 100644 --- a/library/Icinga/Authentication/AuthChain.php +++ b/library/Icinga/Authentication/AuthChain.php @@ -6,6 +6,7 @@ namespace Icinga\Authentication; use Iterator; use Icinga\Data\ConfigObject; use Icinga\Authentication\User\UserBackend; +use Icinga\Authentication\User\UserBackendInterface; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; @@ -25,7 +26,7 @@ class AuthChain implements Iterator /** * The consecutive user backend while looping * - * @var UserBackend + * @var UserBackendInterface */ private $currentBackend; @@ -53,7 +54,7 @@ class AuthChain implements Iterator /** * Return the current user backend * - * @return UserBackend + * @return UserBackendInterface */ public function current() { From 99ac0b78ea51554b6feb20c4d63a59efe18cfd2a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 12:15:05 +0200 Subject: [PATCH 034/239] DbUserBackend: Extend DbRepository and implement UserBackendInterface refs #8826 --- .../Config/Authentication/DbBackendForm.php | 4 +- .../Authentication/User/DbUserBackend.php | 94 ++++++++----------- .../Authentication/DbBackendFormTest.php | 2 +- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/application/forms/Config/Authentication/DbBackendForm.php b/application/forms/Config/Authentication/DbBackendForm.php index 98d955f4a..babd4383b 100644 --- a/application/forms/Config/Authentication/DbBackendForm.php +++ b/application/forms/Config/Authentication/DbBackendForm.php @@ -107,8 +107,8 @@ class DbBackendForm extends Form { try { $dbUserBackend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig())); - if ($dbUserBackend->count() < 1) { - $form->addError($form->translate('No users found under the specified database backend')); + if ($dbUserBackend->select()->where('is_active', true)->count() < 1) { + $form->addError($form->translate('No active users found under the specified database backend')); return false; } } catch (Exception $e) { diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 2286f4e93..08c2006d6 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -3,15 +3,13 @@ namespace Icinga\Authentication\User; -use PDO; -use Icinga\Data\Db\DbConnection; -use Icinga\User; -use Icinga\Exception\AuthenticationException; use Exception; -use Zend_Db_Expr; -use Zend_Db_Select; +use PDO; +use Icinga\Exception\AuthenticationException; +use Icinga\Repository\DbRepository; +use Icinga\User; -class DbUserBackend extends UserBackend +class DbUserBackend extends DbRepository implements UserBackendInterface { /** * The algorithm to use when hashing passwords @@ -28,15 +26,38 @@ class DbUserBackend extends UserBackend const SALT_LENGTH = 12; // 12 is required by MD5 /** - * Connection to the database + * The query columns being provided * - * @var DbConnection + * @var array */ - protected $conn; + protected $queryColumns = array( + 'user' => array( + 'user_name' => 'name', + 'is_active' => 'active', + 'created_at' => 'UNIX_TIMESTAMP(ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(mtime)' + ) + ); - public function __construct(DbConnection $conn) + /** + * The default sort rules to be applied on a query + * + * @var array + */ + protected $sortRules = array( + 'user_name' => array( + 'order' => 'asc' + ) + ); + + /** + * Initialize this database user backend + */ + protected function init() { - $this->conn = $conn; + if (! $this->ds->getTablePrefix()) { + $this->ds->setTablePrefix('icingaweb_'); + } } /** @@ -50,7 +71,7 @@ class DbUserBackend extends UserBackend { $passwordHash = $this->hashPassword($password); - $stmt = $this->conn->getDbAdapter()->prepare( + $stmt = $this->ds->getDbAdapter()->prepare( 'INSERT INTO icingaweb_user VALUES (:name, :active, :password_hash, now(), DEFAULT);' ); $stmt->bindParam(':name', $username, PDO::PARAM_STR); @@ -68,13 +89,13 @@ class DbUserBackend extends UserBackend */ protected function getPasswordHash($username) { - if ($this->conn->getDbType() === 'pgsql') { + if ($this->ds->getDbType() === 'pgsql') { // Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape' - $stmt = $this->conn->getDbAdapter()->prepare( + $stmt = $this->ds->getDbAdapter()->prepare( 'SELECT ENCODE(password_hash, \'escape\') FROM icingaweb_user WHERE name = :name AND active = 1' ); } else { - $stmt = $this->conn->getDbAdapter()->prepare( + $stmt = $this->ds->getDbAdapter()->prepare( 'SELECT password_hash FROM icingaweb_user WHERE name = :name AND active = 1' ); } @@ -86,18 +107,18 @@ class DbUserBackend extends UserBackend $lob = stream_get_contents($lob); } - return $this->conn->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob; + return $this->ds->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob; } /** - * Authenticate the given user and return true on success, false on failure and throw an exception on error + * Authenticate the given user * * @param User $user * @param string $password * - * @return bool + * @return bool True on success, false on failure * - * @throws AuthenticationException + * @throws AuthenticationException In case authentication is not possible due to an error */ public function authenticate(User $user, $password) { @@ -152,37 +173,4 @@ class DbUserBackend extends UserBackend { return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt())); } - - /** - * Get the number of users available - * - * @return int - */ - public function count() - { - $select = new Zend_Db_Select($this->conn->getDbAdapter()); - $row = $select->from( - 'icingaweb_user', - array('count' => 'COUNT(*)') - )->query()->fetchObject(); - - return ($row !== false) ? $row->count : 0; - } - - /** - * Return the names of all available users - * - * @return array - */ - public function listUsers() - { - $query = $this->conn->select()->from('icingaweb_user', array('name')); - - $users = array(); - foreach ($query->fetchAll() as $row) { - $users[] = $row->name; - } - - return $users; - } } diff --git a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php b/test/php/application/forms/Config/Authentication/DbBackendFormTest.php index f574a8292..695ef4885 100644 --- a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php +++ b/test/php/application/forms/Config/Authentication/DbBackendFormTest.php @@ -28,7 +28,7 @@ class DbBackendFormTest extends BaseTestCase { $this->setUpResourceFactoryMock(); Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend') - ->shouldReceive('count') + ->shouldReceive('select->where->count') ->andReturn(2); // Passing array(null) is required to make Mockery call the constructor... From e74194c18ebbd9cfa9337f077ed77f7688a72601 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 12:15:50 +0200 Subject: [PATCH 035/239] ExternalBackend: Implement UserBackendInterface refs #8826 --- .../Authentication/User/ExternalBackend.php | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/library/Icinga/Authentication/User/ExternalBackend.php b/library/Icinga/Authentication/User/ExternalBackend.php index 4f9dd47b3..413c0553b 100644 --- a/library/Icinga/Authentication/User/ExternalBackend.php +++ b/library/Icinga/Authentication/User/ExternalBackend.php @@ -9,14 +9,21 @@ use Icinga\User; /** * Test login with external authentication mechanism, e.g. Apache */ -class ExternalBackend extends UserBackend +class ExternalBackend implements UserBackendInterface { + /** + * The name of this backend + * + * @var string + */ + protected $name; + /** * Regexp expression to strip values from a username * * @var string */ - private $stripUsernameRegexp; + protected $stripUsernameRegexp; /** * Create new authentication backend of type "external" @@ -29,24 +36,37 @@ class ExternalBackend extends UserBackend } /** - * Count the available users + * Set this backend's name * - * Authenticaton backends of type "external" will always return 1 + * @param string $name * - * @return int + * @return $this */ - public function count() + public function setName($name) { - return 1; + $this->name = $name; + return $this; } /** - * Authenticate + * Return this backend's name * - * @param User $user - * @param string $password + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Authenticate the given user * - * @return bool + * @param User $user + * @param string $password + * + * @return bool True on success, false on failure + * + * @throws AuthenticationException In case authentication is not possible due to an error */ public function authenticate(User $user, $password = null) { From c44111732445b6764900c96d26ab898d829f8c31 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 12:18:25 +0200 Subject: [PATCH 036/239] LdapUserBackend: Extend Repository and implement UserBackendInterface refs #8826 --- .../Config/Authentication/LdapBackendForm.php | 9 +- .../Authentication/User/LdapUserBackend.php | 320 ++++++++++++------ .../Authentication/User/UserBackend.php | 47 +-- .../application/forms/AdminAccountPage.php | 9 +- .../Authentication/LdapBackendFormTest.php | 3 +- 5 files changed, 231 insertions(+), 157 deletions(-) diff --git a/application/forms/Config/Authentication/LdapBackendForm.php b/application/forms/Config/Authentication/LdapBackendForm.php index 867f5658c..0ec2c3e38 100644 --- a/application/forms/Config/Authentication/LdapBackendForm.php +++ b/application/forms/Config/Authentication/LdapBackendForm.php @@ -170,13 +170,8 @@ class LdapBackendForm extends Form public static function isValidAuthenticationBackend(Form $form) { try { - $ldapUserBackend = new LdapUserBackend( - ResourceFactory::createResource($form->getResourceConfig()), - $form->getElement('user_class')->getValue(), - $form->getElement('user_name_attribute')->getValue(), - $form->getElement('base_dn')->getValue(), - $form->getElement('filter')->getValue() - ); + $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig())); + $ldapUserBackend->setConfig(new ConfigObject($form->getValues())); $ldapUserBackend->assertAuthenticationPossible(); } catch (AuthenticationException $e) { if (($previous = $e->getPrevious()) !== null) { diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 8bc8f0222..9b2012b46 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -3,29 +3,55 @@ namespace Icinga\Authentication\User; -use Icinga\User; -use Icinga\Protocol\Ldap\Query; -use Icinga\Protocol\Ldap\Connection; +use Icinga\Data\ConfigObject; use Icinga\Exception\AuthenticationException; +use Icinga\Exception\ProgrammingError; +use Icinga\Repository\Repository; +use Icinga\Repository\RepositoryQuery; use Icinga\Protocol\Ldap\Exception as LdapException; use Icinga\Protocol\Ldap\Expression; +use Icinga\User; -class LdapUserBackend extends UserBackend +class LdapUserBackend extends Repository implements UserBackendInterface { /** - * Connection to the LDAP server + * The base DN to use for a query * - * @var Connection + * @var string */ - protected $conn; - protected $baseDn; + /** + * The objectClass where look for users + * + * @var string + */ protected $userClass; + /** + * The attribute name where to find a user's name + * + * @var string + */ protected $userNameAttribute; - protected $customFilter; + /** + * The custom LDAP filter to apply on search queries + * + * @var string + */ + protected $filter; + + /** + * The default sort rules to be applied on a query + * + * @var array + */ + protected $sortRules = array( + 'user_name' => array( + 'order' => 'asc' + ) + ); protected $groupOptions; @@ -41,20 +67,115 @@ class LdapUserBackend extends UserBackend 'samaccountname' => 'sAMAccountName' ); - public function __construct( - Connection $conn, - $userClass, - $userNameAttribute, - $baseDn, - $cutomFilter, - $groupOptions = null - ) { - $this->conn = $conn; - $this->baseDn = trim($baseDn) ?: $conn->getDN(); - $this->userClass = $this->getNormedAttribute($userClass); + /** + * Set the base DN to use for a query + * + * @param string $baseDn + * + * @return $this + */ + public function setBaseDn($baseDn) + { + if (($baseDn = trim($baseDn))) { + $this->baseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a query + * + * @return string + */ + public function getBaseDn() + { + return $this->baseDn; + } + + /** + * Set the objectClass where to look for users + * + * Sets also the base table name for the underlying repository. + * + * @param string $userClass + * + * @return $this + */ + public function setUserClass($userClass) + { + $this->baseTable = $this->userClass = $this->getNormedAttribute($userClass); + return $this; + } + + /** + * Return the objectClass where to look for users + * + * @return string + */ + public function getUserClass() + { + return $this->userClass; + } + + /** + * Set the attribute name where to find a user's name + * + * @param string $userNameAttribute + * + * @return $this + */ + public function setUserNameAttribute($userNameAttribute) + { $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute); - $this->customFilter = trim($cutomFilter); - $this->groupOptions = $groupOptions; + return $this; + } + + /** + * Return the attribute name where to find a user's name + * + * @return string + */ + public function getUserNameAttribute() + { + return $this->userNameAttribute; + } + + /** + * Set the custom LDAP filter to apply on search queries + * + * @param string $filter + * + * @return $this + */ + public function setFilter($filter) + { + if (($filter = trim($filter))) { + $this->filter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on search queries + * + * @return string + */ + public function getFilter() + { + return $this->filter; + } + + public function setGroupOptions(array $options) + { + $this->groupOptions = $options; + return $this; + } + + public function getGroupOptions() + { + return $this->groupOptions; } /** @@ -75,45 +196,69 @@ class LdapUserBackend extends UserBackend } /** - * Create a query to select all usernames + * Apply the given configuration to this backend * - * @return Query + * @param ConfigObject $config + * + * @return $this */ - protected function selectUsers() + public function setConfig(ConfigObject $config) { - $query = $this->conn->select()->setBase($this->baseDn)->from( - $this->userClass, - array( - $this->userNameAttribute - ) - ); + return $this + ->setBaseDn($config->base_dn) + ->setUserClass($config->user_class) + ->setUserNameAttribute($config->user_name_attribute) + ->setFilter($config->filter); + } - if ($this->customFilter) { - $query->addFilter(new Expression($this->customFilter)); + /** + * Return a new query for the given columns + * + * @param array $columns The desired columns, if null all columns will be queried + * + * @return RepositoryQuery + */ + public function select(array $columns = null) + { + $this->initializeQueryColumns(); + + $query = parent::select($columns); + $query->getQuery()->setBase($this->baseDn); + if ($this->filter) { + $query->getQuery()->where(new Expression($this->filter)); } return $query; } /** - * Create a query filtered by the given username + * Initialize this repository's query columns * - * @param string $username - * - * @return Query + * @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet */ - protected function selectUser($username) + protected function initializeQueryColumns() { - return $this->selectUsers()->setUsePagedResults(false)->where( - $this->userNameAttribute, - str_replace('*', '', $username) - ); + if ($this->queryColumns === null) { + if ($this->userClass === null) { + throw new ProgrammingError('It is required to set the objectClass where to look for users first'); + } + if ($this->userNameAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first'); + } + + $this->queryColumns[$this->userClass] = array( + 'user_name' => $this->userNameAttribute, + 'is_active' => 'unknown', // msExchUserAccountControl == 2/512/514? <- AD LDAP + 'created_at' => 'whenCreated', // That's AD LDAP, + 'last_modified' => 'whenChanged' // what's OpenLDAP? + ); + } } /** * Probe the backend to test if authentication is possible * - * Try to bind to the backend and query all available users to check if: + * Try to bind to the backend and fetch a single user to check if: *
      *
    • Connection credentials are correct and the bind is possible
    • *
    • At least one user exists
    • @@ -125,23 +270,23 @@ class LdapUserBackend extends UserBackend public function assertAuthenticationPossible() { try { - $result = $this->selectUsers()->fetchRow(); + $result = $this->select()->fetchRow(); } catch (LdapException $e) { throw new AuthenticationException('Connection not possible.', $e); } if ($result === null) { throw new AuthenticationException( - 'No objects with objectClass="%s" in DN="%s" found. (Filter: %s)', + 'No objects with objectClass "%s" in DN "%s" found. (Filter: %s)', $this->userClass, - $this->baseDn, - $this->customFilter ?: 'None' + $this->baseDn ?: $this->ds->getDn(), + $this->filter ?: 'None' ); } - if (! isset($result->{$this->userNameAttribute})) { + if (! isset($result->user_name)) { throw new AuthenticationException( - 'UserNameAttribute "%s" not existing in objectClass="%s"', + 'UserNameAttribute "%s" not existing in objectClass "%s"', $this->userNameAttribute, $this->userClass ); @@ -163,7 +308,7 @@ class LdapUserBackend extends UserBackend return array(); } - $q = $this->conn->select() + $result = $this->ds->select() ->setBase($this->groupOptions['group_base_dn']) ->from( $this->groupOptions['group_class'], @@ -172,12 +317,10 @@ class LdapUserBackend extends UserBackend ->where( $this->groupOptions['group_member_attribute'], $dn - ); - - $result = $this->conn->fetchAll($q); + ) + ->fetchAll(); $groups = array(); - foreach ($result as $group) { $groups[] = $group->{$this->groupOptions['group_attribute']}; } @@ -186,40 +329,30 @@ class LdapUserBackend extends UserBackend } /** - * Return whether the given user credentials are valid + * Authenticate the given user * - * @param User $user - * @param string $password - * @param boolean $healthCheck Assert that authentication is possible at all + * @param User $user + * @param string $password * - * @return bool + * @return bool True on success, false on failure * - * @throws AuthenticationException In case an error occured or the health check has failed + * @throws AuthenticationException In case authentication is not possible due to an error */ - public function authenticate(User $user, $password, $healthCheck = false) + public function authenticate(User $user, $password) { - if ($healthCheck) { - try { - $this->assertAuthenticationPossible(); - } catch (AuthenticationException $e) { - throw new AuthenticationException( - 'Authentication against backend "%s" not possible.', - $this->getName(), - $e - ); - } - } - try { - $userDn = $this->conn->fetchDN($this->selectUser($user->getUsername())); + $userDn = $this + ->select() + ->where('user_name', str_replace('*', '', $user->getUsername())) + ->getQuery() + ->setUsePagedResults(false) + ->fetchDn(); + if ($userDn === null) { return false; } - $authenticated = $this->conn->testCredentials( - $userDn, - $password - ); + $authenticated = $this->ds->testCredentials($userDn, $password); if ($authenticated) { $groups = $this->getGroups($userDn); if ($groups !== null) { @@ -237,35 +370,4 @@ class LdapUserBackend extends UserBackend ); } } - - /** - * Get the number of users available - * - * @return int - */ - public function count() - { - return $this->selectUsers()->count(); - } - - /** - * Return the names of all available users - * - * @return array - */ - public function listUsers() - { - $users = array(); - foreach ($this->selectUsers()->fetchAll() as $row) { - if (is_array($row->{$this->userNameAttribute})) { - foreach ($row->{$this->userNameAttribute} as $col) { - $users[] = $col; - } - } else { - $users[] = $row->{$this->userNameAttribute}; - } - } - - return $users; - } } diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 8bda07323..3d11289fb 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -160,49 +160,30 @@ class UserBackend $backend = new DbUserBackend($resource); break; case 'msldap': - $groupOptions = array( + $backend = new LdapUserBackend($resource); + $backend->setBaseDn($backendConfig->base_dn); + $backend->setUserClass($backendConfig->get('user_class', 'user')); + $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName')); + $backend->setFilter($backendConfig->filter); + $backend->setGroupOptions(array( 'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()), 'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'), 'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'), 'group_class' => $backendConfig->get('group_class', 'group') - ); - $backend = new LdapUserBackend( - $resource, - $backendConfig->get('user_class', 'user'), - $backendConfig->get('user_name_attribute', 'sAMAccountName'), - $backendConfig->get('base_dn', $resource->getDN()), - $backendConfig->get('filter'), - $groupOptions - ); + )); break; case 'ldap': - if ($backendConfig->user_class === null) { - throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the \'user_class\' directive', - $name - ); - } - if ($backendConfig->user_name_attribute === null) { - throw new ConfigurationError( - 'Authentication configuration for backend "%s" is' - . ' missing the \'user_name_attribute\' directive', - $name - ); - } - $groupOptions = array( + $backend = new LdapUserBackend($resource); + $backend->setBaseDn($backendConfig->base_dn); + $backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson')); + $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid')); + $backend->setFilter($backendConfig->filter); + $backend->setGroupOptions(array( 'group_base_dn' => $backendConfig->group_base_dn, 'group_attribute' => $backendConfig->group_attribute, 'group_member_attribute' => $backendConfig->group_member_attribute, 'group_class' => $backendConfig->group_class - ); - $backend = new LdapUserBackend( - $resource, - $backendConfig->user_class, - $backendConfig->user_name_attribute, - $backendConfig->get('base_dn', $resource->getDN()), - $backendConfig->get('filter'), - $groupOptions - ); + )); break; } diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php index 9b1ed06fc..7378a4095 100644 --- a/modules/setup/application/forms/AdminAccountPage.php +++ b/modules/setup/application/forms/AdminAccountPage.php @@ -268,13 +268,8 @@ class AdminAccountPage extends Form if ($this->backendConfig['backend'] === 'db') { $backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig))); } elseif ($this->backendConfig['backend'] === 'ldap') { - $backend = new LdapUserBackend( - ResourceFactory::createResource(new ConfigObject($this->resourceConfig)), - $this->backendConfig['user_class'], - $this->backendConfig['user_name_attribute'], - $this->backendConfig['base_dn'], - $this->backendConfig['filter'] - ); + $backend = new LdapUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig))); + $backend->setConfig($this->backendConfig); } else { throw new LogicException( sprintf( diff --git a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php b/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php index 43b8bc1d2..442770e83 100644 --- a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php +++ b/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php @@ -29,7 +29,8 @@ class LdapBackendFormTest extends BaseTestCase { $this->setUpResourceFactoryMock(); Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend') - ->shouldReceive('assertAuthenticationPossible')->andReturnNull(); + ->shouldReceive('assertAuthenticationPossible')->andReturnNull() + ->shouldReceive('setConfig')->andReturnNull(); // Passing array(null) is required to make Mockery call the constructor... $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null)); From 437090d2b17089a9a73f7f90feab6484179f42f8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 12:21:17 +0200 Subject: [PATCH 037/239] AdminAccountPage: Backends do provide a unified interface now, use it refs #8826 refs #7693 --- modules/setup/application/forms/AdminAccountPage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php index 7378a4095..a7bd8fc6c 100644 --- a/modules/setup/application/forms/AdminAccountPage.php +++ b/modules/setup/application/forms/AdminAccountPage.php @@ -280,8 +280,8 @@ class AdminAccountPage extends Form } try { - return $backend->listUsers(); - } catch (Exception $e) { + return $backend->select(array('user_name'))->fetchColumn(); + } catch (Exception $_) { // No need to handle anything special here. Error means no users found. return array(); } From 100d475b588db657cec0b24dcfa09c41f1cdb274 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 13:25:07 +0200 Subject: [PATCH 038/239] Fix ldap ConnectionTest If I ever have to look at this test again, I'll drop it. refs #8826 --- test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php b/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php index 182004703..b49250ba5 100644 --- a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php +++ b/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php @@ -126,12 +126,11 @@ class ConnectionTest extends BaseTestCase return Mockery::mock('overload:Icinga\Protocol\Ldap\Query') ->shouldReceive(array( 'from' => Mockery::self(), - 'create' => array('count' => 1), - 'listFields' => array('count' => 1), + 'getColumns' => array('count' => 1), 'getLimit' => 1, 'hasOffset' => false, - 'hasBase' => false, - 'getSortColumns' => array(), + 'getBase' => null, + 'hasOrder' => false, 'getUsePagedResults' => true )); } From 9163fb0f0f5f91d16a06c3f7812bd798c0186f9d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 13:40:54 +0200 Subject: [PATCH 039/239] Drop Icinga\Protocol\Ldap\ConnectionTest ...located at *test*/php/library/Icinga/Protocol/Ldap/. --- .../Icinga/Protocol/Ldap/ConnectionTest.php | 257 ------------------ 1 file changed, 257 deletions(-) delete mode 100644 test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php diff --git a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php b/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php deleted file mode 100644 index b49250ba5..000000000 --- a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php +++ /dev/null @@ -1,257 +0,0 @@ -getAttributesMock; - } - - function ldap_start_tls() - { - global $self; - $self->startTlsCalled = true; - } - - function ldap_set_option($ds, $option, $value) - { - global $self; - $self->activatedOptions[$option] = $value; - return true; - } - - function ldap_set($ds, $option) - { - global $self; - $self->activatedOptions[] = $option; - } - - function ldap_control_paged_result() - { - global $self; - $self->pagedResultsCalled = true; - return true; - } - - function ldap_control_paged_result_response() - { - return true; - } - - function ldap_get_dn() - { - return NULL; - } - - function ldap_free_result() - { - return NULL; - } - } - - private function node(&$element, $name) - { - $element['count']++; - $element[$name] = array('count' => 0); - $element[] = $name; - } - - private function addEntry(&$element, $name, $entry) - { - $element[$name]['count']++; - $element[$name][] = $entry; - } - - private function mockQuery() - { - return Mockery::mock('overload:Icinga\Protocol\Ldap\Query') - ->shouldReceive(array( - 'from' => Mockery::self(), - 'getColumns' => array('count' => 1), - 'getLimit' => 1, - 'hasOffset' => false, - 'getBase' => null, - 'hasOrder' => false, - 'getUsePagedResults' => true - )); - } - - private function connectionFetchAll() - { - $this->mockQuery(); - $this->connection->connect(); - $this->connection->fetchAll(Mockery::self()); - } - - public function setUp() - { - $this->pagedResultsCalled = false; - $this->startTlsCalled = false; - $this->activatedOptions = array(); - - $this->mockLdapFunctions(); - - $config = new ConfigObject( - array( - 'hostname' => 'localhost', - 'root_dn' => 'dc=example,dc=com', - 'bind_dn' => 'cn=user,ou=users,dc=example,dc=com', - 'bind_pw' => '***' - ) - ); - $this->connection = new Connection($config); - - $caps = array('count' => 0); - $this->node($caps, 'defaultNamingContext'); - $this->node($caps, 'namingContexts'); - $this->node($caps, 'supportedCapabilities'); - $this->node($caps, 'supportedControl'); - $this->node($caps, 'supportedLDAPVersion'); - $this->node($caps, 'supportedExtension'); - $this->getAttributesMock = $caps; - } - - public function testUsePageControlWhenAnnounced() - { - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0'); - } - - $this->addEntry($this->getAttributesMock, 'supportedControl', Capability::LDAP_PAGED_RESULT_OID_STRING); - $this->connectionFetchAll(); - - // see ticket #7993 - $this->assertEquals(true, $this->pagedResultsCalled, "Use paged result when capability is present."); - } - - public function testDontUsePagecontrolWhenNotAnnounced() - { - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0'); - } - $this->connectionFetchAll(); - - // see ticket #8490 - $this->assertEquals(false, $this->pagedResultsCalled, "Don't use paged result when capability is not announced."); - } - - public function testUseLdapV2WhenAnnounced() - { - // TODO: Test turned off, see other TODO in Ldap/Connection. - $this->markTestSkipped('LdapV2 currently turned off.'); - - $this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 2); - $this->connectionFetchAll(); - - $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set"); - $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 2); - } - - public function testUseLdapV3WhenAnnounced() - { - $this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 3); - $this->connectionFetchAll(); - - $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set"); - $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active"); - } - - public function testDefaultSettings() - { - $this->connectionFetchAll(); - - $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set"); - $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active"); - - $this->assertArrayHasKey(LDAP_OPT_REFERRALS, $this->activatedOptions, "Following referrals must be turned off"); - $this->assertEquals($this->activatedOptions[LDAP_OPT_REFERRALS], 0, "Following referrals must be turned off"); - } - - - public function testActiveDirectoryDiscovery() - { - $this->addEntry($this->getAttributesMock, 'supportedCapabilities', Capability::LDAP_CAP_ACTIVE_DIRECTORY_OID); - $this->connectionFetchAll(); - - $this->assertEquals(true, $this->connection->getCapabilities()->hasAdOid(), - "Server with LDAP_CAP_ACTIVE_DIRECTORY_OID must be recognized as Active Directory."); - } - - public function testDefaultNamingContext() - { - $this->addEntry($this->getAttributesMock, 'defaultNamingContext', 'dn=default,dn=contex'); - $this->connectionFetchAll(); - - $this->assertEquals('dn=default,dn=contex', $this->connection->getCapabilities()->getDefaultNamingContext(), - 'Default naming context must be correctly recognized.'); - } - - public function testDefaultNamingContextFallback() - { - $this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=some,dn=other,dn=context'); - $this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=default,dn=context'); - $this->connectionFetchAll(); - - $this->assertEquals('dn=some,dn=other,dn=context', $this->connection->getCapabilities()->getDefaultNamingContext(), - 'If defaultNamingContext is missing, the connection must fallback to first namingContext.'); - } -} From b86a0024c35e80d683fe9783437268dcea4d126e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 15:55:36 +0200 Subject: [PATCH 040/239] DbUserBackend: Use is_active as well as a default sort column refs #8826 --- library/Icinga/Authentication/User/DbUserBackend.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 08c2006d6..3cea0ccc9 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -46,7 +46,10 @@ class DbUserBackend extends DbRepository implements UserBackendInterface */ protected $sortRules = array( 'user_name' => array( - 'order' => 'asc' + 'columns' => array( + 'user_name asc', + 'is_active desc' + ) ) ); From 842b043f7fba9dc280ae23d059619986de13a845 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 15:56:13 +0200 Subject: [PATCH 041/239] LdapUserBackend: Use is_active as well as a default sort column refs #8826 --- library/Icinga/Authentication/User/LdapUserBackend.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 9b2012b46..e7fa6767b 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -49,7 +49,10 @@ class LdapUserBackend extends Repository implements UserBackendInterface */ protected $sortRules = array( 'user_name' => array( - 'order' => 'asc' + 'columns' => array( + 'user_name asc', + 'is_active desc' + ) ) ); From f9089c3e0c3fab228d5ac3f3aaefb2b7d8330f8c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 15:56:58 +0200 Subject: [PATCH 042/239] RepositoryQuery: Ensure compatibility with the FilterEditor widget refs #8826 --- library/Icinga/Repository/RepositoryQuery.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index df5d17cd6..e9acb945b 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -65,6 +65,16 @@ class RepositoryQuery implements QueryInterface return $this; } + /** + * Return the columns to fetch + * + * @return array + */ + public function getColumns() + { + return $this->query->getColumns(); + } + /** * Set which columns to fetch * From 3e8ef5cc0f86753794592b448027973f370d19b9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 16:17:14 +0200 Subject: [PATCH 043/239] Ldap\Query: Quick fix for naive filter support Since this will ignore any logical clauses and operators it must be considered a quick fix and be dropped once real filter support exists. refs #8826 --- library/Icinga/Protocol/Ldap/Query.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Query.php b/library/Icinga/Protocol/Ldap/Query.php index d923c8ebd..d5bf1f080 100644 --- a/library/Icinga/Protocol/Ldap/Query.php +++ b/library/Icinga/Protocol/Ldap/Query.php @@ -126,14 +126,17 @@ class Query extends SimpleQuery throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); } - public function applyFilter(Filter $filter) - { - throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); - } - public function addFilter(Filter $filter) { - throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead'); + // TODO: This should be considered a quick fix only. + // Drop this entirely once support for Icinga\Data\Filter is available + if ($filter->isExpression()) { + $this->where($filter->getColumn(), $filter->getValue()); + } elseif ($filter->isChain()) { + foreach ($filter->filters() as $chainOrExpression) { + $this->addFilter($chainOrExpression); + } + } } public function setFilter(Filter $filter) From d0a353c3da2d5de43803779f7e35053beb0d1866 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 16:24:17 +0200 Subject: [PATCH 044/239] Ldap\Connection: Fix result counting Missed to adjust this once I refactored the query execution.. refs #8826 --- library/Icinga/Protocol/Ldap/Connection.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index edda85dcf..9296c5f88 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -241,14 +241,9 @@ class Connection implements Selectable $this->connect(); $this->bind(); - $count = 0; - $results = $this->runQuery($query); - while (! empty($results)) { - $count += ldap_count_entries($this->ds, $results); - $results = $this->runQuery($query); - } - - return $count; + // TODO: That's still not the best solution, this should probably not request any attributes + $res = $this->runQuery($query); + return count($res); } public function fetchAll(Query $query, $fields = array()) From d171dd2ec91ec79ce7c9d718188c0d2bab4024e1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 4 May 2015 17:04:50 +0200 Subject: [PATCH 045/239] Introduce controller UserController Still only able to list users, more to follow... refs #8826 --- application/controllers/UserController.php | 102 +++++++++++++++++++++ application/views/scripts/user/list.phtml | 49 ++++++++++ library/Icinga/Web/Menu.php | 17 ++-- public/css/icinga/main-content.less | 20 ++++ 4 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 application/controllers/UserController.php create mode 100644 application/views/scripts/user/list.phtml diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php new file mode 100644 index 000000000..5ffbcdf25 --- /dev/null +++ b/application/controllers/UserController.php @@ -0,0 +1,102 @@ +redirectNow('user/list'); + } + + /** + * List all users of a single backend + */ + public function listAction() + { + $backend = $this->getUserBackend($this->params->get('backend')); + if ($backend === null) { + $this->view->backend = null; + return; + } + + $query = $backend->select(array( + 'user_name', + 'is_active', + 'created_at', + 'last_modified' + )); + + $filterEditor = Widget::create('filterEditor') + ->setQuery($query) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend') + ->ignoreParams('page') + ->handleRequest($this->getRequest()); + $query->applyFilter($filterEditor->getFilter()); + $this->setupFilterControl($filterEditor); + + $this->view->backend = $backend; + $this->view->users = $query->paginate(); + + $this->setupLimitControl(); + $this->setupPaginationControl($this->view->users); + $this->setupSortControl(array( + 'user_name' => $this->translate('Username'), + 'is_active' => $this->translate('Active'), + 'created_at' => $this->translate('Created at'), + 'last_modified' => $this->translate('Last modified') + )); + } + + /** + * Return the given user backend or the first match in order + * + * @param string $name The name of the backend, or null in case the first match should be returned + * @param bool $selectable Whether the backend should implement the Selectable interface + * + * @return UserBackendInterface + * + * @throws Zend_Controller_Action_Exception In case the given backend name is invalid + */ + protected function getUserBackend($name = null, $selectable = true) + { + $config = Config::app('authentication'); + if ($name !== null) { + if (! $config->hasSection($name)) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('Authentication backend "%s" not found'), $name), + 404 + ); + } else { + $backend = UserBackend::create($name, $config->getSection($name)); + if ($selectable && !$backend instanceof Selectable) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('Authentication backend "%s" is not able to list users'), $name), + 400 + ); + } + } + } else { + $backend = null; + foreach ($config as $backendName => $backendConfig) { + $candidate = UserBackend::create($backendName, $backendConfig); + if (! $selectable || $candidate instanceof Selectable) { + $backend = $candidate; + break; + } + } + } + + return $backend; + } +} diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml new file mode 100644 index 000000000..8ccc37bee --- /dev/null +++ b/application/views/scripts/user/list.phtml @@ -0,0 +1,49 @@ +compact): ?> +
      + tabs; ?> + sortBox; ?> + limiter; ?> + paginator; ?> + filterEditor; ?> +
      + +
      +translate('No backend found which is able to list users') . '
      '; + return; +} + +if (count($users) === 0) { + echo $this->translate('No users found matching the filter') . ''; + return; +} +?> + + + + + + + + + + + + + + + + + + + + +
      translate('Username'); ?>translate('State'); ?>translate('Created at'); ?>translate('Last modified'); ?>
      escape($user->user_name); ?>is_active === null ? $this->translate('N/A') : ( + $user->is_active ? $this->translate('Active') : $this->translate('Inactive') + ); ?> + created_at === null ? $this->translate('N/A') : date('d/m/Y g:i A', $user->created_at); ?> + + last_modified === null ? $this->translate('Never') : date('d/m/Y g:i A', $user->last_modified); ?> +
      \ No newline at end of file diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index bca1e17d1..aff6c7abb 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -236,36 +236,41 @@ class Menu implements RecursiveIterator 'icon' => 'wrench', 'priority' => 200 )); + $section->add(t('User-Management'), array( + 'url' => 'user/list', + 'permission' => 'config/application/*', + 'priority' => 300 + )); $section->add(t('Configuration'), array( 'url' => 'config', 'permission' => 'config/application/*', - 'priority' => 300 + 'priority' => 400 )); $section->add(t('Modules'), array( 'url' => 'config/modules', 'permission' => 'config/modules', - 'priority' => 400 + 'priority' => 500 )); if (Logger::writesToFile()) { $section->add(t('Application Log'), array( 'url' => 'list/applicationlog', - 'priority' => 500 + 'priority' => 600 )); } $section = $this->add($auth->getUser()->getUsername(), array( 'icon' => 'user', - 'priority' => 600 + 'priority' => 700 )); $section->add(t('Preferences'), array( 'url' => 'preference', - 'priority' => 601 + 'priority' => 701 )); $section->add(t('Logout'), array( 'url' => 'authentication/logout', - 'priority' => 700, + 'priority' => 800, 'renderer' => 'ForeignMenuItemRenderer' )); } diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index b442723cb..06a0b1b90 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -202,3 +202,23 @@ table.benchmark { border: 1px solid lightgrey; background-color: #fbfcc5; } + +table.user-list { + th { + &.user-state { + width: 6%; + padding-right: 0.5em; + text-align: right; + } + + &.user-created, &.user-modified { + width: 12%; + padding-right: 0.5em; + text-align: right; + } + } + + td.user-state, td.user-created, td.user-modified { + text-align: right; + } +} \ No newline at end of file From 271e350faae3e476402757f8a016cf9ed284ecec Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 07:12:25 +0200 Subject: [PATCH 046/239] UserController: Add missing closing div tag to the list action's view script refs #8826 --- application/views/scripts/user/list.phtml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 8ccc37bee..b6ff7e0ef 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -46,4 +46,5 @@ if (count($users) === 0) { - \ No newline at end of file + + \ No newline at end of file From d71df6a9b85ed2206fecb9938b463b516fd61798 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 07:30:42 +0200 Subject: [PATCH 047/239] Revert "SimpleQuery: Make compare() alias aware" This reverts commit 6612e4c1ae79c2bc3a5cd08e1594fc152107618e. --- library/Icinga/Data/SimpleQuery.php | 64 ++++++++++------------------- 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index 1507ba589..a069e03cd 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -43,15 +43,6 @@ class SimpleQuery implements QueryInterface */ protected $columns = array(); - /** - * The columns and their aliases flipped in order to handle aliased sort columns - * - * Supposed to be used and populated by $this->compare *only*. - * - * @var array - */ - protected $flippedColumns; - /** * The columns you're using to sort the query result * @@ -228,42 +219,32 @@ class SimpleQuery implements QueryInterface return $this; } - /** - * Compare $a with $b based on this query's sort rules and column aliases - * - * @param object $a - * @param object $b - * @param int $orderIndex - * - * @return int - */ - public function compare($a, $b, $orderIndex = 0) + public function compare($a, $b, $col_num = 0) { - if (! array_key_exists($orderIndex, $this->order)) { - return 0; // Last column to sort reached, rows are considered being equal + // Last column to sort reached, rows are considered being equal + if (! array_key_exists($col_num, $this->order)) { + return 0; + } + $col = $this->order[$col_num][0]; + $dir = $this->order[$col_num][1]; +// TODO: throw Exception if column is missing + //$res = strnatcmp(strtolower($a->$col), strtolower($b->$col)); + $res = @strcmp(strtolower($a->$col), strtolower($b->$col)); + if ($res === 0) { +// return $this->compare($a, $b, $col_num++); + + if (array_key_exists(++$col_num, $this->order)) { + return $this->compare($a, $b, $col_num); + } else { + return 0; + } + } - if ($this->flippedColumns === null) { - $this->flippedColumns = array_flip($this->columns); - } - - $column = $this->order[$orderIndex][0]; - if (array_key_exists($column, $this->flippedColumns)) { - $column = $this->flippedColumns[$column]; - } - - // TODO: throw Exception if column is missing - //$res = strnatcmp(strtolower($a->$column), strtolower($b->$column)); - $result = @strcmp(strtolower($a->$column), strtolower($b->$column)); - if ($result === 0) { - return $this->compare($a, $b, $orderIndex); - } - - $direction = $this->order[$orderIndex][1]; - if ($direction === self::SORT_ASC) { - return $result; + if ($dir === self::SORT_ASC) { + return $res; } else { - return $result * -1; + return $res * -1; } } @@ -445,7 +426,6 @@ class SimpleQuery implements QueryInterface public function columns(array $columns) { $this->columns = $columns; - $this->flippedColumns = null; // Reset, due to updated columns return $this; } From bd136d39f472be8db78861214b44212f52e4dcfb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 07:31:50 +0200 Subject: [PATCH 048/239] SimpleQuery: Make compare() alias aware refs #8826 refs #7693 --- library/Icinga/Data/SimpleQuery.php | 64 +++++++++++++++++++---------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index a069e03cd..3477c2331 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -43,6 +43,15 @@ class SimpleQuery implements QueryInterface */ protected $columns = array(); + /** + * The columns and their aliases flipped in order to handle aliased sort columns + * + * Supposed to be used and populated by $this->compare *only*. + * + * @var array + */ + protected $flippedColumns; + /** * The columns you're using to sort the query result * @@ -219,32 +228,42 @@ class SimpleQuery implements QueryInterface return $this; } - public function compare($a, $b, $col_num = 0) + /** + * Compare $a with $b based on this query's sort rules and column aliases + * + * @param object $a + * @param object $b + * @param int $orderIndex + * + * @return int + */ + public function compare($a, $b, $orderIndex = 0) { - // Last column to sort reached, rows are considered being equal - if (! array_key_exists($col_num, $this->order)) { - return 0; - } - $col = $this->order[$col_num][0]; - $dir = $this->order[$col_num][1]; -// TODO: throw Exception if column is missing - //$res = strnatcmp(strtolower($a->$col), strtolower($b->$col)); - $res = @strcmp(strtolower($a->$col), strtolower($b->$col)); - if ($res === 0) { -// return $this->compare($a, $b, $col_num++); - - if (array_key_exists(++$col_num, $this->order)) { - return $this->compare($a, $b, $col_num); - } else { - return 0; - } - + if (! array_key_exists($orderIndex, $this->order)) { + return 0; // Last column to sort reached, rows are considered being equal } - if ($dir === self::SORT_ASC) { - return $res; + if ($this->flippedColumns === null) { + $this->flippedColumns = array_flip($this->columns); + } + + $column = $this->order[$orderIndex][0]; + if (array_key_exists($column, $this->flippedColumns)) { + $column = $this->flippedColumns[$column]; + } + + // TODO: throw Exception if column is missing + //$res = strnatcmp(strtolower($a->$column), strtolower($b->$column)); + $result = @strcmp(strtolower($a->$column), strtolower($b->$column)); + if ($result === 0) { + return $this->compare($a, $b, ++$orderIndex); + } + + $direction = $this->order[$orderIndex][1]; + if ($direction === self::SORT_ASC) { + return $result; } else { - return $res * -1; + return $result * -1; } } @@ -426,6 +445,7 @@ class SimpleQuery implements QueryInterface public function columns(array $columns) { $this->columns = $columns; + $this->flippedColumns = null; // Reset, due to updated columns return $this; } From 8cf0c29223c71a88229a05667dd596c80bcd25a4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 07:36:14 +0200 Subject: [PATCH 049/239] UserController: Add tab for the list action refs #8826 --- application/controllers/UserController.php | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 5ffbcdf25..b5fcc50f1 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -11,6 +11,14 @@ use Icinga\Web\Widget; class UserController extends Controller { + /** + * Initialize this controller + */ + public function init() + { + $this->createTabs(); + } + /** * Redirect to this controller's list action */ @@ -45,6 +53,7 @@ class UserController extends Controller $query->applyFilter($filterEditor->getFilter()); $this->setupFilterControl($filterEditor); + $this->getTabs()->activate('user/list'); $this->view->backend = $backend; $this->view->users = $query->paginate(); @@ -99,4 +108,21 @@ class UserController extends Controller return $backend; } + + /** + * Create the tabs + */ + protected function createTabs() + { + $tabs = $this->getTabs(); + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'icon' => 'users', + 'url' => 'user/list' + ) + ); + } } From 7b2fc1ba41154d8778d671cd4e497ea84922160d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 08:26:38 +0200 Subject: [PATCH 050/239] Make class UserGroupBackend being just a factory for user group backends refs #8826 --- .../UserGroup/UserGroupBackend.php | 81 +++++-------------- 1 file changed, 22 insertions(+), 59 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php index 410b5114d..a6382c8e5 100644 --- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -8,19 +8,18 @@ use Icinga\Application\Icinga; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; -use Icinga\User; /** - * Base class and factory for user group backends + * Factory for user group backends */ -abstract class UserGroupBackend +class UserGroupBackend { /** * The default user group backend types provided by Icinga Web 2 * * @var array */ - private static $defaultBackends = array( // I would have liked it if I were able to declare this as constant :'( + protected static $defaultBackends = array( 'db', 'ini' ); @@ -33,39 +32,9 @@ abstract class UserGroupBackend protected static $customBackends; /** - * The name of this backend - * - * @var string + * Register all custom user group backends from all loaded modules */ - protected $name; - - /** - * Set this backend's name - * - * @param string $name - * - * @return $this - */ - public function setName($name) - { - $this->name = (string) $name; - return $this; - } - - /** - * Return this backend's name - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Fetch all custom user group backends from all loaded modules - */ - public static function loadCustomUserGroupBackends() + public static function registerCustomUserGroupBackends() { if (static::$customBackends !== null) { return; @@ -77,7 +46,7 @@ abstract class UserGroupBackend foreach ($module->getUserGroupBackends() as $identifier => $className) { if (array_key_exists($identifier, $providedBy)) { Logger::warning( - 'Cannot register UserGroupBackend of type "%s" provided by module "%s".' + 'Cannot register user group backend of type "%s" provided by module "%s".' . ' The type is already provided by module "%s"', $identifier, $module->getName(), @@ -85,7 +54,7 @@ abstract class UserGroupBackend ); } elseif (in_array($identifier, static::$defaultBackends)) { Logger::warning( - 'Cannot register UserGroupBackend of type "%s" provided by module "%s".' + 'Cannot register user group backend of type "%s" provided by module "%s".' . ' The type is a default type provided by Icinga Web 2', $identifier, $module->getName() @@ -99,29 +68,23 @@ abstract class UserGroupBackend } /** - * Validate and return the class for the given custom user group backend + * Return the class for the given custom user group backend * * @param string $identifier The identifier of the custom user group backend * * @return string|null The name of the class or null in case there was no * backend found with the given identifier * - * @throws ConfigurationError In case the class could not be successfully validated + * @throws ConfigurationError In case the class associated to the given identifier does not exist */ protected static function getCustomUserGroupBackend($identifier) { - static::loadCustomUserGroupBackends(); + static::registerCustomUserGroupBackends(); if (array_key_exists($identifier, static::$customBackends)) { $className = static::$customBackends[$identifier]; if (! class_exists($className)) { throw new ConfigurationError( - 'Cannot utilize UserGroupBackend of type "%s". Class "%s" does not exist', - $identifier, - $className - ); - } elseif (! is_subclass_of($className, __CLASS__)) { - throw new ConfigurationError( - 'Cannot utilize UserGroupBackend of type "%s". Class "%s" is not a sub-type of UserGroupBackend', + 'Cannot utilize user group backend of type "%s". Class "%s" does not exist', $identifier, $className ); @@ -132,12 +95,12 @@ abstract class UserGroupBackend } /** - * Create and return a UserGroupBackend with the given name and given configuration applied to it + * Create and return a user group backend with the given name and given configuration applied to it * * @param string $name * @param ConfigObject $backendConfig * - * @return UserGroupBackend + * @return UserGroupBackendInterface * * @throws ConfigurationError */ @@ -158,6 +121,15 @@ abstract class UserGroupBackend // Do not attempt to load a custom user group backend unless it's actually required } elseif (($customClass = static::getCustomUserGroupBackend($backendType)) !== null) { $backend = new $customClass($backendConfig); + if (! is_a($backend, 'Icinga\Authentication\UserGroup\UserGroupBackendInterface')) { + throw new ConfigurationError( + 'Cannot utilize user group backend of type "%s".' + . ' Class "%s" does not implement UserGroupBackendInterface', + $backendType, + $customClass + ); + } + $backend->setName($name); return $backend; } else { @@ -189,13 +161,4 @@ abstract class UserGroupBackend $backend->setName($name); return $backend; } - - /** - * Get the groups the given user is a member of - * - * @param User $user - * - * @return array - */ - abstract public function getMemberships(User $user); } From b1454c199ade46ac6d82d5bd158b77cc4e4bcc24 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 08:27:11 +0200 Subject: [PATCH 051/239] Introduce interface UserGroupBackendInterface refs #8826 --- .../UserGroup/UserGroupBackendInterface.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php new file mode 100644 index 000000000..a567d1f0a --- /dev/null +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php @@ -0,0 +1,37 @@ + Date: Tue, 5 May 2015 09:23:29 +0200 Subject: [PATCH 052/239] DbUserGroupBackend: Extend DbRepository and implement UserGroupBackendInterface refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index a5a2a539a..7e5341e69 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -3,59 +3,80 @@ namespace Icinga\Authentication\UserGroup; -use Icinga\Data\Db\DbConnection; +use Icinga\Repository\DbRepository; use Icinga\User; -/** - * Database user group backend - */ -class DbUserGroupBackend extends UserGroupBackend +class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterface { /** - * Connection to the database + * The query columns being provided * - * @var DbConnection + * @var array */ - private $conn; + protected $queryColumns = array( + 'group' => array( + 'group_name' => 'name', + 'parent_name' => 'parent', + 'created_at' => 'UNIX_TIMESTAMP(ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(mtime)' + ) + ); /** - * Create a new database user group backend + * The default sort rules to be applied on a query * - * @param DbConnection $conn + * @var array */ - public function __construct(DbConnection $conn) + protected $sortRules = array( + 'group_name' => array( + 'columns' => array( + 'group_name', + 'parent_name' + ) + ) + ); + + /** + * Initialize this database user group backend + */ + protected function init() { - $this->conn = $conn; + if (! $this->ds->getTablePrefix()) { + $this->ds->setTablePrefix('icingaweb_'); + } } /** - * (non-PHPDoc) - * @see UserGroupBackend::getMemberships() For the method documentation. + * Return the groups the given user is a member of + * + * @param User $user + * + * @return array */ public function getMemberships(User $user) { $groups = array(); - $groupsStmt = $this->conn->getDbAdapter() - ->select() - ->from($this->conn->getTablePrefix() . 'group', array('name', 'parent')) - ->query(); + $groupsStmt = $this->select(array('group_name', 'parent_name'))->getQuery()->getSelectQuery()->query(); foreach ($groupsStmt as $group) { - $groups[$group->name] = $group->parent; + $groups[$group->group_name] = $group->parent_name; } + $memberships = array(); - $membershipsStmt = $this->conn->getDbAdapter() + $membershipsStmt = $this->ds->getDbAdapter() // TODO: Use the join feature, once available ->select() - ->from($this->conn->getTablePrefix() . 'group_membership', array('group_name')) + ->from($this->ds->getTablePrefix() . 'group_membership', array('group_name')) ->where('username = ?', $user->getUsername()) ->query(); foreach ($membershipsStmt as $membership) { $memberships[] = $membership->group_name; $parent = $groups[$membership->group_name]; - while (isset($parent)) { + while ($parent !== null) { $memberships[] = $parent; - $parent = $groups[$parent]; + // Usually a parent is an existing group, but since we do not have a constraint on our table.. + $parent = isset($groups[$parent]) ? $groups[$parent] : null; } } + return $memberships; } } From 1682b0ee32f904d8df119dd137a258bce7070980 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 09:24:28 +0200 Subject: [PATCH 053/239] Introduce controller GroupController Still only able to list groups, more to follow... refs #8826 --- application/controllers/GroupController.php | 137 ++++++++++++++++++++ application/controllers/UserController.php | 9 ++ application/views/scripts/group/list.phtml | 50 +++++++ public/css/icinga/main-content.less | 20 +++ 4 files changed, 216 insertions(+) create mode 100644 application/controllers/GroupController.php create mode 100644 application/views/scripts/group/list.phtml diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php new file mode 100644 index 000000000..ffa32584a --- /dev/null +++ b/application/controllers/GroupController.php @@ -0,0 +1,137 @@ +createTabs(); + } + + /** + * Redirect to this controller's list action + */ + public function indexAction() + { + $this->redirectNow('group/list'); + } + + /** + * List all user groups of a single backend + */ + public function listAction() + { + $backend = $this->getUserGroupBackend($this->params->get('backend')); + if ($backend === null) { + $this->view->backend = null; + return; + } + + $query = $backend->select(array( + 'group_name', + 'parent_name', + 'created_at', + 'last_modified' + )); + + $filterEditor = Widget::create('filterEditor') + ->setQuery($query) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend') + ->ignoreParams('page') + ->handleRequest($this->getRequest()); + $query->applyFilter($filterEditor->getFilter()); + $this->setupFilterControl($filterEditor); + + $this->getTabs()->activate('group/list'); + $this->view->backend = $backend; + $this->view->groups = $query->paginate(); + + $this->setupLimitControl(); + $this->setupPaginationControl($this->view->groups); + $this->setupSortControl(array( + 'group_name' => $this->translate('Group'), + 'parent_name' => $this->translate('Parent'), + 'created_at' => $this->translate('Created at'), + 'last_modified' => $this->translate('Last modified') + )); + } + + /** + * Return the given user group backend or the first match in order + * + * @param string $name The name of the backend, or null in case the first match should be returned + * @param bool $selectable Whether the backend should implement the Selectable interface + * + * @return UserGroupBackendInterface + * + * @throws Zend_Controller_Action_Exception In case the given backend name is invalid + */ + protected function getUserGroupBackend($name = null, $selectable = true) + { + $config = Config::app('groups'); + if ($name !== null) { + if (! $config->hasSection($name)) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('User group backend "%s" not found'), $name), + 404 + ); + } else { + $backend = UserGroupBackend::create($name, $config->getSection($name)); + if ($selectable && !$backend instanceof Selectable) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('User group backend "%s" is not able to list groups'), $name), + 400 + ); + } + } + } else { + $backend = null; + foreach ($config as $backendName => $backendConfig) { + $candidate = UserGroupBackend::create($backendName, $backendConfig); + if (! $selectable || $candidate instanceof Selectable) { + $backend = $candidate; + break; + } + } + } + + return $backend; + } + + /** + * Create the tabs + */ + protected function createTabs() + { + $tabs = $this->getTabs(); + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'icon' => 'users', + 'url' => 'user/list' + ) + ); + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('Groups'), + 'icon' => 'cubes', + 'url' => 'group/list' + ) + ); + } +} diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index b5fcc50f1..ccb5aac03 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -124,5 +124,14 @@ class UserController extends Controller 'url' => 'user/list' ) ); + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('Groups'), + 'icon' => 'cubes', + 'url' => 'group/list' + ) + ); } } diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml new file mode 100644 index 000000000..3871b675e --- /dev/null +++ b/application/views/scripts/group/list.phtml @@ -0,0 +1,50 @@ +compact): ?> +
      + tabs; ?> + sortBox; ?> + limiter; ?> + paginator; ?> + filterEditor; ?> +
      + +
      +translate('No backend found which is able to list groups') . '
      '; + return; +} + +if (count($groups) === 0) { + echo $this->translate('No groups found matching the filter') . ''; + return; +} +?> + + + + + + + + + + + + + + + + + + + + +
      translate('Group'); ?>translate('Parent'); ?>translate('Created at'); ?>translate('Last modified'); ?>
      escape($group->group_name); ?> + parent_name === null ? $this->translate('None', 'user.group.parent') : $this->escape($group->parent_name); ?> + + created_at === null ? $this->translate('N/A') : date('d/m/Y g:i A', $group->created_at); ?> + + last_modified === null ? $this->translate('Never') : date('d/m/Y g:i A', $group->last_modified); ?> +
      + \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 06a0b1b90..830ecdd44 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -221,4 +221,24 @@ table.user-list { td.user-state, td.user-created, td.user-modified { text-align: right; } +} + +table.group-list { + th { + &.group-parent { + width: 6%; + padding-right: 0.5em; + text-align: right; + } + + &.group-created, &.group-modified { + width: 12%; + padding-right: 0.5em; + text-align: right; + } + } + + td.group-parent, td.group-created, td.group-modified { + text-align: right; + } } \ No newline at end of file From 37e47f0d3f9a21d9f68253bfe2e0c49704159845 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 09:34:23 +0200 Subject: [PATCH 054/239] DbUserBackend: Add case insensitive filter column `user' refs #8826 --- library/Icinga/Authentication/User/DbUserBackend.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 3cea0ccc9..367418f7b 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -32,6 +32,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface */ protected $queryColumns = array( 'user' => array( + 'user' => 'name COLLATE utf8_general_ci', 'user_name' => 'name', 'is_active' => 'active', 'created_at' => 'UNIX_TIMESTAMP(ctime)', @@ -39,6 +40,13 @@ class DbUserBackend extends DbRepository implements UserBackendInterface ) ); + /** + * The columns which are not permitted to be queried + * + * @var array + */ + protected $filterColumns = array('user'); + /** * The default sort rules to be applied on a query * From de68d7893865ae29ed1dae9470de9c446240fd48 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 09:34:49 +0200 Subject: [PATCH 055/239] DbUserGroupBackend: Add case insensitive filter columns `group' and `parent' refs #8826 --- .../Authentication/UserGroup/DbUserGroupBackend.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index 7e5341e69..32d65ab53 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -15,13 +15,22 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ protected $queryColumns = array( 'group' => array( + 'group' => 'name COLLATE utf8_general_ci', 'group_name' => 'name', + 'parent' => 'parent COLLATE utf8_general_ci', 'parent_name' => 'parent', 'created_at' => 'UNIX_TIMESTAMP(ctime)', 'last_modified' => 'UNIX_TIMESTAMP(mtime)' ) ); + /** + * The columns which are not permitted to be queried + * + * @var array + */ + protected $filterColumns = array('group', 'parent'); + /** * The default sort rules to be applied on a query * From 5cc7f267282932dca38e279dae97227d24cc9321 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 15:21:34 +0200 Subject: [PATCH 056/239] ConfigObject: Extend ArrayDatasource This makes it possible to use a ini file as repository!!!1 One thing is missing: Section names are currently ignored and should be mapped to a virtual column. refs #8826 --- library/Icinga/Application/Config.php | 16 ++++++++++-- library/Icinga/Data/ConfigObject.php | 36 +++++++-------------------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php index 8a1746b69..6bad014a6 100644 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Application/Config.php @@ -9,13 +9,15 @@ use LogicException; use UnexpectedValueException; use Icinga\Util\File; use Icinga\Data\ConfigObject; +use Icinga\Data\Selectable; +use Icinga\Data\SimpleQuery; use Icinga\File\Ini\IniWriter; use Icinga\Exception\NotReadableError; /** * Container for INI like configuration and global registry of application and module related configuration. */ -class Config implements Countable, Iterator +class Config implements Countable, Iterator, Selectable { /** * Configuration directory where ALL (application and module) configuration is located @@ -85,6 +87,16 @@ class Config implements Countable, Iterator return $this; } + /** + * Provide a query for the internal config object + * + * @return SimpleQuery + */ + public function select() + { + return $this->config->select(); + } + /** * Return the count of available sections * @@ -92,7 +104,7 @@ class Config implements Countable, Iterator */ public function count() { - return $this->config->count(); + return $this->select()->count(); } /** diff --git a/library/Icinga/Data/ConfigObject.php b/library/Icinga/Data/ConfigObject.php index 641d72846..de00e5b5e 100644 --- a/library/Icinga/Data/ConfigObject.php +++ b/library/Icinga/Data/ConfigObject.php @@ -4,22 +4,15 @@ namespace Icinga\Data; use Iterator; -use Countable; use ArrayAccess; -use LogicException; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Exception\ProgrammingError; /** * Container for configuration values */ -class ConfigObject implements Countable, Iterator, ArrayAccess +class ConfigObject extends ArrayDatasource implements Iterator, ArrayAccess { - /** - * This config's data - * - * @var array - */ - protected $data; - /** * Create a new config * @@ -27,15 +20,14 @@ class ConfigObject implements Countable, Iterator, ArrayAccess */ public function __construct(array $data = array()) { - $this->data = array(); - - foreach ($data as $key => $value) { + // Convert all embedded arrays to ConfigObjects as well + foreach ($data as & $value) { if (is_array($value)) { - $this->data[$key] = new static($value); - } else { - $this->data[$key] = $value; + $value = new static($value); } } + + parent::__construct($data); } /** @@ -55,16 +47,6 @@ class ConfigObject implements Countable, Iterator, ArrayAccess $this->data = $array; } - /** - * Return the count of available sections and properties - * - * @return int - */ - public function count() - { - return count($this->data); - } - /** * Reset the current position of $this->data * @@ -197,7 +179,7 @@ class ConfigObject implements Countable, Iterator, ArrayAccess public function offsetSet($key, $value) { if ($key === null) { - throw new LogicException('Appending values without an explicit key is not supported'); + throw new ProgrammingError('Appending values without an explicit key is not supported'); } $this->$key = $value; From 89029308ef578bbfa0f49e9a76d81656578bbb72 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 15:24:18 +0200 Subject: [PATCH 057/239] IniUserGroupBackend: Extend Repository and implement UserGroupBackendInterface Note that it was necessary to change the structure of ini files providing the membership information. They need to be structured like our db table rows now. refs #8826 --- .../UserGroup/IniUserGroupBackend.php | 97 ++++++++++++------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index ad9178899..6377bd9c0 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -3,61 +3,84 @@ namespace Icinga\Authentication\UserGroup; -use Icinga\Application\Config; -use Icinga\Exception\ConfigurationError; +use Icinga\Repository\Repository; use Icinga\User; use Icinga\Util\String; -/** - * INI user group backend - */ -class IniUserGroupBackend extends UserGroupBackend +class IniUserGroupBackend extends Repository implements UserGroupBackendInterface { /** - * Config + * The query columns being provided * - * @var Config + * @var array */ - private $config; + protected $queryColumns = array( + 'groups' => array( + 'group' => 'name', + 'group_name' => 'name', + 'parent' => 'parent', + 'parent_name' => 'parent', + 'created_at' => 'ctime', + 'last_modified' => 'mtime', + 'users' + ) + ); /** - * Create a new INI user group backend + * The columns which are not permitted to be queried * - * @param Config $config + * @var array */ - public function __construct(Config $config) - { - $this->config = $config; - } + protected $filterColumns = array('group', 'parent'); /** - * (non-PHPDoc) - * @see UserGroupBackend::getMemberships() For the method documentation. + * The default sort rules to be applied on a query + * + * @var array + */ + protected $sortRules = array( + 'group_name' => array( + 'columns' => array( + 'group_name', + 'parent_name' + ) + ) + ); + + /** + * Return the groups the given user is a member of + * + * @param User $user + * + * @return array */ public function getMemberships(User $user) { - $username = strtolower($user->getUsername()); + $result = $this->select()->fetchAll(); + $groups = array(); - foreach ($this->config as $name => $section) { - if (empty($section->users)) { - throw new ConfigurationError( - 'Membership section \'%s\' in \'%s\' is missing the \'users\' section', - $name, - $this->config->getConfigFile() - ); - } - if (empty($section->groups)) { - throw new ConfigurationError( - 'Membership section \'%s\' in \'%s\' is missing the \'groups\' section', - $name, - $this->config->getConfigFile() - ); - } - $users = array_map('strtolower', String::trimSplit($section->users)); - if (in_array($username, $users)) { - $groups = array_merge($groups, array_diff(String::trimSplit($section->groups), $groups)); + foreach ($result as $group) { + if ($group->group_name) { // TODO: Can we set this somehow automatically to the section's name?? + $groups[$group->group_name] = $group->parent_name; } } - return $groups; + + $username = strtolower($user->getUsername()); + $memberships = array(); + foreach ($result as $group) { + if ($group->group_name && $group->users) { + $users = array_map('strtolower', String::trimSplit($group->users)); + if (! in_array($group->group_name, $memberships) && in_array($username, $users)) { + $memberships[] = $group->group_name; + $parent = $groups[$group->group_name]; + while ($parent !== null) { + $memberships[] = $parent; + $parent = isset($groups[$parent]) ? $groups[$parent] : null; + } + } + } + } + + return $memberships; } } From e228404bf46ce202455e198eaff5dcdde830d90a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 May 2015 15:36:15 +0200 Subject: [PATCH 058/239] Adjust ConfigObjectTest as ConfigObjects are not countable anymore They are of course still indirectly countable, by using Config::count() or ConfigObject::select()::count(). --- test/php/library/Icinga/Data/ConfigObjectTest.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/test/php/library/Icinga/Data/ConfigObjectTest.php b/test/php/library/Icinga/Data/ConfigObjectTest.php index eabdf80cf..820d17b83 100644 --- a/test/php/library/Icinga/Data/ConfigObjectTest.php +++ b/test/php/library/Icinga/Data/ConfigObjectTest.php @@ -60,14 +60,6 @@ class ConfigObjectTest extends BaseTestCase ); } - public function testWhetherConfigObjectsAreCountable() - { - $config = new ConfigObject(array('a' => 'b', 'c' => array('d' => 'e'))); - - $this->assertInstanceOf('Countable', $config, 'ConfigObject objects do not implement interface `Countable\''); - $this->assertEquals(2, count($config), 'ConfigObject objects do not count properties and sections correctly'); - } - public function testWhetherConfigObjectsAreTraversable() { $config = new ConfigObject(array('a' => 'b', 'c' => 'd')); @@ -124,7 +116,7 @@ class ConfigObjectTest extends BaseTestCase } /** - * @expectedException LogicException + * @expectedException \Icinga\Exception\ProgrammingError */ public function testWhetherItIsNotPossibleToAppendProperties() { @@ -142,9 +134,6 @@ class ConfigObjectTest extends BaseTestCase $this->assertFalse(isset($config->c), 'ConfigObjects do not allow to unset sections'); } - /** - * @depends testWhetherConfigObjectsAreCountable - */ public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections() { $config = new ConfigObject(); From f1f1710f47fea6393423edb9314a141a54699859 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 08:07:42 +0200 Subject: [PATCH 059/239] Config: Add method getConfigObject to access the internal ConfigObject --- library/Icinga/Application/Config.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php index 6bad014a6..02851b975 100644 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Application/Config.php @@ -87,6 +87,16 @@ class Config implements Countable, Iterator, Selectable return $this; } + /** + * Return the internal ConfigObject + * + * @return ConfigObject + */ + public function getConfigObject() + { + return $this->config; + } + /** * Provide a query for the internal config object * From b1cbc1422bdfb0bac278374f229d181d88d17916 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 08:40:02 +0200 Subject: [PATCH 060/239] ArrayDatasource: Add support for associative arrays Keys are now preserved in case a non-numeric one is found. By using setKeyColumn() it is now also possible to map such a key to a specific column of a row generated by createResult(). --- .../Icinga/Data/DataArray/ArrayDatasource.php | 76 +++++++++++++++---- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index 72ccc4b4c..6a2ff573f 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -12,6 +12,16 @@ class ArrayDatasource implements Selectable protected $result; + /** + * The name of the column to map array keys on + * + * In case the array being used as data source provides keys of type string,this name + * will be used to set such as column on each row, if the column is not set already. + * + * @var string + */ + protected $keyColumn; + /** * Constructor, create a new Datasource for the given Array * @@ -22,6 +32,29 @@ class ArrayDatasource implements Selectable $this->data = (array) $array; } + /** + * Set the name of the column to map array keys on + * + * @param string $name + * + * @return $this + */ + public function setKeyColumn($name) + { + $this->keyColumn = $name; + return $this; + } + + /** + * Return the name of the column to map array keys on + * + * @return string + */ + public function getKeyColumn() + { + return $this->keyColumn; + } + /** * Instantiate a Query object * @@ -83,39 +116,52 @@ class ArrayDatasource implements Selectable if ($this->hasResult()) { return $this; } - $result = array(); $columns = $query->getColumns(); $filter = $query->getFilter(); - foreach ($this->data as & $row) { + $foundStringKey = false; + $result = array(); + foreach ($this->data as $key => $row) { + if (is_string($key) && $this->keyColumn !== null && !isset($row->{$this->keyColumn})) { + $row = clone $row; // Make sure that this won't affect the actual data + $row->{$this->keyColumn} = $key; + } if (! $filter->matches($row)) { continue; } // Get only desired columns if asked so - if (empty($columns)) { - $result[] = $row; - } else { - $c_row = (object) array(); - foreach ($columns as $alias => $key) { - if (is_int($alias)) { - $alias = $key; + if (! empty($columns)) { + $filteredRow = (object) array(); + foreach ($columns as $alias => $name) { + if (! is_string($alias)) { + $alias = $name; } - if (isset($row->$key)) { - $c_row->$alias = $row->$key; + + if (isset($row->$name)) { + $filteredRow->$alias = $row->$name; } else { - $c_row->$alias = null; + $filteredRow->$alias = null; } } - $result[] = $c_row; + } else { + $filteredRow = $row; } + + $foundStringKey |= is_string($key); + $result[$key] = $filteredRow; } // Sort the result - if ($query->hasOrder()) { - usort($result, array($query, 'compare')); + if ($foundStringKey) { + uasort($result, array($query, 'compare')); + } else { + usort($result, array($query, 'compare')); + } + } elseif (! $foundStringKey) { + $result = array_values($result); } $this->setResult($result); From 9c799dca2284be249779c40b59b4ef6bb2cb11a2 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 08:41:54 +0200 Subject: [PATCH 061/239] IniUserGroupBackend: Automatically set section names on column `name' refs #8826 --- .../UserGroup/IniUserGroupBackend.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index 6377bd9c0..ba92093aa 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -47,6 +47,14 @@ class IniUserGroupBackend extends Repository implements UserGroupBackendInterfac ) ); + /** + * Initialize this ini user group backend + */ + protected function init() + { + $this->ds->getConfigObject()->setKeyColumn('name'); + } + /** * Return the groups the given user is a member of * @@ -60,17 +68,15 @@ class IniUserGroupBackend extends Repository implements UserGroupBackendInterfac $groups = array(); foreach ($result as $group) { - if ($group->group_name) { // TODO: Can we set this somehow automatically to the section's name?? - $groups[$group->group_name] = $group->parent_name; - } + $groups[$group->group_name] = $group->parent_name; } $username = strtolower($user->getUsername()); $memberships = array(); foreach ($result as $group) { - if ($group->group_name && $group->users) { + if ($group->users && !in_array($group->group_name, $memberships)) { $users = array_map('strtolower', String::trimSplit($group->users)); - if (! in_array($group->group_name, $memberships) && in_array($username, $users)) { + if (in_array($username, $users)) { $memberships[] = $group->group_name; $parent = $groups[$group->group_name]; while ($parent !== null) { From d63381c0020faa2698f290de1850d84c9e9a11a3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 09:12:48 +0200 Subject: [PATCH 062/239] ArrayDatasource: Add missing and fix existing documentation --- .../Icinga/Data/DataArray/ArrayDatasource.php | 97 +++++++++++++++++-- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index 6a2ff573f..b2ecce533 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -8,8 +8,18 @@ use Icinga\Data\SimpleQuery; class ArrayDatasource implements Selectable { + /** + * The array being used as data source + * + * @var array + */ protected $data; + /** + * The current result + * + * @var array + */ protected $result; /** @@ -23,13 +33,13 @@ class ArrayDatasource implements Selectable protected $keyColumn; /** - * Constructor, create a new Datasource for the given Array + * Create a new data source for the given array * - * @param array $array The array you're going to use as a data source + * @param array $data The array you're going to use as a data source */ - public function __construct(array $array) + public function __construct(array $data) { - $this->data = (array) $array; + $this->data = $data; } /** @@ -56,15 +66,22 @@ class ArrayDatasource implements Selectable } /** - * Instantiate a Query object + * Provide a query for this data source * - * @return SimpleQuery + * @return SimpleQuery */ public function select() { return new SimpleQuery($this); } + /** + * Fetch and return a column of all rows of the result set as an array + * + * @param SimpleQuery $query + * + * @return array + */ public function fetchColumn(SimpleQuery $query) { $result = array(); @@ -75,6 +92,13 @@ class ArrayDatasource implements Selectable return $result; } + /** + * Fetch and return all rows of the given query's result as a flattened key/value based array + * + * @param SimpleQuery $query + * + * @return array + */ public function fetchPairs(SimpleQuery $query) { $result = array(); @@ -91,6 +115,13 @@ class ArrayDatasource implements Selectable return $result; } + /** + * Fetch and return the first row of the given query's result + * + * @param SimpleQuery $query + * + * @return object|false The row or false in case the result is empty + */ public function fetchRow(SimpleQuery $query) { $result = $this->getResult($query); @@ -100,17 +131,38 @@ class ArrayDatasource implements Selectable return $result[0]; } + /** + * Fetch and return all rows of the given query's result as an array + * + * @param SimpleQuery $query + * + * @return array + */ public function fetchAll(SimpleQuery $query) { return $this->getResult($query); } + /** + * Count all rows of the given query's result + * + * @param SimpleQuery $query + * + * @return int + */ public function count(SimpleQuery $query) { $this->createResult($query); return count($this->result); } + /** + * Create the result for the given query, in case there is none yet + * + * @param SimpleQuery $query + * + * @return $this + */ protected function createResult(SimpleQuery $query) { if ($this->hasResult()) { @@ -168,7 +220,14 @@ class ArrayDatasource implements Selectable return $this; } - protected function getLimitedResult($query) + /** + * Apply the limit, if any, of the given query to the current result and return the result + * + * @param SimpleQuery $query + * + * @return array + */ + protected function getLimitedResult(SimpleQuery $query) { if ($query->hasLimit()) { if ($query->hasOffset()) { @@ -182,16 +241,36 @@ class ArrayDatasource implements Selectable } } + /** + * Return whether a query result exists + * + * @return bool + */ protected function hasResult() { return $this->result !== null; } - protected function setResult($result) + /** + * Set the current result + * + * @param array $result + * + * @return $this + */ + protected function setResult(array $result) { - return $this->result = $result; + $this->result = $result; + return $this; } + /** + * Return the result for the given query + * + * @param SimpleQuery $query + * + * @return array + */ protected function getResult(SimpleQuery $query) { if (! $this->hasResult()) { From f2ad2838f431e7f3da1cdeb3998361c67eed2fbe Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 09:39:43 +0200 Subject: [PATCH 063/239] ArrayDatasource: Apply a query's limit and offset when creating the result --- .../Icinga/Data/DataArray/ArrayDatasource.php | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index b2ecce533..243fb1d7d 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -171,8 +171,11 @@ class ArrayDatasource implements Selectable $columns = $query->getColumns(); $filter = $query->getFilter(); + $offset = $query->hasOffset() ? $query->getOffset() : 0; + $limit = $query->hasLimit() ? $query->getLimit() : 0; $foundStringKey = false; $result = array(); + $skipped = 0; foreach ($this->data as $key => $row) { if (is_string($key) && $this->keyColumn !== null && !isset($row->{$this->keyColumn})) { $row = clone $row; // Make sure that this won't affect the actual data @@ -181,6 +184,9 @@ class ArrayDatasource implements Selectable if (! $filter->matches($row)) { continue; + } elseif ($skipped < $offset) { + $skipped++; + continue; } // Get only desired columns if asked so @@ -203,6 +209,10 @@ class ArrayDatasource implements Selectable $foundStringKey |= is_string($key); $result[$key] = $filteredRow; + + if (count($result) === $limit) { + break; + } } // Sort the result @@ -220,27 +230,6 @@ class ArrayDatasource implements Selectable return $this; } - /** - * Apply the limit, if any, of the given query to the current result and return the result - * - * @param SimpleQuery $query - * - * @return array - */ - protected function getLimitedResult(SimpleQuery $query) - { - if ($query->hasLimit()) { - if ($query->hasOffset()) { - $offset = $query->getOffset(); - } else { - $offset = 0; - } - return array_slice($this->result, $offset, $query->getLimit()); - } else { - return $this->result; - } - } - /** * Return whether a query result exists * @@ -276,6 +265,6 @@ class ArrayDatasource implements Selectable if (! $this->hasResult()) { $this->createResult($query); } - return $this->getLimitedResult($query); + return $this->result; } } From 7b2ed3bef7fe4eb77ad2a28af709c984abcdbec3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 10:08:07 +0200 Subject: [PATCH 064/239] ArrayDatasource: Create a new result when counting There is usually no limit and offset when a query is going to be counted so the cached result must not be used. --- .../Icinga/Data/DataArray/ArrayDatasource.php | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index 243fb1d7d..25ba47ed1 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -22,6 +22,13 @@ class ArrayDatasource implements Selectable */ protected $result; + /** + * The result of a counted query + * + * @var int + */ + protected $count; + /** * The name of the column to map array keys on * @@ -89,6 +96,7 @@ class ArrayDatasource implements Selectable $arr = (array) $row; $result[] = array_shift($arr); } + return $result; } @@ -110,8 +118,10 @@ class ArrayDatasource implements Selectable $keys[1] = $keys[0]; } } + $result[$row->{$keys[0]}] = $row->{$keys[1]}; } + return $result; } @@ -152,27 +162,27 @@ class ArrayDatasource implements Selectable */ public function count(SimpleQuery $query) { - $this->createResult($query); - return count($this->result); + if ($this->count === null) { + $this->count = count($this->createResult($query)); + } + + return $this->count; } /** - * Create the result for the given query, in case there is none yet + * Create and return the result for the given query * * @param SimpleQuery $query * - * @return $this + * @return array */ protected function createResult(SimpleQuery $query) { - if ($this->hasResult()) { - return $this; - } - $columns = $query->getColumns(); $filter = $query->getFilter(); $offset = $query->hasOffset() ? $query->getOffset() : 0; $limit = $query->hasLimit() ? $query->getLimit() : 0; + $foundStringKey = false; $result = array(); $skipped = 0; @@ -226,8 +236,7 @@ class ArrayDatasource implements Selectable $result = array_values($result); } - $this->setResult($result); - return $this; + return $result; } /** @@ -263,8 +272,9 @@ class ArrayDatasource implements Selectable protected function getResult(SimpleQuery $query) { if (! $this->hasResult()) { - $this->createResult($query); + $this->setResult($this->createResult($query)); } + return $this->result; } } From cfa91761924439cdd0a11efdc4832d9dd4930cc0 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 10:10:29 +0200 Subject: [PATCH 065/239] ArrayDatasource: use array_shift in fetchRow() instead of index access Since associative arrays are supported, the numeric index 0 might not be the first entry in the result. --- library/Icinga/Data/DataArray/ArrayDatasource.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index 25ba47ed1..11f8e59e8 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -138,7 +138,8 @@ class ArrayDatasource implements Selectable if (empty($result)) { return false; } - return $result[0]; + + return array_shift($result); } /** From 108f55128f5f01c90ad783f428655d58face896c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 10:24:50 +0200 Subject: [PATCH 066/239] Ldap\Query: Fix access of a filter's expression --- library/Icinga/Protocol/Ldap/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Protocol/Ldap/Query.php b/library/Icinga/Protocol/Ldap/Query.php index d5bf1f080..b54f8b15e 100644 --- a/library/Icinga/Protocol/Ldap/Query.php +++ b/library/Icinga/Protocol/Ldap/Query.php @@ -131,7 +131,7 @@ class Query extends SimpleQuery // TODO: This should be considered a quick fix only. // Drop this entirely once support for Icinga\Data\Filter is available if ($filter->isExpression()) { - $this->where($filter->getColumn(), $filter->getValue()); + $this->where($filter->getColumn(), $filter->getExpression()); } elseif ($filter->isChain()) { foreach ($filter->filters() as $chainOrExpression) { $this->addFilter($chainOrExpression); From 4044e56a03e8364d31b2e472b97f2680419fc516 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 10:27:26 +0200 Subject: [PATCH 067/239] LdapUserBackend: Provide filter column `user' refs #8826 --- library/Icinga/Authentication/User/LdapUserBackend.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index e7fa6767b..051b5672f 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -42,6 +42,13 @@ class LdapUserBackend extends Repository implements UserBackendInterface */ protected $filter; + /** + * The columns which are not permitted to be queried + * + * @var array + */ + protected $filterColumns = array('user'); + /** * The default sort rules to be applied on a query * @@ -250,6 +257,7 @@ class LdapUserBackend extends Repository implements UserBackendInterface } $this->queryColumns[$this->userClass] = array( + 'user' => $this->userNameAttribute, 'user_name' => $this->userNameAttribute, 'is_active' => 'unknown', // msExchUserAccountControl == 2/512/514? <- AD LDAP 'created_at' => 'whenCreated', // That's AD LDAP, From 16a7b010bcf34988f61ae85bf39a5e0a0b2100c8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 10:41:39 +0200 Subject: [PATCH 068/239] SimpleQuery: Ignore limit and offset when counting --- library/Icinga/Data/SimpleQuery.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index 3477c2331..875f5222a 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -426,13 +426,15 @@ class SimpleQuery implements QueryInterface } /** - * Count all rows of the result set + * Count all rows of the result set, ignoring limit and offset * - * @return int + * @return int */ public function count() { - return $this->ds->count($this); + $query = clone $this; + $query->limit(0, 0); + return $this->ds->count($query); } /** From 4d83b2f93d8e59b4a20643f6f0e9b1a8277d60ea Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 6 May 2015 12:18:57 +0200 Subject: [PATCH 069/239] Authentication\Manager: Fix invalid class path in use statement refs #8826 --- library/Icinga/Authentication/Manager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index 9d86b6ad6..8a150c8b0 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -4,7 +4,7 @@ namespace Icinga\Authentication; use Exception; -use Icinga\Authentication\Usergroup\UserGroupBackend; +use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Exception\NotReadableError; From ba4330de43f8614564614719842a4d418495cc24 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 7 May 2015 08:03:07 +0200 Subject: [PATCH 070/239] Repository: We do not overwrite properties, we're initializing them refs #8826 --- library/Icinga/Repository/Repository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index e1b39e1c8..dde702bc8 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -45,7 +45,7 @@ abstract class Repository implements Selectable /** * The query columns being provided * - * This must be overwritten by concrete repository implementations, in the following format + * This must be initialized by concrete repository implementations, in the following format *
      
            *  array(
            *      'baseTable' => array(
      @@ -70,7 +70,7 @@ abstract class Repository implements Selectable
           /**
            * The default sort rules to be applied on a query
            *
      -     * This may be overwritten by concrete repository implementations, in the following format
      +     * This may be initialized by concrete repository implementations, in the following format
            * 
      
            *  array(
            *      'alias_or_column_name' => array(
      
      From 99be3587148f1c764e57c2381d315844b11fe6a0 Mon Sep 17 00:00:00 2001
      From: Johannes Meyer 
      Date: Thu, 7 May 2015 08:28:32 +0200
      Subject: [PATCH 071/239] Repository: Make it possible to initialize column
       properties lazily
      
      refs #8826
      ---
       .../Authentication/User/LdapUserBackend.php   | 24 ++---
       library/Icinga/Repository/Repository.php      | 87 ++++++++++++++-----
       2 files changed, 77 insertions(+), 34 deletions(-)
      
      diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php
      index 051b5672f..7b9ffa253 100644
      --- a/library/Icinga/Authentication/User/LdapUserBackend.php
      +++ b/library/Icinga/Authentication/User/LdapUserBackend.php
      @@ -230,8 +230,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
            */
           public function select(array $columns = null)
           {
      -        $this->initializeQueryColumns();
      -
               $query = parent::select($columns);
               $query->getQuery()->setBase($this->baseDn);
               if ($this->filter) {
      @@ -244,26 +242,28 @@ class LdapUserBackend extends Repository implements UserBackendInterface
           /**
            * Initialize this repository's query columns
            *
      +     * @return  array
      +     *
            * @throws  ProgrammingError    In case either $this->userNameAttribute or $this->userClass has not been set yet
            */
           protected function initializeQueryColumns()
           {
      -        if ($this->queryColumns === null) {
      -            if ($this->userClass === null) {
      -                throw new ProgrammingError('It is required to set the objectClass where to look for users first');
      -            }
      -            if ($this->userNameAttribute === null) {
      -                throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
      -            }
      +        if ($this->userClass === null) {
      +            throw new ProgrammingError('It is required to set the objectClass where to look for users first');
      +        }
      +        if ($this->userNameAttribute === null) {
      +            throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
      +        }
       
      -            $this->queryColumns[$this->userClass] = array(
      +        return array(
      +            $this->userClass => array(
                       'user'          => $this->userNameAttribute,
                       'user_name'     => $this->userNameAttribute,
                       'is_active'     => 'unknown', // msExchUserAccountControl == 2/512/514? <- AD LDAP
                       'created_at'    => 'whenCreated', // That's AD LDAP,
                       'last_modified' => 'whenChanged' // what's OpenLDAP?
      -            );
      -        }
      +            )
      +        );
           }
       
           /**
      diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
      index dde702bc8..4f578f8fd 100644
      --- a/library/Icinga/Repository/Repository.php
      +++ b/library/Icinga/Repository/Repository.php
      @@ -121,14 +121,6 @@ abstract class Repository implements Selectable
               $this->aliasColumnMap = array();
       
               $this->init();
      -
      -        if ($this->filterColumns === null) {
      -            $this->filterColumns = $this->getFilterColumns();
      -        }
      -
      -        if ($this->sortRules === null) {
      -            $this->sortRules = $this->getSortRules();
      -        }
           }
       
           /**
      @@ -187,7 +179,7 @@ abstract class Repository implements Selectable
           public function getBaseTable()
           {
               if ($this->baseTable === null) {
      -            $queryColumns = $this->queryColumns; // Copy because of reset()
      +            $queryColumns = $this->getQueryColumns();
                   reset($queryColumns);
                   $this->baseTable = key($queryColumns);
                   if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
      @@ -198,31 +190,81 @@ abstract class Repository implements Selectable
               return $this->baseTable;
           }
       
      +    /**
      +     * Return the query columns being provided
      +     *
      +     * Calls $this->initializeQueryColumns() in case $this->queryColumns is null.
      +     *
      +     * @return  array
      +     */
      +    public function getQueryColumns()
      +    {
      +        if ($this->queryColumns === null) {
      +            $this->queryColumns = $this->initializeQueryColumns();
      +        }
      +
      +        return $this->queryColumns;
      +    }
      +
      +    /**
      +     * Overwrite this in your repository implementation in case you need to initialize the query columns lazily
      +     *
      +     * @return  array
      +     */
      +    protected function initializeQueryColumns()
      +    {
      +        return array();
      +    }
      +
           /**
            * Return the columns (or aliases) which are not permitted to be queried
            *
      +     * Calls $this->initializeFilterColumns() in case $this->filterColumns is null.
      +     *
            * @return  array
            */
           public function getFilterColumns()
           {
      -        if ($this->filterColumns !== null) {
      -            return $this->filterColumns;
      +        if ($this->filterColumns === null) {
      +            $this->filterColumns = $this->initializeFilterColumns();
               }
       
      +        return $this->filterColumns;
      +    }
      +
      +    /**
      +     * Overwrite this in your repository implementation in case you need to initialize the filter columns lazily
      +     *
      +     * @return  array
      +     */
      +    protected function initializeFilterColumns()
      +    {
               return array();
           }
       
           /**
            * Return the default sort rules to be applied on a query
            *
      +     * Calls $this->initializeSortRules() in case $this->sortRules is null.
      +     *
            * @return  array
            */
           public function getSortRules()
           {
      -        if ($this->sortRules !== null) {
      -            return $this->sortRules;
      +        if ($this->sortRules === null) {
      +            $this->sortRules = $this->initializeSortRules();
               }
       
      +        return $this->sortRules;
      +    }
      +
      +    /**
      +     * Overwrite this in your repository implementation in case you need to initialize the sort rules lazily
      +     *
      +     * @return  array
      +     */
      +    protected function initializeSortRules()
      +    {
               return array();
           }
       
      @@ -232,15 +274,9 @@ abstract class Repository implements Selectable
            * @param   array   $columns    The desired columns, if null all columns will be queried
            *
            * @return  RepositoryQuery
      -     *
      -     * @throws  ProgrammingError    In case $this->queryColumns has not been initialized yet
            */
           public function select(array $columns = null)
           {
      -        if (empty($this->queryColumns)) {
      -            throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
      -        }
      -
               $this->initializeAliasMaps();
       
               $query = new RepositoryQuery($this);
      @@ -250,6 +286,8 @@ abstract class Repository implements Selectable
       
           /**
            * Initialize $this->aliasTableMap and $this->aliasColumnMap
      +     *
      +     * @throws  ProgrammingError    In case $this->queryColumns does not provide any column information
            */
           protected function initializeAliasMaps()
           {
      @@ -257,7 +295,12 @@ abstract class Repository implements Selectable
                   return;
               }
       
      -        foreach ($this->queryColumns as $table => $columns) {
      +        $queryColumns = $this->getQueryColumns();
      +        if (empty($queryColumns)) {
      +            throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
      +        }
      +
      +        foreach ($queryColumns as $table => $columns) {
                   foreach ($columns as $alias => $column) {
                       if (! is_string($alias)) {
                           $this->aliasTableMap[$column] = $table;
      @@ -297,7 +340,7 @@ abstract class Repository implements Selectable
            */
           public function hasQueryColumn($name)
           {
      -        return array_key_exists($name, $this->aliasColumnMap) && !in_array($name, $this->filterColumns);
      +        return array_key_exists($name, $this->aliasColumnMap) && !in_array($name, $this->getFilterColumns());
           }
       
           /**
      @@ -311,7 +354,7 @@ abstract class Repository implements Selectable
            */
           public function requireQueryColumn($name)
           {
      -        if (in_array($name, $this->filterColumns)) {
      +        if (in_array($name, $this->getFilterColumns())) {
                   throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
               }
               if (! array_key_exists($name, $this->aliasColumnMap)) {
      
      From eac5e398beb2d34867b8c29cb435aebc54ec3a5a Mon Sep 17 00:00:00 2001
      From: Johannes Meyer 
      Date: Thu, 7 May 2015 09:04:50 +0200
      Subject: [PATCH 072/239] Repository: Initialize the internal column and table
       maps lazily as well
      
      refs #8826
      ---
       library/Icinga/Repository/Repository.php | 65 ++++++++++++++++--------
       1 file changed, 45 insertions(+), 20 deletions(-)
      
      diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
      index 4f578f8fd..701b0ff55 100644
      --- a/library/Icinga/Repository/Repository.php
      +++ b/library/Icinga/Repository/Repository.php
      @@ -269,19 +269,31 @@ abstract class Repository implements Selectable
           }
       
           /**
      -     * Return a new query for the given columns
      +     * Return an array to map table names to aliases
            *
      -     * @param   array   $columns    The desired columns, if null all columns will be queried
      -     *
      -     * @return  RepositoryQuery
      +     * @return  array
            */
      -    public function select(array $columns = null)
      +    protected function getAliasTableMap()
           {
      -        $this->initializeAliasMaps();
      +        if (empty($this->aliasTableMap)) {
      +            $this->initializeAliasMaps();
      +        }
       
      -        $query = new RepositoryQuery($this);
      -        $query->from($this->getBaseTable(), $columns);
      -        return $query;
      +        return $this->aliasTableMap;
      +    }
      +
      +    /**
      +     * Return a flattened array to map query columns to aliases
      +     *
      +     * @return  array
      +     */
      +    protected function getAliasColumnMap()
      +    {
      +        if (empty($this->aliasColumnMap)) {
      +            $this->initializeAliasMaps();
      +        }
      +
      +        return $this->aliasColumnMap;
           }
       
           /**
      @@ -291,10 +303,6 @@ abstract class Repository implements Selectable
            */
           protected function initializeAliasMaps()
           {
      -        if (! empty($this->aliasColumnMap)) {
      -            return;
      -        }
      -
               $queryColumns = $this->getQueryColumns();
               if (empty($queryColumns)) {
                   throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
      @@ -313,6 +321,20 @@ abstract class Repository implements Selectable
               }
           }
       
      +    /**
      +     * Return a new query for the given columns
      +     *
      +     * @param   array   $columns    The desired columns, if null all columns will be queried
      +     *
      +     * @return  RepositoryQuery
      +     */
      +    public function select(array $columns = null)
      +    {
      +        $query = new RepositoryQuery($this);
      +        $query->from($this->getBaseTable(), $columns);
      +        return $query;
      +    }
      +
           /**
            * Return this repository's query columns mapped to their respective aliases
            *
      @@ -321,7 +343,7 @@ abstract class Repository implements Selectable
           public function requireAllQueryColumns()
           {
               $map = array();
      -        foreach ($this->aliasColumnMap as $alias => $_) {
      +        foreach ($this->getAliasColumnMap() as $alias => $_) {
                   if ($this->hasQueryColumn($alias)) {
                       // Just in case $this->requireQueryColumn has been overwritten and there is some magic going on
                       $map[$alias] = $this->requireQueryColumn($alias);
      @@ -340,7 +362,7 @@ abstract class Repository implements Selectable
            */
           public function hasQueryColumn($name)
           {
      -        return array_key_exists($name, $this->aliasColumnMap) && !in_array($name, $this->getFilterColumns());
      +        return array_key_exists($name, $this->getAliasColumnMap()) && !in_array($name, $this->getFilterColumns());
           }
       
           /**
      @@ -357,11 +379,13 @@ abstract class Repository implements Selectable
               if (in_array($name, $this->getFilterColumns())) {
                   throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
               }
      -        if (! array_key_exists($name, $this->aliasColumnMap)) {
      +
      +        $aliasColumnMap = $this->getAliasColumnMap();
      +        if (! array_key_exists($name, $aliasColumnMap)) {
                   throw new QueryException(t('Query column "%s" not found'), $name);
               }
       
      -        return $this->aliasColumnMap[$name];
      +        return $aliasColumnMap[$name];
           }
       
           /**
      @@ -373,7 +397,7 @@ abstract class Repository implements Selectable
            */
           public function hasFilterColumn($name)
           {
      -        return array_key_exists($name, $this->aliasColumnMap);
      +        return array_key_exists($name, $this->getAliasColumnMap());
           }
       
           /**
      @@ -387,10 +411,11 @@ abstract class Repository implements Selectable
            */
           public function requireFilterColumn($name)
           {
      -        if (! array_key_exists($name, $this->aliasColumnMap)) {
      +        $aliasColumnMap = $this->getAliasColumnMap();
      +        if (! array_key_exists($name, $aliasColumnMap)) {
                   throw new QueryException(t('Filter column "%s" not found'), $name);
               }
       
      -        return $this->aliasColumnMap[$name];
      +        return $aliasColumnMap[$name];
           }
       }
      
      From f83d16acb2db348095598f893f21954dc088f915 Mon Sep 17 00:00:00 2001
      From: Johannes Meyer 
      Date: Thu, 7 May 2015 14:45:47 +0200
      Subject: [PATCH 073/239] RepositoryQuery: Do not lose the repository context
       during pagination
      
      refs #8826
      ---
       library/Icinga/Repository/RepositoryQuery.php |  4 +-
       .../Web/Paginator/Adapter/QueryAdapter.php    | 61 +++++++++++++------
       2 files changed, 44 insertions(+), 21 deletions(-)
      
      diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
      index e9acb945b..3cbde0228 100644
      --- a/library/Icinga/Repository/RepositoryQuery.php
      +++ b/library/Icinga/Repository/RepositoryQuery.php
      @@ -376,7 +376,9 @@ class RepositoryQuery implements QueryInterface
            */
           public function paginate($itemsPerPage = null, $pageNumber = null)
           {
      -        return $this->query->paginate($itemsPerPage, $pageNumber);
      +        $paginator = $this->query->paginate($itemsPerPage, $pageNumber);
      +        $paginator->getAdapter()->setQuery($this);
      +        return $paginator;
           }
       
           /**
      diff --git a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
      index 5d85a4428..3f2b71cc3 100644
      --- a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
      +++ b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
      @@ -4,44 +4,64 @@
       namespace Icinga\Web\Paginator\Adapter;
       
       use Zend_Paginator_Adapter_Interface;
      -
      -/**
      - * @see Zend_Paginator_Adapter_Interface
      - */
      +use Icinga\Data\QueryInterface;
       
       class QueryAdapter implements Zend_Paginator_Adapter_Interface
       {
           /**
      -     * Array
      +     * The query being paginated
            *
      -     * @var array
      +     * @var QueryInterface
            */
      -    protected $query = null;
      +    protected $query;
       
           /**
            * Item count
            *
      -     * @var integer
      +     * @var int
            */
      -    protected $count = null;
      +    protected $count;
       
           /**
      -     * Constructor.
      +     * Create a new QueryAdapter
            *
      -     * @param array $query Query to paginate
      +     * @param   QueryInterface  $query      The query to paginate
            */
      -    // TODO: This might be ready for (QueryInterface $query)
      -    public function __construct($query)
      +    public function __construct(QueryInterface $query)
           {
      -        $this->query = $query;
      +        $this->setQuery($query);
           }
       
           /**
      -     * Returns an array of items for a page.
      +     * Set the query to paginate
            *
      -     * @param  integer $offset Page offset
      -     * @param  integer $itemCountPerPage Number of items per page
      -     * @return array
      +     * @param   QueryInterface  $query
      +     *
      +     * @return  $this
      +     */
      +    public function setQuery(QueryInterface $query)
      +    {
      +        $this->query = $query;
      +        return $this;
      +    }
      +
      +    /**
      +     * Return the query being paginated
      +     *
      +     * @return  QueryInterface
      +     */
      +    public function getQuery()
      +    {
      +        return $this->query;
      +    }
      +
      +    /**
      +     * Fetch and return the rows in the given range of the query result
      +     *
      +     * @param   int     $offset             Page offset
      +     * @param   int     $itemCountPerPage   Number of items per page
      +     *
      +     * @return  array
            */
           public function getItems($offset, $itemCountPerPage)
           {
      @@ -49,15 +69,16 @@ class QueryAdapter implements Zend_Paginator_Adapter_Interface
           }
       
           /**
      -     * Returns the total number of items in the query result.
      +     * Return the total number of items in the query result
            *
      -     * @return integer
      +     * @return  int
            */
           public function count()
           {
               if ($this->count === null) {
                   $this->count = $this->query->count();
               }
      +
               return $this->count;
           }
       }
      
      From f383ddd00a25850eab4e5032f106c81862efaa43 Mon Sep 17 00:00:00 2001
      From: Johannes Meyer 
      Date: Thu, 7 May 2015 14:49:13 +0200
      Subject: [PATCH 074/239] Repository: Add support for client side value
       conversion
      
      refs #8826
      ---
       library/Icinga/Repository/Repository.php      | 136 ++++++++++++++++++
       library/Icinga/Repository/RepositoryQuery.php |  82 +++++++++--
       2 files changed, 209 insertions(+), 9 deletions(-)
      
      diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
      index 701b0ff55..c4ff95ce2 100644
      --- a/library/Icinga/Repository/Repository.php
      +++ b/library/Icinga/Repository/Repository.php
      @@ -3,6 +3,7 @@
       
       namespace Icinga\Repository;
       
      +use Icinga\Application\Logger;
       use Icinga\Data\Selectable;
       use Icinga\Exception\ProgrammingError;
       use Icinga\Exception\QueryException;
      @@ -95,6 +96,21 @@ abstract class Repository implements Selectable
            */
           protected $sortRules;
       
      +    /**
      +     * The value conversion rules to apply on a query
      +     *
      +     * This may be initialized by concrete repository implementations and describes for which aliases or column
      +     * names what type of conversion is available. For entries, where the key is the alias/column and the value
      +     * is the type identifier, the repository attempts to find a conversion method for the alias/column first and,
      +     * if none is found, then for the type. If an entry only provides a value, which is the alias/column, the
      +     * repository only attempts to find a conversion method for the alias/column. The name of a conversion method
      +     * is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and
      +     * groupname will be translated to retrieveGroupname)
      +     *
      +     * @var array
      +     */
      +    protected $conversionRules;
      +
           /**
            * An array to map table names to aliases
            *
      @@ -268,6 +284,32 @@ abstract class Repository implements Selectable
               return array();
           }
       
      +    /**
      +     * Return the value conversion rules to apply on a query
      +     *
      +     * Calls $this->initializeConversionRules() in case $this->conversionRules is null.
      +     *
      +     * @return  array
      +     */
      +    public function getConversionRules()
      +    {
      +        if ($this->conversionRules === null) {
      +            $this->conversionRules = $this->initializeConversionRules();
      +        }
      +
      +        return $this->conversionRules;
      +    }
      +
      +    /**
      +     * Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily
      +     *
      +     * @return  array
      +     */
      +    protected function initializeConversionRules()
      +    {
      +        return array();
      +    }
      +
           /**
            * Return an array to map table names to aliases
            *
      @@ -335,6 +377,100 @@ abstract class Repository implements Selectable
               return $query;
           }
       
      +    /**
      +     * Return whether this repository is capable of converting values
      +     *
      +     * @return  bool
      +     */
      +    public function providesValueConversion()
      +    {
      +        $conversionRules = $this->getConversionRules();
      +        return !empty($conversionRules);
      +    }
      +
      +    /**
      +     * Convert a value supposed to be transmitted to the data source
      +     *
      +     * @param   string  $name       The alias or column name
      +     * @param   mixed   $value      The value to convert
      +     *
      +     * @return  mixed               If conversion was possible, the converted value, otherwise the unchanged value
      +     */
      +    public function persistColumn($name, $value)
      +    {
      +        $converter = $this->getConverter($name, 'persist');
      +        if ($converter !== null) {
      +            $value = $this->$converter($value);
      +        }
      +
      +        return $value;
      +    }
      +
      +    /**
      +     * Convert a value which was fetched from the data source
      +     *
      +     * @param   string  $name       The alias or column name
      +     * @param   mixed   $value      The value to convert
      +     *
      +     * @return  mixed               If conversion was possible, the converted value, otherwise the unchanged value
      +     */
      +    public function retrieveColumn($name, $value)
      +    {
      +        $converter = $this->getConverter($name, 'retrieve');
      +        if ($converter !== null) {
      +            $value = $this->$converter($value);
      +        }
      +
      +        return $value;
      +    }
      +
      +    /**
      +     * Return the name of the conversion method for the given alias or column name and context
      +     *
      +     * @param   string  $name       The alias or column name for which to return a conversion method
      +     * @param   string  $context    The context of the conversion: persist or retrieve
      +     *
      +     * @return  string
      +     *
      +     * @throws  ProgrammingError    In case a conversion rule is found but not any conversion method
      +     */
      +    protected function getConverter($name, $context)
      +    {
      +        $conversionRules = $this->getConversionRules();
      +
      +        // Check for a conversion method for the alias/column first
      +        if (array_key_exists($name, $conversionRules) || in_array($name, $conversionRules)) {
      +            $methodName = $context . join('', array_map('ucfirst', explode('_', $name)));
      +            if (method_exists($this, $methodName)) {
      +                return $methodName;
      +            }
      +        }
      +
      +        // The conversion method for the type is just a fallback, but it is required to exist if defined
      +        if (isset($conversionRules[$name])) {
      +            $identifier = join('', array_map('ucfirst', explode('_', $conversionRules[$name])));
      +            if (! method_exists($this, $context . $identifier)) {
      +                // Do not throw an error in case at least one conversion method exists
      +                if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) {
      +                    throw new ProgrammingError(
      +                        'Cannot find any conversion method for type "%s"'
      +                        . '. Add a proper conversion method or remove the type definition',
      +                        $conversionRules[$name]
      +                    );
      +                }
      +
      +                Logger::debug(
      +                    'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
      +                    $context . $identifier,
      +                    $conversionRules[$name],
      +                    $this->getName()
      +                );
      +            } else {
      +                return $context . $identifier;
      +            }
      +        }
      +    }
      +
           /**
            * Return this repository's query columns mapped to their respective aliases
            *
      diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
      index 3cbde0228..8c47bf6ff 100644
      --- a/library/Icinga/Repository/RepositoryQuery.php
      +++ b/library/Icinga/Repository/RepositoryQuery.php
      @@ -132,7 +132,10 @@ class RepositoryQuery implements QueryInterface
            */
           public function where($column, $value = null)
           {
      -        $this->query->where($this->repository->requireFilterColumn($column), $value);
      +        $this->query->where(
      +            $this->repository->requireFilterColumn($column),
      +            $this->repository->persistColumn($column, $value)
      +        );
               return $this;
           }
       
      @@ -182,13 +185,15 @@ class RepositoryQuery implements QueryInterface
               return $this;
           }
       
      -    /*+
      +    /**
            * Recurse the given filter and notify the repository about each required filter column
            */
           protected function requireFilterColumns(Filter $filter)
           {
               if ($filter->isExpression()) {
      -            $filter->setColumn($this->repository->requireFilterColumn($filter->getColumn()));
      +            $column = $filter->getColumn();
      +            $filter->setColumn($this->repository->requireFilterColumn($column));
      +            $filter->setExpression($this->repository->persistColumn($column, $filter->getExpression()));
               } elseif ($filter->isChain()) {
                   foreach ($filter->filters() as $chainOrExpression) {
                       $this->requireFilterColumns($chainOrExpression);
      @@ -392,7 +397,14 @@ class RepositoryQuery implements QueryInterface
                   $this->order();
               }
       
      -        return $this->query->fetchOne();
      +        $result = $this->query->fetchOne();
      +        if ($this->repository->providesValueConversion()) {
      +            $columns = $this->getColumns();
      +            $column = isset($columns[0]) ? $columns[0] : key($columns);
      +            return $this->repository->retrieveColumn($column, $result);
      +        }
      +
      +        return $result;
           }
       
           /**
      @@ -406,7 +418,18 @@ class RepositoryQuery implements QueryInterface
                   $this->order();
               }
       
      -        return $this->query->fetchRow();
      +        $result = $this->query->fetchRow();
      +        if ($this->repository->providesValueConversion()) {
      +            foreach ($this->getColumns() as $alias => $column) {
      +                if (! is_string($alias)) {
      +                    $alias = $column;
      +                }
      +
      +                $result->$alias = $this->repository->retrieveColumn($alias, $result->$alias);
      +            }
      +        }
      +
      +        return $result;
           }
       
           /**
      @@ -422,11 +445,23 @@ class RepositoryQuery implements QueryInterface
                   $this->order();
               }
       
      -        return $this->query->fetchColumn($columnIndex);
      +        $results = $this->query->fetchColumn($columnIndex);
      +        if ($this->repository->providesValueConversion()) {
      +            $columns = $this->getColumns();
      +            $aliases = array_keys($columns);
      +            $column = is_int($aliases[$columnIndex]) ? $columns[$columnIndex] : $aliases[$columnIndex];
      +            foreach ($results as & $value) {
      +                $value = $this->repository->retrieveColumn($column, $value);
      +            }
      +        }
      +
      +        return $results;
           }
       
           /**
      -     * Fetch and return all rows of this query's result as a flattened key/value based array
      +     * Fetch and return all rows of this query's result set as an array of key-value pairs
      +     *
      +     * The first column is the key, the second column is the value.
            *
            * @return  array
            */
      @@ -436,7 +471,22 @@ class RepositoryQuery implements QueryInterface
                   $this->order();
               }
       
      -        return $this->query->fetchPairs();
      +        $results = $this->query->fetchPairs();
      +        if ($this->repository->providesValueConversion()) {
      +            $columns = $this->getColumns();
      +            $aliases = array_keys($columns);
      +            $newResults = array();
      +            foreach ($results as $colOneValue => $colTwoValue) {
      +                $colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0];
      +                $colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]);
      +                $colOneValue = $this->repository->retrieveColumn($colOne, $colOneValue);
      +                $newResults[$colOneValue] = $this->repository->retrieveColumn($colTwo, $colTwoValue);
      +            }
      +
      +            $results = $newResults;
      +        }
      +
      +        return $results;
           }
       
           /**
      @@ -450,7 +500,21 @@ class RepositoryQuery implements QueryInterface
                   $this->order();
               }
       
      -        return $this->query->fetchAll();
      +        $results = $this->query->fetchAll();
      +        if ($this->repository->providesValueConversion()) {
      +            $columns = $this->getColumns();
      +            foreach ($results as $row) {
      +                foreach ($columns as $alias => $column) {
      +                    if (! is_string($alias)) {
      +                        $alias = $column;
      +                    }
      +
      +                    $row->$alias = $this->repository->retrieveColumn($alias, $row->$alias);
      +                }
      +            }
      +        }
      +
      +        return $results;
           }
       
           /**
      
      From 1e1b9540f02f6a6f859ca1b7766c86436b70a012 Mon Sep 17 00:00:00 2001
      From: Johannes Meyer 
      Date: Fri, 8 May 2015 09:54:45 +0200
      Subject: [PATCH 075/239] UserController: Add backend selection control
      
      refs #8826
      ---
       application/controllers/UserController.php | 67 +++++++++++++++++-----
       application/views/scripts/user/list.phtml  |  5 +-
       public/css/icinga/main-content.less        | 18 ++++++
       3 files changed, 75 insertions(+), 15 deletions(-)
      
      diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
      index ccb5aac03..0f3a073a3 100644
      --- a/application/controllers/UserController.php
      +++ b/application/controllers/UserController.php
      @@ -5,8 +5,8 @@ use \Zend_Controller_Action_Exception;
       use Icinga\Application\Config;
       use Icinga\Authentication\User\UserBackend;
       use Icinga\Authentication\User\UserBackendInterface;
      -use Icinga\Data\Selectable;
       use Icinga\Web\Controller;
      +use Icinga\Web\Form;
       use Icinga\Web\Widget;
       
       class UserController extends Controller
      @@ -32,6 +32,26 @@ class UserController extends Controller
            */
           public function listAction()
           {
      +        $backendNames = array_map(
      +            function ($b) { return $b->getName(); },
      +            $this->loadUserBackends('Icinga\Data\Selectable')
      +        );
      +        $this->view->backendSelection = new Form();
      +        $this->view->backendSelection->setAttrib('class', 'backend-selection');
      +        $this->view->backendSelection->setUidDisabled();
      +        $this->view->backendSelection->setMethod('GET');
      +        $this->view->backendSelection->setTokenDisabled();
      +        $this->view->backendSelection->addElement(
      +            'select',
      +            'backend',
      +            array(
      +                'autosubmit'    => true,
      +                'label'         => $this->translate('Authentication Backend'),
      +                'multiOptions'  => array_combine($backendNames, $backendNames),
      +                'value'         => $this->params->get('backend')
      +            )
      +        );
      +
               $backend = $this->getUserBackend($this->params->get('backend'));
               if ($backend === null) {
                   $this->view->backend = null;
      @@ -67,20 +87,40 @@ class UserController extends Controller
               ));
           }
       
      +    /**
      +     * Return all user backends implementing the given interface
      +     *
      +     * @param   string  $interface      The class path of the interface, or null if no interface check should be made
      +     *
      +     * @return  array
      +     */
      +    protected function loadUserBackends($interface = null)
      +    {
      +        $backends = array();
      +        foreach (Config::app('authentication') as $backendName => $backendConfig) {
      +            $candidate = UserBackend::create($backendName, $backendConfig);
      +            if (! $interface || $candidate instanceof $interface) {
      +                $backends[] = $candidate;
      +            }
      +        }
      +
      +        return $backends;
      +    }
      +
           /**
            * Return the given user backend or the first match in order
            *
            * @param   string  $name           The name of the backend, or null in case the first match should be returned
      -     * @param   bool    $selectable     Whether the backend should implement the Selectable interface
      +     * @param   string  $interface      The interface the backend should implement, no interface check if null
            *
            * @return  UserBackendInterface
            *
            * @throws  Zend_Controller_Action_Exception    In case the given backend name is invalid
            */
      -    protected function getUserBackend($name = null, $selectable = true)
      +    protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable')
           {
      -        $config = Config::app('authentication');
               if ($name !== null) {
      +            $config = Config::app('authentication');
                   if (! $config->hasSection($name)) {
                       throw new Zend_Controller_Action_Exception(
                           sprintf($this->translate('Authentication backend "%s" not found'), $name),
      @@ -88,22 +128,21 @@ class UserController extends Controller
                       );
                   } else {
                       $backend = UserBackend::create($name, $config->getSection($name));
      -                if ($selectable && !$backend instanceof Selectable) {
      +                if ($interface && !$backend instanceof $interface) {
      +                    $interfaceParts = explode('\\', strtolower($interface));
                           throw new Zend_Controller_Action_Exception(
      -                        sprintf($this->translate('Authentication backend "%s" is not able to list users'), $name),
      +                        sprintf(
      +                            $this->translate('Authentication backend "%s" is not %s'),
      +                            $name,
      +                            array_pop($interfaceParts)
      +                        ),
                               400
                           );
                       }
                   }
               } else {
      -            $backend = null;
      -            foreach ($config as $backendName => $backendConfig) {
      -                $candidate = UserBackend::create($backendName, $backendConfig);
      -                if (! $selectable || $candidate instanceof Selectable) {
      -                    $backend = $candidate;
      -                    break;
      -                }
      -            }
      +            $backends = $this->loadUserBackends($interface);
      +            $backend = array_shift($backends);
               }
       
               return $backend;
      diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml
      index b6ff7e0ef..a914312c0 100644
      --- a/application/views/scripts/user/list.phtml
      +++ b/application/views/scripts/user/list.phtml
      @@ -4,7 +4,10 @@
         sortBox; ?>
         limiter; ?>
         paginator; ?>
      -  filterEditor; ?>
      +  
      + backendSelection; ?> + filterEditor; ?> +
      diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 830ecdd44..a3e8cb69c 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -241,4 +241,22 @@ table.group-list { td.group-parent, td.group-created, td.group-modified { text-align: right; } +} + +form.backend-selection { + float: right; + + div.element { + margin: 0; + + label { + width: auto; + margin-right: 0.5em; + } + + select { + width: 11.5em; + margin-left: 0; + } + } } \ No newline at end of file From 5ace0a08f3e23ce31d5f6d933b8c79c14c0ea66b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 May 2015 09:56:07 +0200 Subject: [PATCH 076/239] GroupController: Add backend selection control refs #8826 --- application/controllers/GroupController.php | 67 ++++++++++++++++----- application/views/scripts/group/list.phtml | 5 +- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index ffa32584a..d7a208826 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -5,8 +5,8 @@ use \Zend_Controller_Action_Exception; use Icinga\Application\Config; use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Authentication\UserGroup\UserGroupBackendInterface; -use Icinga\Data\Selectable; use Icinga\Web\Controller; +use Icinga\Web\Form; use Icinga\Web\Widget; class GroupController extends Controller @@ -32,6 +32,26 @@ class GroupController extends Controller */ public function listAction() { + $backendNames = array_map( + function ($b) { return $b->getName(); }, + $this->loadUserGroupBackends('Icinga\Data\Selectable') + ); + $this->view->backendSelection = new Form(); + $this->view->backendSelection->setAttrib('class', 'backend-selection'); + $this->view->backendSelection->setUidDisabled(); + $this->view->backendSelection->setMethod('GET'); + $this->view->backendSelection->setTokenDisabled(); + $this->view->backendSelection->addElement( + 'select', + 'backend', + array( + 'autosubmit' => true, + 'label' => $this->translate('Usergroup Backend'), + 'multiOptions' => array_combine($backendNames, $backendNames), + 'value' => $this->params->get('backend') + ) + ); + $backend = $this->getUserGroupBackend($this->params->get('backend')); if ($backend === null) { $this->view->backend = null; @@ -67,20 +87,40 @@ class GroupController extends Controller )); } + /** + * Return all user group backends implementing the given interface + * + * @param string $interface The class path of the interface, or null if no interface check should be made + * + * @return array + */ + protected function loadUserGroupBackends($interface = null) + { + $backends = array(); + foreach (Config::app('groups') as $backendName => $backendConfig) { + $candidate = UserGroupBackend::create($backendName, $backendConfig); + if (! $interface || $candidate instanceof $interface) { + $backends[] = $candidate; + } + } + + return $backends; + } + /** * Return the given user group backend or the first match in order * * @param string $name The name of the backend, or null in case the first match should be returned - * @param bool $selectable Whether the backend should implement the Selectable interface + * @param string $interface The interface the backend should implement, no interface check if null * * @return UserGroupBackendInterface * * @throws Zend_Controller_Action_Exception In case the given backend name is invalid */ - protected function getUserGroupBackend($name = null, $selectable = true) + protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable') { - $config = Config::app('groups'); if ($name !== null) { + $config = Config::app('groups'); if (! $config->hasSection($name)) { throw new Zend_Controller_Action_Exception( sprintf($this->translate('User group backend "%s" not found'), $name), @@ -88,22 +128,21 @@ class GroupController extends Controller ); } else { $backend = UserGroupBackend::create($name, $config->getSection($name)); - if ($selectable && !$backend instanceof Selectable) { + if ($interface && !$backend instanceof $interface) { + $interfaceParts = explode('\\', strtolower($interface)); throw new Zend_Controller_Action_Exception( - sprintf($this->translate('User group backend "%s" is not able to list groups'), $name), + sprintf( + $this->translate('User group backend "%s" is not %s'), + $name, + array_pop($interfaceParts) + ), 400 ); } } } else { - $backend = null; - foreach ($config as $backendName => $backendConfig) { - $candidate = UserGroupBackend::create($backendName, $backendConfig); - if (! $selectable || $candidate instanceof Selectable) { - $backend = $candidate; - break; - } - } + $backends = $this->loadUserGroupBackends($interface); + $backend = array_shift($backends); } return $backend; diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index 3871b675e..6e63fafdb 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -4,7 +4,10 @@ sortBox; ?> limiter; ?> paginator; ?> - filterEditor; ?> +
      + backendSelection; ?> + filterEditor; ?> +
      From 938da806cae8fa9dc44814b4a5de0510aab7ba81 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 May 2015 12:15:02 +0200 Subject: [PATCH 077/239] Repository: Recurse a filter in the repository instead of in the query This allows to recurse and adjust a filter outside the query context as well refs #8826 --- library/Icinga/Repository/Repository.php | 19 ++++++++++++++++++ library/Icinga/Repository/RepositoryQuery.php | 20 ++----------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index c4ff95ce2..3ecdbe61c 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -4,6 +4,7 @@ namespace Icinga\Repository; use Icinga\Application\Logger; +use Icinga\Data\Filter\Filter; use Icinga\Data\Selectable; use Icinga\Exception\ProgrammingError; use Icinga\Exception\QueryException; @@ -471,6 +472,24 @@ abstract class Repository implements Selectable } } + /** + * Recurse the given filter, require each filter column and convert all values + * + * @param Filter $filter + */ + public function requireFilter(Filter $filter) + { + if ($filter->isExpression()) { + $column = $filter->getColumn(); + $filter->setColumn($this->requireFilterColumn($column)); + $filter->setExpression($this->persistColumn($column, $filter->getExpression())); + } elseif ($filter->isChain()) { + foreach ($filter->filters() as $chainOrExpression) { + $this->requireFilter($chainOrExpression); + } + } + } + /** * Return this repository's query columns mapped to their respective aliases * diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 8c47bf6ff..806bdd93a 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -164,7 +164,7 @@ class RepositoryQuery implements QueryInterface */ public function setFilter(Filter $filter) { - $this->requireFilterColumns($filter); + $this->repository->requireFilter($filter); $this->query->setFilter($filter); return $this; } @@ -180,27 +180,11 @@ class RepositoryQuery implements QueryInterface */ public function addFilter(Filter $filter) { - $this->requireFilterColumns($filter); + $this->repository->requireFilter($filter); $this->query->addFilter($filter); return $this; } - /** - * Recurse the given filter and notify the repository about each required filter column - */ - protected function requireFilterColumns(Filter $filter) - { - if ($filter->isExpression()) { - $column = $filter->getColumn(); - $filter->setColumn($this->repository->requireFilterColumn($column)); - $filter->setExpression($this->repository->persistColumn($column, $filter->getExpression())); - } elseif ($filter->isChain()) { - foreach ($filter->filters() as $chainOrExpression) { - $this->requireFilterColumns($chainOrExpression); - } - } - } - /** * Return the current filter * From 12ff708ac0c4af9653b562714fadd8bad3806fe8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 May 2015 15:17:36 +0200 Subject: [PATCH 078/239] Introduce exception StatementException refs #8826 --- library/Icinga/Exception/StatementException.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 library/Icinga/Exception/StatementException.php diff --git a/library/Icinga/Exception/StatementException.php b/library/Icinga/Exception/StatementException.php new file mode 100644 index 000000000..0bf2bb80a --- /dev/null +++ b/library/Icinga/Exception/StatementException.php @@ -0,0 +1,8 @@ + Date: Fri, 8 May 2015 15:18:28 +0200 Subject: [PATCH 079/239] Introduce interface Extensible refs #8826 --- library/Icinga/Data/Extensible.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 library/Icinga/Data/Extensible.php diff --git a/library/Icinga/Data/Extensible.php b/library/Icinga/Data/Extensible.php new file mode 100644 index 000000000..8b9f39d42 --- /dev/null +++ b/library/Icinga/Data/Extensible.php @@ -0,0 +1,22 @@ + Date: Fri, 8 May 2015 15:18:42 +0200 Subject: [PATCH 080/239] Introduce interface Reducible refs #8826 --- library/Icinga/Data/Reducible.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 library/Icinga/Data/Reducible.php diff --git a/library/Icinga/Data/Reducible.php b/library/Icinga/Data/Reducible.php new file mode 100644 index 000000000..bfe627243 --- /dev/null +++ b/library/Icinga/Data/Reducible.php @@ -0,0 +1,23 @@ + Date: Fri, 8 May 2015 15:18:56 +0200 Subject: [PATCH 081/239] Introduce interface Updatable refs #8826 --- library/Icinga/Data/Updatable.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 library/Icinga/Data/Updatable.php diff --git a/library/Icinga/Data/Updatable.php b/library/Icinga/Data/Updatable.php new file mode 100644 index 000000000..ec07537cb --- /dev/null +++ b/library/Icinga/Data/Updatable.php @@ -0,0 +1,24 @@ + Date: Fri, 8 May 2015 15:22:51 +0200 Subject: [PATCH 082/239] Introduce class IniRepository refs #8826 --- library/Icinga/Repository/IniRepository.php | 201 ++++++++++++++++++++ library/Icinga/Repository/Repository.php | 53 ++++++ 2 files changed, 254 insertions(+) create mode 100644 library/Icinga/Repository/IniRepository.php diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php new file mode 100644 index 000000000..7953422be --- /dev/null +++ b/library/Icinga/Repository/IniRepository.php @@ -0,0 +1,201 @@ + + *
    • Insert, update and delete capabilities
    • + *
    + */ +abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible +{ + /** + * Insert the given data for the given target + * + * In case the data source provides a valid key column, $data must provide a proper + * value for it which is then being used as the section name instead of $target. + * + * @param string $target + * @param array $data + * + * @throws StatementException In case the operation has failed + */ + public function insert($target, array $data) + { + $newData = $this->requireStatementColumns($data); + $section = $this->extractSectionName($target, $newData); + + if ($this->ds->hasSection($section)) { + throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section); + } + + $this->ds->setSection($section, $newData); + + try { + $this->ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Update the target with the given data and optionally limit the affected entries by using a filter + * + * The section(s) to update are either identified by $filter or $target, in order. If neither of both + * is given, all sections provided by the data source are going to be updated. Uniqueness of a section's + * name will be ensured. + * + * @param string $target + * @param array $data + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function update($target, array $data, Filter $filter = null) + { + $newData = $this->requireStatementColumns($data); + $keyColumn = $this->ds->getConfigObject()->getKeyColumn(); + if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) { + throw new StatementException( + t('Cannot update. Column "%s" holds a section\'s name which must be unique'), + $keyColumn + ); + } + + if ($target && !$filter) { + if (! $this->ds->hasSection($target)) { + throw new StatementException(t('Cannot update. Section "%s" does not exist'), $target); + } + + $results = array($target => $this->ds->getSection($target)); + } else { + $query = $this->ds->select(); + if ($filter) { + $this->requireFilter($filter); + $query->applyFilter($filter); + } + + $results = $query->fetchAll(); + } + + $newSection = null; + foreach ($results as $section => $config) { + if ($newSection !== null) { + throw new StatementException( + t('Cannot update. Column "%s" holds a section\'s name which must be unique'), + $keyColumn + ); + } + + foreach ($newData as $column => $value) { + if ($keyColumn && $column === $keyColumn) { + $newSection = $value; + } else { + $config->$column = $value; + } + } + + if ($keyColumn && isset($config->$keyColumn) && $config->$keyColumn === $section) { + unset($config->$keyColumn); + } + + if ($newSection) { + if ($this->ds->hasSection($newSection)) { + throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection); + } + + $this->ds->removeSection($section)->setSection($newSection, $config); + } else { + $this->ds->setSection($section, $config); + } + } + + try { + $this->ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Delete entries in the given target, optionally limiting the affected entries by using a filter + * + * The section(s) to delete are either identified by $filter or $target, in order. If neither of both + * is given, all sections provided by the data source are going to be deleted. + * + * @param string $target + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function delete($target, Filter $filter = null) + { + if ($target && !$filter) { + if (! $this->ds->hasSection($target)) { + return; // Nothing to do + } + + $results = array($target => $this->ds->getSection($target)); + } else { + $query = $this->ds->select(); + if ($filter) { + $this->requireFilter($filter); + $query->applyFilter($filter); + } + + $results = $query->fetchAll(); + } + + foreach ($results as $section => $_) { + $this->ds->removeSection($section); + } + + try { + $this->ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Extract and return the section name off of the given $data, if available, or validate $target + * + * @param string $target + * @param array $data + * + * @return string + * + * @throws ProgrammingError In case no valid section name is available + */ + protected function extractSectionName($target, array & $data) + { + if (($keyColumn = $this->ds->getConfigObject()->getKeyColumn())) { + if (! isset($data[$keyColumn])) { + throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn); + } + + $target = $data[$keyColumn]; + unset($data[$keyColumn]); + } + + if (! is_string($target)) { + throw new ProgrammingError( + 'Neither the data source nor the $target parameter provide a valid section name' + ); + } + + return $target; + } +} diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 3ecdbe61c..1726426ba 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -8,6 +8,7 @@ use Icinga\Data\Filter\Filter; use Icinga\Data\Selectable; use Icinga\Exception\ProgrammingError; use Icinga\Exception\QueryException; +use Icinga\Exception\StatementException; /** * Abstract base class for concrete repository implementations @@ -573,4 +574,56 @@ abstract class Repository implements Selectable return $aliasColumnMap[$name]; } + + /** + * Return whether the given column name or alias is a valid statement column + * + * @param string $name The column name or alias to check + * + * @return bool + */ + public function hasStatementColumn($name) + { + return $this->hasQueryColumn($name); + } + + /** + * Validate that the given column is a valid statement column and return it or the actual name if it's an alias + * + * @param string $name The name or alias of the column to validate + * + * @return string The given column's name + * + * @throws StatementException In case the given column is not a statement column + */ + public function requireStatementColumn($name) + { + if (in_array($name, $this->filterColumns)) { + throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name); + } + + $aliasColumnMap = $this->getAliasColumnMap(); + if (! array_key_exists($name, $aliasColumnMap)) { + throw new StatementException('Statement column "%s" not found', $name); + } + + return $aliasColumnMap[$name]; + } + + /** + * Resolve the given aliases or column names supposed to be persisted and convert their values + * + * @param array $data + * + * @return array + */ + public function requireStatementColumns(array $data) + { + $resolved = array(); + foreach ($data as $alias => $value) { + $resolved[$this->requireStatementColumn($alias)] = $this->persistColumn($alias, $value); + } + + return $resolved; + } } From 59ec11f0476b40632a5a32e2a05b8ad09fcb14d4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 May 2015 15:26:35 +0200 Subject: [PATCH 083/239] IniUserGroupBackend: Extend IniRepository We are now able to insert, update and delete user groups stored in INI files refs #8826 --- .../UserGroup/IniUserGroupBackend.php | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index ba92093aa..62fed0179 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -3,11 +3,13 @@ namespace Icinga\Authentication\UserGroup; -use Icinga\Repository\Repository; +use Icinga\Exception\StatementException; +use Icinga\Data\Filter\Filter; +use Icinga\Repository\IniRepository; use Icinga\User; use Icinga\Util\String; -class IniUserGroupBackend extends Repository implements UserGroupBackendInterface +class IniUserGroupBackend extends IniRepository implements UserGroupBackendInterface { /** * The query columns being provided @@ -55,6 +57,35 @@ class IniUserGroupBackend extends Repository implements UserGroupBackendInterfac $this->ds->getConfigObject()->setKeyColumn('name'); } + /** + * Add a new group to this backend + * + * @param string $target + * @param array $data + * + * @throws StatementException In case the operation has failed + */ + public function insert($target, array $data) + { + $data['created_at'] = time(); + parent::insert($target, $data); + } + + /** + * Update groups of this backend, optionally limited using a filter + * + * @param string $target + * @param array $data + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function update($target, array $data, Filter $filter = null) + { + $data['last_modified'] = time(); + parent::update($target, $data, $filter); + } + /** * Return the groups the given user is a member of * From f1c82fc3188885069298f075858baf2c4ff770f4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 May 2015 15:28:10 +0200 Subject: [PATCH 084/239] IniUserGroupBackend: Convert timestamps and arrays... ...to formatted datetime strings and comma separated strings respectively refs #8826 --- .../UserGroup/IniUserGroupBackend.php | 11 ++ library/Icinga/Repository/Repository.php | 100 ++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index 62fed0179..e70c9953c 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -49,6 +49,17 @@ class IniUserGroupBackend extends IniRepository implements UserGroupBackendInter ) ); + /** + * The value conversion rules to apply on a query + * + * @var array + */ + protected $conversionRules = array( + 'created_at' => 'date_time', + 'last_modified' => 'date_time', + 'users' => 'comma_separated_string' + ); + /** * Initialize this ini user group backend */ diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 1726426ba..f33140b1f 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -3,12 +3,14 @@ namespace Icinga\Repository; +use DateTime; use Icinga\Application\Logger; use Icinga\Data\Filter\Filter; use Icinga\Data\Selectable; use Icinga\Exception\ProgrammingError; use Icinga\Exception\QueryException; use Icinga\Exception\StatementException; +use Icinga\Util\String; /** * Abstract base class for concrete repository implementations @@ -22,6 +24,11 @@ use Icinga\Exception\StatementException; */ abstract class Repository implements Selectable { + /** + * The format to use when converting values of type date_time + */ + const DATETIME_FORMAT = 'd/m/Y g:i A'; + /** * The name of this repository * @@ -473,6 +480,99 @@ abstract class Repository implements Selectable } } + /** + * Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT + * + * @param mixed $value + * + * @return string + */ + protected function persistDateTime($value) + { + if (is_numeric($value)) { + $value = date(static::DATETIME_FORMAT, $value); + } elseif ($value instanceof DateTime) { + $value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone + } elseif ($value !== null) { + throw new ProgrammingError( + 'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object', + $value + ); + } + + return $value; + } + + /** + * Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp + * + * @param string $value + * + * @return int + */ + protected function retrieveDateTime($value) + { + if (is_numeric($value)) { + $value = (int) $value; + } elseif (is_string($value)) { + $dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value); + if ($dateTime === false) { + Logger::debug( + 'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"', + $value, + static::DATETIME_FORMAT, + $this->getName() + ); + $value = null; + } else { + $value = $dateTime->getTimestamp(); + } + } elseif ($value !== null) { + throw new ProgrammingError( + 'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string', + $value + ); + } + + return $value; + } + + /** + * Convert the given array to an comma separated string + * + * @param array|string $value + * + * @return string + */ + protected function persistCommaSeparatedString($value) + { + if (is_array($value)) { + $value = join(',', array_map('trim', $value)); + } elseif ($value !== null && !is_string($value)) { + throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value); + } + + return $value; + } + + /** + * Convert the given comma separated string to an array + * + * @param string $value + * + * @return array + */ + protected function retrieveCommaSeparatedString($value) + { + if ($value && is_string($value)) { + $value = String::trimSplit($value); + } elseif ($value !== null) { + throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value); + } + + return $value; + } + /** * Recurse the given filter, require each filter column and convert all values * From 30bc1db6ee44de9a80c066503e3da32ff2cb3391 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 11 May 2015 07:46:36 +0200 Subject: [PATCH 085/239] IniRepository: There is no need to fetch the results using a query Icinga\Application\Config is iterable. refs #8826 --- library/Icinga/Repository/IniRepository.php | 30 +++++++++------------ 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php index 7953422be..6b95fcdc1 100644 --- a/library/Icinga/Repository/IniRepository.php +++ b/library/Icinga/Repository/IniRepository.php @@ -79,19 +79,21 @@ abstract class IniRepository extends Repository implements Extensible, Updatable throw new StatementException(t('Cannot update. Section "%s" does not exist'), $target); } - $results = array($target => $this->ds->getSection($target)); + $contents = array($target => $this->ds->getSection($target)); } else { - $query = $this->ds->select(); if ($filter) { $this->requireFilter($filter); - $query->applyFilter($filter); } - $results = $query->fetchAll(); + $contents = iterator_to_array($this->ds); } $newSection = null; - foreach ($results as $section => $config) { + foreach ($contents as $section => $config) { + if ($filter && !$filter->matches($config)) { + continue; + } + if ($newSection !== null) { throw new StatementException( t('Cannot update. Column "%s" holds a section\'s name which must be unique'), @@ -107,10 +109,6 @@ abstract class IniRepository extends Repository implements Extensible, Updatable } } - if ($keyColumn && isset($config->$keyColumn) && $config->$keyColumn === $section) { - unset($config->$keyColumn); - } - if ($newSection) { if ($this->ds->hasSection($newSection)) { throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection); @@ -147,19 +145,17 @@ abstract class IniRepository extends Repository implements Extensible, Updatable return; // Nothing to do } - $results = array($target => $this->ds->getSection($target)); + $this->ds->removeSection($target); } else { - $query = $this->ds->select(); if ($filter) { $this->requireFilter($filter); - $query->applyFilter($filter); } - $results = $query->fetchAll(); - } - - foreach ($results as $section => $_) { - $this->ds->removeSection($section); + foreach (iterator_to_array($this->ds) as $section => $config) { + if (! $filter || $filter->matches($config)) { + $this->ds->removeSection($section); + } + } } try { From 3aaa6d39a14879830a6155c07133fe50ea8db251 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 11 May 2015 13:25:50 +0200 Subject: [PATCH 086/239] DbConnection: Make it possible to insert, update and delete table rows refs #8826 --- library/Icinga/Data/Db/DbConnection.php | 120 +++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index 83c74fb8e..d0d2ddf98 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -8,14 +8,22 @@ use Zend_Db; use Icinga\Application\Benchmark; use Icinga\Data\ConfigObject; use Icinga\Data\Db\DbQuery; +use Icinga\Data\Extensible; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterNot; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\Reducible; use Icinga\Data\ResourceFactory; use Icinga\Data\Selectable; +use Icinga\Data\Updatable; use Icinga\Exception\ConfigurationError; +use Icinga\Exception\ProgrammingError; /** * Encapsulate database connections and query creation */ -class DbConnection implements Selectable +class DbConnection implements Selectable, Extensible, Updatable, Reducible { /** * Connection config @@ -259,4 +267,114 @@ class DbConnection implements Selectable { return $this->dbAdapter->fetchPairs($query->getSelectQuery()); } + + /** + * Insert a table row with the given data + * + * @param string $table + * @param array $bind + * + * @return int The number of affected rows + */ + public function insert($table, array $bind) + { + return $this->dbAdapter->insert($table, $bind); + } + + /** + * Update table rows with the given data, optionally limited by using a filter + * + * @param string $table + * @param array $bind + * @param Filter $filter + * + * @return int The number of affected rows + */ + public function update($table, array $bind, Filter $filter = null) + { + return $this->dbAdapter->update($table, $bind, $filter ? $this->renderFilter($filter) : ''); + } + + /** + * Delete table rows, optionally limited by using a filter + * + * @param string $table + * @param Filter $filter + * + * @return int The number of affected rows + */ + public function delete($table, Filter $filter = null) + { + return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : ''); + } + + /** + * Render and return the given filter as SQL-WHERE clause + * + * @param Filter $filter + * + * @return string + */ + public function renderFilter(Filter $filter, $level = 0) + { + // TODO: This is supposed to supersede DbQuery::renderFilter() + $where = ''; + if ($filter->isChain()) { + if ($filter instanceof FilterAnd) { + $operator = ' AND '; + } elseif ($filter instanceof FilterOr) { + $operator = ' OR '; + } elseif ($filter instanceof FilterNot) { + $operator = ' AND '; + $where .= ' NOT '; + } else { + throw new ProgrammingError('Cannot render filter: %s', get_class($filter)); + } + + if (! $filter->isEmpty()) { + $parts = array(); + foreach ($filter->filters() as $filterPart) { + $part = $this->renderFilter($filterPart, $level + 1); + if ($part) { + $parts[] = $part; + } + } + + if ($level > 0) { + $where .= ' (' . implode($operator, $parts) . ') '; + } else { + $where .= implode($operator, $parts); + } + } + } else { + $where .= $this->renderFilterExpression($filter); + } + + return $where; + } + + /** + * Render and return the given filter expression + * + * @param Filter $filter + * + * @return string + */ + protected function renderFilterExpression(Filter $filter) + { + $column = $filter->getColumn(); + $sign = $filter->getSign(); + $value = $filter->getExpression(); + + if (is_array($value) && $sign === '=') { + // TODO: Should we support this? Doesn't work for blub* + return $column . ' IN (' . $this->dbAdapter->quote($value) . ')'; + } elseif ($sign === '=' && strpos($value, '*') !== false) { + return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value)); + } elseif ($sign === '!=' && strpos($value, '*') !== false) { + return $column . ' NOT LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value)); + } else { + return $column . ' ' . $sign . ' ' . $this->dbAdapter->quote($value); + } + } } From ca166b0175fe6fdee226566700387e7214471be4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 11 May 2015 13:26:41 +0200 Subject: [PATCH 087/239] DbRepository: Add insert, update and delete capabilities refs #8826 --- library/Icinga/Repository/DbRepository.php | 191 ++++++++++++++++++++- 1 file changed, 190 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 8ba43efaf..2b9901c21 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -3,6 +3,10 @@ namespace Icinga\Repository; +use Icinga\Data\Extensible; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Reducible; +use Icinga\Data\Updatable; use Icinga\Exception\IcingaException; use Icinga\Exception\ProgrammingError; @@ -12,10 +16,45 @@ use Icinga\Exception\ProgrammingError; * Additionally provided features: *
      *
    • Automatic table prefix handling
    • + *
    • Insert, update and delete capabilities
    • + *
    • Differentiation between statement and query columns
    • *
    */ -abstract class DbRepository extends Repository +abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible { + /** + * The statement columns being provided + * + * This may be initialized by repositories which are going to make use of table aliases. It allows to provide + * alias-less column names to be used for a statement. The array needs to be in the following format: + *
    
    +     *  array(
    +     *      'table_name' => array(
    +     *          'column1',
    +     *          'alias1' => 'column2',
    +     *          'alias2' => 'column3'
    +     *      )
    +     *  )
    +     * 
    
    +     *
    +     * @var array
    +     */
    +    protected $statementColumns;
    +
    +    /**
    +     * An array to map table names to statement columns/aliases
    +     *
    +     * @var array
    +     */
    +    protected $statementTableMap;
    +
    +    /**
    +     * A flattened array to map statement columns to aliases
    +     *
    +     * @var array
    +     */
    +    protected $statementColumnMap;
    +
         /**
          * Return the base table name this repository is responsible for
          *
    @@ -61,4 +100,154 @@ abstract class DbRepository extends Repository
     
             return $table;
         }
    +
    +    /**
    +     * Insert a table row with the given data
    +     *
    +     * @param   string  $table
    +     * @param   array   $bind
    +     */
    +    public function insert($table, array $bind)
    +    {
    +        $this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($bind));
    +    }
    +
    +    /**
    +     * Update table rows with the given data, optionally limited by using a filter
    +     *
    +     * @param   string  $table
    +     * @param   array   $bind
    +     * @param   Filter  $filter
    +     */
    +    public function update($table, array $bind, Filter $filter = null)
    +    {
    +        if ($filter) {
    +            $this->requireFilter($filter);
    +        }
    +
    +        $this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($bind), $filter);
    +    }
    +
    +    /**
    +     * Delete table rows, optionally limited by using a filter
    +     *
    +     * @param   string  $table
    +     * @param   Filter  $filter
    +     */
    +    public function delete($table, Filter $filter = null)
    +    {
    +        if ($filter) {
    +            $this->requireFilter($filter);
    +        }
    +
    +        $this->ds->delete($this->prependTablePrefix($table), $filter);
    +    }
    +
    +    /**
    +     * Return the statement columns being provided
    +     *
    +     * Calls $this->initializeStatementColumns() in case $this->statementColumns is null.
    +     *
    +     * @return  array
    +     */
    +    public function getStatementColumns()
    +    {
    +        if ($this->statementColumns === null) {
    +            $this->statementColumns = $this->initializeStatementColumns();
    +        }
    +
    +        return $this->statementColumns;
    +    }
    +
    +    /**
    +     * Overwrite this in your repository implementation in case you need to initialize the statement columns lazily
    +     *
    +     * @return  array
    +     */
    +    protected function initializeStatementColumns()
    +    {
    +        return array();
    +    }
    +
    +    /**
    +     * Return an array to map table names to statement columns/aliases
    +     *
    +     * @return  array
    +     */
    +    protected function getStatementTableMap()
    +    {
    +        if ($this->statementTableMap === null) {
    +            $this->initializeStatementMaps();
    +        }
    +
    +        return $this->statementTableMap;
    +    }
    +
    +    /**
    +     * Return a flattened array to map statement columns to aliases
    +     *
    +     * @return  array
    +     */
    +    protected function getStatementColumnMap()
    +    {
    +        if ($this->statementColumnMap === null) {
    +            $this->initializeStatementMaps();
    +        }
    +
    +        return $this->statementColumnMap;
    +    }
    +
    +    /**
    +     * Initialize $this->statementTableMap and $this->statementColumnMap
    +     */
    +    protected function initializeStatementMaps()
    +    {
    +        foreach ($this->getStatementColumns() as $table => $columns) {
    +            foreach ($columns as $alias => $column) {
    +                if (! is_string($alias)) {
    +                    $this->statementTableMap[$column] = $table;
    +                    $this->statementColumnMap[$column] = $column;
    +                } else {
    +                    $this->statementTableMap[$alias] = $table;
    +                    $this->statementColumnMap[$alias] = $column;
    +                }
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Return whether the given column name or alias is a valid statement column
    +     *
    +     * @param   string  $name   The column name or alias to check
    +     *
    +     * @return  bool
    +     */
    +    public function hasStatementColumn($name)
    +    {
    +        $statementColumnMap = $this->getStatementColumnMap();
    +        if (! array_key_exists($name, $statementColumnMap)) {
    +            return parent::hasStatementColumn($name);
    +        }
    +
    +        return true;
    +    }
    +
    +    /**
    +     * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
    +     *
    +     * @param   string  $name       The name or alias of the column to validate
    +     *
    +     * @return  string              The given column's name
    +     *
    +     * @throws  QueryException      In case the given column is not a statement column
    +     */
    +    public function requireStatementColumn($name)
    +    {
    +        $statementColumnMap = $this->getStatementColumnMap();
    +        if (! array_key_exists($name, $statementColumnMap)) {
    +            return parent::requireStatementColumn($name);
    +        }
    +
    +        return $statementColumnMap[$name];
    +    }
     }
    
    From b3957c556be2a25242b16069c0517581aba8e495 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 11 May 2015 13:28:01 +0200
    Subject: [PATCH 088/239] DbUserGroupBackend: Properly utilize the insert and
     update capability
    
    refs #8826
    ---
     .../UserGroup/DbUserGroupBackend.php          | 38 +++++++++++++++++++
     1 file changed, 38 insertions(+)
    
    diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    index 32d65ab53..1e4ad395e 100644
    --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    @@ -3,6 +3,7 @@
     
     namespace Icinga\Authentication\UserGroup;
     
    +use Icinga\Data\Filter\Filter;
     use Icinga\Repository\DbRepository;
     use Icinga\User;
     
    @@ -24,6 +25,18 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
             )
         );
     
    +    /**
    +     * The statement columns being provided
    +     *
    +     * @var array
    +     */
    +    protected $statementColumns = array(
    +        'group' => array(
    +            'created_at'    => 'ctime',
    +            'last_modified' => 'mtime'
    +        )
    +    );
    +
         /**
          * The columns which are not permitted to be queried
          *
    @@ -55,6 +68,31 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
             }
         }
     
    +    /**
    +     * Insert a table row with the given data
    +     *
    +     * @param   string  $table
    +     * @param   array   $bind
    +     */
    +    public function insert($table, array $bind)
    +    {
    +        $bind['created_at'] = date('Y-m-d H:i:s');
    +        parent::insert($table, $bind);
    +    }
    +
    +    /**
    +     * Update table rows with the given data, optionally limited by using a filter
    +     *
    +     * @param   string  $table
    +     * @param   array   $bind
    +     * @param   Filter  $filter
    +     */
    +    public function update($table, array $bind, Filter $filter = null)
    +    {
    +        $bind['last_modified'] = date('Y-m-d H:i:s');
    +        parent::update($table, $bind, $filter);
    +    }
    +
         /**
          * Return the groups the given user is a member of
          *
    
    From 44bbd93cbc3b63367c63a4b3f5a3b64e6b158518 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 11 May 2015 16:00:24 +0200
    Subject: [PATCH 089/239] DbUserBackend: Provide a custom insert and update
     implementation
    
    As we're transmitting password hashes which may contain special chars
    and the like, we need to utilize prepared statements with explicit types.
    
    refs #8826
    ---
     .../Authentication/User/DbUserBackend.php     | 115 ++++++++++++++++--
     1 file changed, 102 insertions(+), 13 deletions(-)
    
    diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php
    index 367418f7b..93602da21 100644
    --- a/library/Icinga/Authentication/User/DbUserBackend.php
    +++ b/library/Icinga/Authentication/User/DbUserBackend.php
    @@ -5,6 +5,7 @@ namespace Icinga\Authentication\User;
     
     use Exception;
     use PDO;
    +use Icinga\Data\Filter\Filter;
     use Icinga\Exception\AuthenticationException;
     use Icinga\Repository\DbRepository;
     use Icinga\User;
    @@ -40,6 +41,19 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
             )
         );
     
    +    /**
    +     * The statement columns being provided
    +     *
    +     * @var array
    +     */
    +    protected $statementColumns = array(
    +        'user' => array(
    +            'password'      => 'password_hash',
    +            'created_at'    => 'ctime',
    +            'last_modified' => 'mtime'
    +        )
    +    );
    +
         /**
          * The columns which are not permitted to be queried
          *
    @@ -61,6 +75,13 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
             )
         );
     
    +    /**
    +     * The value conversion rules to apply on a query/statement
    +     *
    +     * @var array
    +     */
    +    protected $conversionRules = array('password');
    +
         /**
          * Initialize this database user backend
          */
    @@ -72,23 +93,91 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
         }
     
         /**
    -     * Add a new user
    +     * Insert a table row with the given data
          *
    -     * @param   string  $username   The name of the new user
    -     * @param   string  $password   The new user's password
    -     * @param   bool    $active     Whether the user is active
    +     * @param   string  $table
    +     * @param   array   $data
          */
    -    public function addUser($username, $password, $active = true)
    +    public function insert($table, array $data)
         {
    -        $passwordHash = $this->hashPassword($password);
    +        $newData['created_at'] = date('Y-m-d H:i:s');
    +        $newData = $this->requireStatementColumns($data);
     
    -        $stmt = $this->ds->getDbAdapter()->prepare(
    -            'INSERT INTO icingaweb_user VALUES (:name, :active, :password_hash, now(), DEFAULT);'
    -        );
    -        $stmt->bindParam(':name', $username, PDO::PARAM_STR);
    -        $stmt->bindParam(':active', $active, PDO::PARAM_INT);
    -        $stmt->bindParam(':password_hash', $passwordHash, PDO::PARAM_LOB);
    -        $stmt->execute();
    +        $values = array();
    +        foreach ($newData as $column => $_) {
    +            $values[] = ':' . $column;
    +        }
    +
    +        $sql = 'INSERT INTO '
    +            . $this->prependTablePrefix($table)
    +            . ' (' . join(', ', array_keys($newData)) . ') '
    +            . 'VALUES (' . join(', ', $values) . ')';
    +        $statement = $this->ds->getDbAdapter()->prepare($sql);
    +
    +        foreach ($newData as $column => $value) {
    +            $type = PDO::PARAM_STR;
    +            if ($column === 'password_hash') {
    +                $type = PDO::PARAM_LOB;
    +            } elseif ($column === 'active') {
    +                $type = PDO::PARAM_INT;
    +            }
    +
    +            $statement->bindValue(':' . $column, $value, $type);
    +        }
    +
    +        $statement->execute();
    +    }
    +
    +    /**
    +     * Update table rows with the given data, optionally limited by using a filter
    +     *
    +     * @param   string  $table
    +     * @param   array   $data
    +     * @param   Filter  $filter
    +     */
    +    public function update($table, array $data, Filter $filter = null)
    +    {
    +        $newData['last_modified'] = date('Y-m-d H:i:s');
    +        $newData = $this->requireStatementColumns($data);
    +        if ($filter) {
    +            $this->requireFilter($filter);
    +        }
    +
    +        $set = array();
    +        foreach ($newData as $column => $_) {
    +            $set[] = $column . ' = :' . $column;
    +        }
    +
    +        $sql = 'UPDATE '
    +            . $this->prependTablePrefix($table)
    +            . ' SET ' . join(', ', $set)
    +            . ($filter ? ' WHERE ' . $this->ds->renderFilter($filter) : '');
    +        $statement = $this->ds->getDbAdapter()->prepare($sql);
    +
    +        foreach ($newData as $column => $value) {
    +            $type = PDO::PARAM_STR;
    +            if ($column === 'password_hash') {
    +                $type = PDO::PARAM_LOB;
    +            } elseif ($column === 'active') {
    +                $type = PDO::PARAM_INT;
    +            }
    +
    +            $statement->bindValue(':' . $column, $value, $type);
    +        }
    +
    +        $statement->execute();
    +    }
    +
    +    /**
    +     * Hash and return the given password
    +     *
    +     * @param   string  $value
    +     *
    +     * @return  string
    +     */
    +    protected function persistPassword($value)
    +    {
    +        return $this->hashPassword($value);
         }
     
         /**
    
    From 399bbf0795cc502c75ab94538d920ba97300cb45 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 11 May 2015 16:01:20 +0200
    Subject: [PATCH 090/239] AuthenticationStep: Adjust usage of a DbUserBackend's
     insert capability
    
    refs #8826
    ---
     .../setup/library/Setup/Steps/AuthenticationStep.php  | 11 ++++++-----
     1 file changed, 6 insertions(+), 5 deletions(-)
    
    diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php
    index 913f83066..58d9d68d7 100644
    --- a/modules/setup/library/Setup/Steps/AuthenticationStep.php
    +++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php
    @@ -88,11 +88,12 @@ class AuthenticationStep extends Step
                     ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
                 );
     
    -            if (array_search($this->data['adminAccountData']['username'], $backend->listUsers()) === false) {
    -                $backend->addUser(
    -                    $this->data['adminAccountData']['username'],
    -                    $this->data['adminAccountData']['password']
    -                );
    +            if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
    +                $backend->insert('user', array(
    +                    'user_name' => $this->data['adminAccountData']['username'],
    +                    'password'  => $this->data['adminAccountData']['password'],
    +                    'is_active' => true
    +                ));
                 }
             } catch (Exception $e) {
                 $this->dbError = $e;
    
    From 053c9cdcb358a95886f126f57afd797bef771bad Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Tue, 12 May 2015 15:38:29 +0200
    Subject: [PATCH 091/239] Repository: Check whether a column is queried from
     the correct table
    
    refs #8826
    ---
     .../Authentication/User/DbUserBackend.php     |   6 +-
     library/Icinga/Repository/DbRepository.php    |  98 +++++++++++--
     library/Icinga/Repository/IniRepository.php   |   8 +-
     library/Icinga/Repository/Repository.php      | 136 ++++++++++++------
     library/Icinga/Repository/RepositoryQuery.php |  29 ++--
     5 files changed, 202 insertions(+), 75 deletions(-)
    
    diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php
    index 93602da21..441e6cdf3 100644
    --- a/library/Icinga/Authentication/User/DbUserBackend.php
    +++ b/library/Icinga/Authentication/User/DbUserBackend.php
    @@ -101,7 +101,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
         public function insert($table, array $data)
         {
             $newData['created_at'] = date('Y-m-d H:i:s');
    -        $newData = $this->requireStatementColumns($data);
    +        $newData = $this->requireStatementColumns($table, $data);
     
             $values = array();
             foreach ($newData as $column => $_) {
    @@ -138,9 +138,9 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
         public function update($table, array $data, Filter $filter = null)
         {
             $newData['last_modified'] = date('Y-m-d H:i:s');
    -        $newData = $this->requireStatementColumns($data);
    +        $newData = $this->requireStatementColumns($table, $data);
             if ($filter) {
    -            $this->requireFilter($filter);
    +            $this->requireFilter($table, $filter);
             }
     
             $set = array();
    diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
    index 2b9901c21..80788ed73 100644
    --- a/library/Icinga/Repository/DbRepository.php
    +++ b/library/Icinga/Repository/DbRepository.php
    @@ -9,6 +9,7 @@ use Icinga\Data\Reducible;
     use Icinga\Data\Updatable;
     use Icinga\Exception\IcingaException;
     use Icinga\Exception\ProgrammingError;
    +use Icinga\Exception\StatementException;
     
     /**
      * Abstract base class for concrete database repository implementations
    @@ -101,6 +102,37 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
             return $table;
         }
     
    +    /**
    +     * Remove the datasource's prefix from the given table name and return the remaining part
    +     *
    +     * @param   mixed   $table
    +     *
    +     * @return  mixed
    +     */
    +    protected function removeTablePrefix($table)
    +    {
    +        $prefix = $this->ds->getTablePrefix();
    +        if (! $prefix) {
    +            return $table;
    +        }
    +
    +        if (is_array($table)) {
    +            foreach ($table as & $tableName) {
    +                if (strpos($tableName, $prefix) === 0) {
    +                    $tableName = str_replace($prefix, '', $tableName);
    +                }
    +            }
    +        } elseif (is_string($table)) {
    +            if (strpos($table, $prefix) === 0) {
    +                $table = str_replace($prefix, '', $table);
    +            }
    +        } else {
    +            throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
    +        }
    +
    +        return $table;
    +    }
    +
         /**
          * Insert a table row with the given data
          *
    @@ -109,7 +141,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
          */
         public function insert($table, array $bind)
         {
    -        $this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($bind));
    +        $this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
         }
     
         /**
    @@ -122,10 +154,10 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
         public function update($table, array $bind, Filter $filter = null)
         {
             if ($filter) {
    -            $this->requireFilter($filter);
    +            $this->requireFilter($table, $filter);
             }
     
    -        $this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($bind), $filter);
    +        $this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
         }
     
         /**
    @@ -137,7 +169,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
         public function delete($table, Filter $filter = null)
         {
             if ($filter) {
    -            $this->requireFilter($filter);
    +            $this->requireFilter($table, $filter);
             }
     
             $this->ds->delete($this->prependTablePrefix($table), $filter);
    @@ -216,17 +248,56 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
         }
     
         /**
    -     * Return whether the given column name or alias is a valid statement column
    +     * Return whether the given query column name or alias is available in the given table
          *
    +     * @param   mixed   $table
    +     * @param   string  $column
    +     *
    +     * @return  bool
    +     */
    +    public function validateQueryColumnAssociation($table, $column)
    +    {
    +        if (is_array($table)) {
    +            $table = array_shift($table);
    +        }
    +
    +        return parent::validateQueryColumnAssociation($this->removeTablePrefix($table), $column);
    +    }
    +
    +    /**
    +     * Return whether the given statement column name or alias is available in the given table
    +     *
    +     * @param   mixed   $table
    +     * @param   string  $column
    +     *
    +     * @return  bool
    +     */
    +    public function validateStatementColumnAssociation($table, $column)
    +    {
    +        if (is_array($table)) {
    +            $table = array_shift($table);
    +        }
    +
    +        $statementTableMap = $this->getStatementTableMap();
    +        return $statementTableMap[$column] === $this->removeTablePrefix($table);
    +    }
    +
    +    /**
    +     * Return whether the given column name or alias of the given table is a valid statement column
    +     *
    +     * @param   mixed   $table  The table where to look for the column or alias
          * @param   string  $name   The column name or alias to check
          *
          * @return  bool
          */
    -    public function hasStatementColumn($name)
    +    public function hasStatementColumn($table, $name)
         {
             $statementColumnMap = $this->getStatementColumnMap();
    -        if (! array_key_exists($name, $statementColumnMap)) {
    -            return parent::hasStatementColumn($name);
    +        if (
    +            ! array_key_exists($name, $statementColumnMap)
    +            || !$this->validateStatementColumnAssociation($table, $name)
    +        ) {
    +            return parent::hasStatementColumn($table, $name);
             }
     
             return true;
    @@ -235,17 +306,22 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
         /**
          * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
          *
    +     * @param   mixed   $table      The table for which to require the column
          * @param   string  $name       The name or alias of the column to validate
          *
          * @return  string              The given column's name
          *
    -     * @throws  QueryException      In case the given column is not a statement column
    +     * @throws  StatementException  In case the given column is not a statement column
          */
    -    public function requireStatementColumn($name)
    +    public function requireStatementColumn($table, $name)
         {
             $statementColumnMap = $this->getStatementColumnMap();
             if (! array_key_exists($name, $statementColumnMap)) {
    -            return parent::requireStatementColumn($name);
    +            return parent::requireStatementColumn($table, $name);
    +        }
    +
    +        if (! $this->validateStatementColumnAssociation($table, $name)) {
    +            throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
             }
     
             return $statementColumnMap[$name];
    diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
    index 6b95fcdc1..6d6d24ee6 100644
    --- a/library/Icinga/Repository/IniRepository.php
    +++ b/library/Icinga/Repository/IniRepository.php
    @@ -34,7 +34,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
          */
         public function insert($target, array $data)
         {
    -        $newData = $this->requireStatementColumns($data);
    +        $newData = $this->requireStatementColumns($target, $data);
             $section = $this->extractSectionName($target, $newData);
     
             if ($this->ds->hasSection($section)) {
    @@ -65,7 +65,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
          */
         public function update($target, array $data, Filter $filter = null)
         {
    -        $newData = $this->requireStatementColumns($data);
    +        $newData = $this->requireStatementColumns($target, $data);
             $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
             if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) {
                 throw new StatementException(
    @@ -82,7 +82,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
                 $contents = array($target => $this->ds->getSection($target));
             } else {
                 if ($filter) {
    -                $this->requireFilter($filter);
    +                $this->requireFilter($target, $filter);
                 }
     
                 $contents = iterator_to_array($this->ds);
    @@ -148,7 +148,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
                 $this->ds->removeSection($target);
             } else {
                 if ($filter) {
    -                $this->requireFilter($filter);
    +                $this->requireFilter($target, $filter);
                 }
     
                 foreach (iterator_to_array($this->ds) as $section => $config) {
    diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
    index f33140b1f..24cd48161 100644
    --- a/library/Icinga/Repository/Repository.php
    +++ b/library/Icinga/Repository/Repository.php
    @@ -574,63 +574,87 @@ abstract class Repository implements Selectable
         }
     
         /**
    -     * Recurse the given filter, require each filter column and convert all values
    +     * Recurse the given filter, require each column for the given table and convert all values
          *
    +     * @param   string  $table
          * @param   Filter  $filter
          */
    -    public function requireFilter(Filter $filter)
    +    public function requireFilter($table, Filter $filter)
         {
             if ($filter->isExpression()) {
                 $column = $filter->getColumn();
    -            $filter->setColumn($this->requireFilterColumn($column));
    +            $filter->setColumn($this->requireFilterColumn($table, $column));
                 $filter->setExpression($this->persistColumn($column, $filter->getExpression()));
             } elseif ($filter->isChain()) {
                 foreach ($filter->filters() as $chainOrExpression) {
    -                $this->requireFilter($chainOrExpression);
    +                $this->requireFilter($table, $chainOrExpression);
                 }
             }
         }
     
         /**
    -     * Return this repository's query columns mapped to their respective aliases
    +     * Return this repository's query columns of the given table mapped to their respective aliases
    +     *
    +     * @param   string  $table
          *
          * @return  array
          */
    -    public function requireAllQueryColumns()
    +    public function requireAllQueryColumns($table)
         {
             $map = array();
             foreach ($this->getAliasColumnMap() as $alias => $_) {
    -            if ($this->hasQueryColumn($alias)) {
    +            if ($this->hasQueryColumn($table, $alias)) {
                     // Just in case $this->requireQueryColumn has been overwritten and there is some magic going on
    -                $map[$alias] = $this->requireQueryColumn($alias);
    +                $map[$alias] = $this->requireQueryColumn($table, $alias);
                 }
             }
     
             return $map;
         }
     
    +    /**
    +     * Return whether the given query column name or alias is available in the given table
    +     *
    +     * @param   string  $table
    +     * @param   string  $column
    +     *
    +     * @return  bool
    +     */
    +    public function validateQueryColumnAssociation($table, $column)
    +    {
    +        $aliasTableMap = $this->getAliasTableMap();
    +        return $aliasTableMap[$column] === $table;
    +    }
    +
         /**
          * Return whether the given column name or alias is a valid query column
          *
    +     * @param   string  $table  The table where to look for the column or alias
          * @param   string  $name   The column name or alias to check
          *
          * @return  bool
          */
    -    public function hasQueryColumn($name)
    +    public function hasQueryColumn($table, $name)
         {
    -        return array_key_exists($name, $this->getAliasColumnMap()) && !in_array($name, $this->getFilterColumns());
    +        if (in_array($name, $this->getFilterColumns())) {
    +            return false;
    +        }
    +
    +        return array_key_exists($name, $this->getAliasColumnMap())
    +            && $this->validateQueryColumnAssociation($table, $name);
         }
     
         /**
          * Validate that the given column is a valid query target and return it or the actual name if it's an alias
          *
    +     * @param   string  $table      The table where to look for the column or alias
          * @param   string  $name       The name or alias of the column to validate
          *
          * @return  string              The given column's name
          *
          * @throws  QueryException      In case the given column is not a valid query column
          */
    -    public function requireQueryColumn($name)
    +    public function requireQueryColumn($table, $name)
         {
             if (in_array($name, $this->getFilterColumns())) {
                 throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
    @@ -641,62 +665,75 @@ abstract class Repository implements Selectable
                 throw new QueryException(t('Query column "%s" not found'), $name);
             }
     
    -        return $aliasColumnMap[$name];
    -    }
    -
    -    /**
    -     * Return whether the given column name or alias is a valid filter column
    -     *
    -     * @param   string  $name   The column name or alias to check
    -     *
    -     * @return  bool
    -     */
    -    public function hasFilterColumn($name)
    -    {
    -        return array_key_exists($name, $this->getAliasColumnMap());
    -    }
    -
    -    /**
    -     * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
    -     *
    -     * @param   string  $name       The name or alias of the column to validate
    -     *
    -     * @return  string              The given column's name
    -     *
    -     * @throws  QueryException      In case the given column is not a valid filter column
    -     */
    -    public function requireFilterColumn($name)
    -    {
    -        $aliasColumnMap = $this->getAliasColumnMap();
    -        if (! array_key_exists($name, $aliasColumnMap)) {
    -            throw new QueryException(t('Filter column "%s" not found'), $name);
    +        if (! $this->validateQueryColumnAssociation($table, $name)) {
    +            throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
             }
     
             return $aliasColumnMap[$name];
         }
     
         /**
    -     * Return whether the given column name or alias is a valid statement column
    +     * Return whether the given column name or alias is a valid filter column
          *
    +     * @param   string  $table  The table where to look for the column or alias
          * @param   string  $name   The column name or alias to check
          *
          * @return  bool
          */
    -    public function hasStatementColumn($name)
    +    public function hasFilterColumn($table, $name)
         {
    -        return $this->hasQueryColumn($name);
    +        return array_key_exists($name, $this->getAliasColumnMap())
    +            && $this->validateQueryColumnAssociation($table, $name);
    +    }
    +
    +    /**
    +     * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
    +     *
    +     * @param   string  $table      The table where to look for the column or alias
    +     * @param   string  $name       The name or alias of the column to validate
    +     *
    +     * @return  string              The given column's name
    +     *
    +     * @throws  QueryException      In case the given column is not a valid filter column
    +     */
    +    public function requireFilterColumn($table, $name)
    +    {
    +        $aliasColumnMap = $this->getAliasColumnMap();
    +        if (! array_key_exists($name, $aliasColumnMap)) {
    +            throw new QueryException(t('Filter column "%s" not found'), $name);
    +        }
    +
    +        if (! $this->validateQueryColumnAssociation($table, $name)) {
    +            throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
    +        }
    +
    +        return $aliasColumnMap[$name];
    +    }
    +
    +    /**
    +     * Return whether the given column name or alias of the given table is a valid statement column
    +     *
    +     * @param   string  $table  The table where to look for the column or alias
    +     * @param   string  $name   The column name or alias to check
    +     *
    +     * @return  bool
    +     */
    +    public function hasStatementColumn($table, $name)
    +    {
    +        return $this->hasQueryColumn($table, $name);
         }
     
         /**
          * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
          *
    +     * @param   string  $table      The table for which to require the column
          * @param   string  $name       The name or alias of the column to validate
          *
          * @return  string              The given column's name
          *
          * @throws  StatementException  In case the given column is not a statement column
          */
    -    public function requireStatementColumn($name)
    +    public function requireStatementColumn($table, $name)
         {
             if (in_array($name, $this->filterColumns)) {
                 throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
    @@ -707,21 +744,26 @@ abstract class Repository implements Selectable
                 throw new StatementException('Statement column "%s" not found', $name);
             }
     
    +        if (! $this->validateQueryColumnAssociation($table, $name)) {
    +            throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
    +        }
    +
             return $aliasColumnMap[$name];
         }
     
         /**
    -     * Resolve the given aliases or column names supposed to be persisted and convert their values
    +     * Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
          *
    +     * @param   string  $table
          * @param   array   $data
          *
          * @return  array
          */
    -    public function requireStatementColumns(array $data)
    +    public function requireStatementColumns($table, array $data)
         {
             $resolved = array();
             foreach ($data as $alias => $value) {
    -            $resolved[$this->requireStatementColumn($alias)] = $this->persistColumn($alias, $value);
    +            $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($alias, $value);
             }
     
             return $resolved;
    diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
    index 806bdd93a..86527a66e 100644
    --- a/library/Icinga/Repository/RepositoryQuery.php
    +++ b/library/Icinga/Repository/RepositoryQuery.php
    @@ -28,6 +28,13 @@ class RepositoryQuery implements QueryInterface
          */
         protected $query;
     
    +    /**
    +     * The current target to be queried
    +     *
    +     * @var mixed
    +     */
    +    protected $target;
    +
         /**
          * Create a new repository query
          *
    @@ -54,14 +61,15 @@ class RepositoryQuery implements QueryInterface
          *
          * This notifies the repository about each desired query column.
          *
    -     * @param   mixed   $target     The type and purpose of this parameter depends on this query's repository
    +     * @param   mixed   $target     The target from which to fetch the columns
          * @param   array   $columns    If null or an empty array, all columns will be fetched
          *
          * @return  $this
          */
         public function from($target, array $columns = null)
         {
    -        $this->query->from($target, $this->prepareQueryColumns($columns));
    +        $this->query->from($target, $this->prepareQueryColumns($target, $columns));
    +        $this->target = $target;
             return $this;
         }
     
    @@ -86,7 +94,7 @@ class RepositoryQuery implements QueryInterface
          */
         public function columns(array $columns)
         {
    -        $this->query->columns($this->prepareQueryColumns($columns));
    +        $this->query->columns($this->prepareQueryColumns($this->target, $columns));
             return $this;
         }
     
    @@ -95,18 +103,19 @@ class RepositoryQuery implements QueryInterface
          *
          * This notifies the repository about each desired query column.
          *
    +     * @param   mixed   $target             The target where to look for each column
          * @param   array   $desiredColumns     Pass null or an empty array to require all query columns
          *
          * @return  array                       The desired columns indexed by their respective alias
          */
    -    protected function prepareQueryColumns(array $desiredColumns = null)
    +    protected function prepareQueryColumns($target, array $desiredColumns = null)
         {
             if (empty($desiredColumns)) {
    -            $columns = $this->repository->requireAllQueryColumns();
    +            $columns = $this->repository->requireAllQueryColumns($target);
             } else {
                 $columns = array();
                 foreach ($desiredColumns as $customAlias => $columnAlias) {
    -                $resolvedColumn = $this->repository->requireQueryColumn($columnAlias);
    +                $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias);
                     if ($resolvedColumn !== $columnAlias) {
                         $columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
                     } elseif (is_string($customAlias)) {
    @@ -133,7 +142,7 @@ class RepositoryQuery implements QueryInterface
         public function where($column, $value = null)
         {
             $this->query->where(
    -            $this->repository->requireFilterColumn($column),
    +            $this->repository->requireFilterColumn($this->target, $column),
                 $this->repository->persistColumn($column, $value)
             );
             return $this;
    @@ -164,7 +173,7 @@ class RepositoryQuery implements QueryInterface
          */
         public function setFilter(Filter $filter)
         {
    -        $this->repository->requireFilter($filter);
    +        $this->repository->requireFilter($this->target, $filter);
             $this->query->setFilter($filter);
             return $this;
         }
    @@ -180,7 +189,7 @@ class RepositoryQuery implements QueryInterface
          */
         public function addFilter(Filter $filter)
         {
    -        $this->repository->requireFilter($filter);
    +        $this->repository->requireFilter($this->target, $filter);
             $this->query->addFilter($filter);
             return $this;
         }
    @@ -245,7 +254,7 @@ class RepositoryQuery implements QueryInterface
     
                 try {
                     $this->query->order(
    -                    $this->repository->requireFilterColumn($column),
    +                    $this->repository->requireFilterColumn($this->target, $column),
                         $direction ? $baseDirection : ($specificDirection ?: $baseDirection)
                     );
                 } catch (QueryException $_) {
    
    From 2e0a444f13443583048910c4a5228a4214039cec Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Tue, 12 May 2015 15:49:24 +0200
    Subject: [PATCH 092/239] GroupController: Ensure that the sort and dir
     parameters are being applied
    
    refs #8826
    ---
     application/controllers/GroupController.php | 16 ++++++++++------
     1 file changed, 10 insertions(+), 6 deletions(-)
    
    diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
    index d7a208826..0f45802cb 100644
    --- a/application/controllers/GroupController.php
    +++ b/application/controllers/GroupController.php
    @@ -16,6 +16,7 @@ class GroupController extends Controller
          */
         public function init()
         {
    +        parent::init();
             $this->createTabs();
         }
     
    @@ -79,12 +80,15 @@ class GroupController extends Controller
     
             $this->setupLimitControl();
             $this->setupPaginationControl($this->view->groups);
    -        $this->setupSortControl(array(
    -            'group_name'    => $this->translate('Group'),
    -            'parent_name'   => $this->translate('Parent'),
    -            'created_at'    => $this->translate('Created at'),
    -            'last_modified' => $this->translate('Last modified')
    -        ));
    +        $this->setupSortControl(
    +            array(
    +                'group_name'    => $this->translate('Group'),
    +                'parent_name'   => $this->translate('Parent'),
    +                'created_at'    => $this->translate('Created at'),
    +                'last_modified' => $this->translate('Last modified')
    +            ),
    +            $query
    +        );
         }
     
         /**
    
    From a9f0fd0708db382fd79eaa2f965acf1a83601d83 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Tue, 12 May 2015 15:49:45 +0200
    Subject: [PATCH 093/239] UserController: Ensure that the sort and dir
     parameters are being applied
    
    refs #8826
    ---
     application/controllers/UserController.php | 16 ++++++++++------
     1 file changed, 10 insertions(+), 6 deletions(-)
    
    diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
    index 0f3a073a3..587bd4107 100644
    --- a/application/controllers/UserController.php
    +++ b/application/controllers/UserController.php
    @@ -16,6 +16,7 @@ class UserController extends Controller
          */
         public function init()
         {
    +        parent::init();
             $this->createTabs();
         }
     
    @@ -79,12 +80,15 @@ class UserController extends Controller
     
             $this->setupLimitControl();
             $this->setupPaginationControl($this->view->users);
    -        $this->setupSortControl(array(
    -            'user_name'     => $this->translate('Username'),
    -            'is_active'     => $this->translate('Active'),
    -            'created_at'    => $this->translate('Created at'),
    -            'last_modified' => $this->translate('Last modified')
    -        ));
    +        $this->setupSortControl(
    +            array(
    +                'user_name'     => $this->translate('Username'),
    +                'is_active'     => $this->translate('Active'),
    +                'created_at'    => $this->translate('Created at'),
    +                'last_modified' => $this->translate('Last modified')
    +            ),
    +            $query
    +        );
         }
     
         /**
    
    From 7d08dd27651dfea564b21f0ebccf65e5952728b2 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 09:15:18 +0200
    Subject: [PATCH 094/239] DbConnection: Adjust insert and update to support
     custom type definitions
    
    This strips the custom insert and update implementataions in
    DbUserBackend down so that it does not need to do such low level stuff...
    
    refs #8826
    ---
     .../Authentication/User/DbUserBackend.php     | 78 ++++++-------------
     library/Icinga/Data/Db/DbConnection.php       | 48 +++++++++++-
     2 files changed, 67 insertions(+), 59 deletions(-)
    
    diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php
    index 441e6cdf3..36402d862 100644
    --- a/library/Icinga/Authentication/User/DbUserBackend.php
    +++ b/library/Icinga/Authentication/User/DbUserBackend.php
    @@ -96,76 +96,44 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
          * Insert a table row with the given data
          *
          * @param   string  $table
    -     * @param   array   $data
    +     * @param   array   $bind
          */
    -    public function insert($table, array $data)
    +    public function insert($table, array $bind)
         {
    -        $newData['created_at'] = date('Y-m-d H:i:s');
    -        $newData = $this->requireStatementColumns($table, $data);
    -
    -        $values = array();
    -        foreach ($newData as $column => $_) {
    -            $values[] = ':' . $column;
    -        }
    -
    -        $sql = 'INSERT INTO '
    -            . $this->prependTablePrefix($table)
    -            . ' (' . join(', ', array_keys($newData)) . ') '
    -            . 'VALUES (' . join(', ', $values) . ')';
    -        $statement = $this->ds->getDbAdapter()->prepare($sql);
    -
    -        foreach ($newData as $column => $value) {
    -            $type = PDO::PARAM_STR;
    -            if ($column === 'password_hash') {
    -                $type = PDO::PARAM_LOB;
    -            } elseif ($column === 'active') {
    -                $type = PDO::PARAM_INT;
    -            }
    -
    -            $statement->bindValue(':' . $column, $value, $type);
    -        }
    -
    -        $statement->execute();
    +        $bind['created_at'] = date('Y-m-d H:i:s');
    +        $this->ds->insert(
    +            $this->prependTablePrefix($table),
    +            $this->requireStatementColumns($table, $bind),
    +            array(
    +                'active'        => PDO::PARAM_INT,
    +                'password_hash' => PDO::PARAM_LOB
    +            )
    +        );
         }
     
         /**
          * Update table rows with the given data, optionally limited by using a filter
          *
          * @param   string  $table
    -     * @param   array   $data
    +     * @param   array   $bind
          * @param   Filter  $filter
          */
    -    public function update($table, array $data, Filter $filter = null)
    +    public function update($table, array $bind, Filter $filter = null)
         {
    -        $newData['last_modified'] = date('Y-m-d H:i:s');
    -        $newData = $this->requireStatementColumns($table, $data);
    +        $bind['last_modified'] = date('Y-m-d H:i:s');
             if ($filter) {
                 $this->requireFilter($table, $filter);
             }
     
    -        $set = array();
    -        foreach ($newData as $column => $_) {
    -            $set[] = $column . ' = :' . $column;
    -        }
    -
    -        $sql = 'UPDATE '
    -            . $this->prependTablePrefix($table)
    -            . ' SET ' . join(', ', $set)
    -            . ($filter ? ' WHERE ' . $this->ds->renderFilter($filter) : '');
    -        $statement = $this->ds->getDbAdapter()->prepare($sql);
    -
    -        foreach ($newData as $column => $value) {
    -            $type = PDO::PARAM_STR;
    -            if ($column === 'password_hash') {
    -                $type = PDO::PARAM_LOB;
    -            } elseif ($column === 'active') {
    -                $type = PDO::PARAM_INT;
    -            }
    -
    -            $statement->bindValue(':' . $column, $value, $type);
    -        }
    -
    -        $statement->execute();
    +        $this->ds->update(
    +            $this->prependTablePrefix($table),
    +            $this->requireStatementColumns($table, $bind),
    +            $filter,
    +            array(
    +                'active'        => PDO::PARAM_INT,
    +                'password_hash' => PDO::PARAM_LOB
    +            )
    +        );
         }
     
         /**
    diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php
    index d0d2ddf98..ebe6e5743 100644
    --- a/library/Icinga/Data/Db/DbConnection.php
    +++ b/library/Icinga/Data/Db/DbConnection.php
    @@ -271,28 +271,68 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible
         /**
          * Insert a table row with the given data
          *
    +     * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
    +     * as third parameter $types to define a different type than string for a particular column.
    +     *
          * @param   string  $table
          * @param   array   $bind
    +     * @param   array   $types
          *
          * @return  int             The number of affected rows
          */
    -    public function insert($table, array $bind)
    +    public function insert($table, array $bind, array $types = array())
         {
    -        return $this->dbAdapter->insert($table, $bind);
    +        $values = array();
    +        foreach ($bind as $column => $_) {
    +            $values[] = ':' . $column;
    +        }
    +
    +        $sql = 'INSERT INTO ' . $table
    +            . ' (' . join(', ', array_keys($bind)) . ') '
    +            . 'VALUES (' . join(', ', $values) . ')';
    +        $statement = $this->dbAdapter->prepare($sql);
    +
    +        foreach ($bind as $column => $value) {
    +            $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
    +            $statement->bindValue(':' . $column, $value, $type);
    +        }
    +
    +        $statement->execute();
    +        return $statement->rowCount();
         }
     
         /**
          * Update table rows with the given data, optionally limited by using a filter
          *
    +     * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
    +     * as fourth parameter $types to define a different type than string for a particular column.
    +     *
          * @param   string  $table
          * @param   array   $bind
          * @param   Filter  $filter
    +     * @param   array   $types
          *
          * @return  int             The number of affected rows
          */
    -    public function update($table, array $bind, Filter $filter = null)
    +    public function update($table, array $bind, Filter $filter = null, array $types = array())
         {
    -        return $this->dbAdapter->update($table, $bind, $filter ? $this->renderFilter($filter) : '');
    +        $set = array();
    +        foreach ($bind as $column => $_) {
    +            $set[] = $column . ' = :' . $column;
    +        }
    +
    +        $sql = 'UPDATE ' . $table
    +            . ' SET ' . join(', ', $set)
    +            . ($filter ? ' WHERE ' . $this->renderFilter($filter) : '');
    +        $statement = $this->dbAdapter->prepare($sql);
    +
    +        foreach ($bind as $column => $value) {
    +            $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
    +            $statement->bindValue(':' . $column, $value, $type);
    +        }
    +
    +        $statement->execute();
    +        return $statement->rowCount();
         }
     
         /**
    
    From 104c1c6bba286a5a9888eebd3ef615cc757ae785 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 09:16:24 +0200
    Subject: [PATCH 095/239] DbUserBackend: Utilize Zend_Db_Select when fetching
     the password hash
    
    ---
     .../Authentication/User/DbUserBackend.php     | 19 ++++++++++---------
     1 file changed, 10 insertions(+), 9 deletions(-)
    
    diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php
    index 36402d862..362b2fb02 100644
    --- a/library/Icinga/Authentication/User/DbUserBackend.php
    +++ b/library/Icinga/Authentication/User/DbUserBackend.php
    @@ -159,18 +159,19 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
         {
             if ($this->ds->getDbType() === 'pgsql') {
                 // Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
    -            $stmt = $this->ds->getDbAdapter()->prepare(
    -                'SELECT ENCODE(password_hash, \'escape\') FROM icingaweb_user WHERE name = :name AND active = 1'
    -            );
    +            $columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
             } else {
    -            $stmt = $this->ds->getDbAdapter()->prepare(
    -                'SELECT password_hash FROM icingaweb_user WHERE name = :name AND active = 1'
    -            );
    +            $columns = array('password_hash');
             }
     
    -        $stmt->execute(array(':name' => $username));
    -        $stmt->bindColumn(1, $lob, PDO::PARAM_LOB);
    -        $stmt->fetch(PDO::FETCH_BOUND);
    +        $query = $this->ds->select()
    +            ->from($this->prependTablePrefix('user'), $columns)
    +            ->where('name', $username)
    +            ->where('active', true);
    +        $statement = $this->ds->getDbAdapter()->prepare($query->getSelectQuery());
    +        $statement->execute();
    +        $statement->bindColumn(1, $lob, PDO::PARAM_LOB);
    +        $statement->fetch(PDO::FETCH_BOUND);
             if (is_resource($lob)) {
                 $lob = stream_get_contents($lob);
             }
    
    From d5d0c67d2c62998a737a2e2114e826e41339e486 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 09:48:46 +0200
    Subject: [PATCH 096/239] IniRepository: Do not handle $target as a section's
     name
    
    That's bullshit.
    
    refs #8826
    ---
     library/Icinga/Repository/IniRepository.php | 103 +++++++++-----------
     1 file changed, 46 insertions(+), 57 deletions(-)
    
    diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
    index 6d6d24ee6..43f42e749 100644
    --- a/library/Icinga/Repository/IniRepository.php
    +++ b/library/Icinga/Repository/IniRepository.php
    @@ -4,6 +4,7 @@
     namespace Icinga\Repository;
     
     use Exception;
    +use Icinga\Application\Config;
     use Icinga\Data\Extensible;
     use Icinga\Data\Filter\Filter;
     use Icinga\Data\Updatable;
    @@ -21,11 +22,33 @@ use Icinga\Exception\StatementException;
      */
     abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
     {
    +    /**
    +     * The datasource being used
    +     *
    +     * @var Config
    +     */
    +    protected $ds;
    +
    +    /**
    +     * Create a new INI repository object
    +     *
    +     * @param   Config  $ds         The data source to use
    +     *
    +     * @throws  ProgrammingError    In case the given data source does not provide a valid key column
    +     */
    +    public function __construct(Config $ds)
    +    {
    +        parent::__construct($ds); // First! Due to init().
    +
    +        if (! $ds->getConfigObject()->getKeyColumn()) {
    +            throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
    +        }
    +    }
    +
         /**
          * Insert the given data for the given target
          *
    -     * In case the data source provides a valid key column, $data must provide a proper
    -     * value for it which is then being used as the section name instead of $target.
    +     * $data must provide a proper value for the data source's key column.
          *
          * @param   string  $target
          * @param   array   $data
    @@ -35,7 +58,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
         public function insert($target, array $data)
         {
             $newData = $this->requireStatementColumns($target, $data);
    -        $section = $this->extractSectionName($target, $newData);
    +        $section = $this->extractSectionName($newData);
     
             if ($this->ds->hasSection($section)) {
                 throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
    @@ -53,10 +76,6 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
         /**
          * Update the target with the given data and optionally limit the affected entries by using a filter
          *
    -     * The section(s) to update are either identified by $filter or $target, in order. If neither of both
    -     * is given, all sections provided by the data source are going to be updated. Uniqueness of a section's
    -     * name will be ensured.
    -     *
          * @param   string  $target
          * @param   array   $data
          * @param   Filter  $filter
    @@ -67,30 +86,20 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
         {
             $newData = $this->requireStatementColumns($target, $data);
             $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
    -        if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) {
    +        if ($filter === null && isset($newData[$keyColumn])) {
                 throw new StatementException(
                     t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
                     $keyColumn
                 );
             }
     
    -        if ($target && !$filter) {
    -            if (! $this->ds->hasSection($target)) {
    -                throw new StatementException(t('Cannot update. Section "%s" does not exist'), $target);
    -            }
    -
    -            $contents = array($target => $this->ds->getSection($target));
    -        } else {
    -            if ($filter) {
    -                $this->requireFilter($target, $filter);
    -            }
    -
    -            $contents = iterator_to_array($this->ds);
    +        if ($filter !== null) {
    +            $this->requireFilter($target, $filter);
             }
     
             $newSection = null;
    -        foreach ($contents as $section => $config) {
    -            if ($filter && !$filter->matches($config)) {
    +        foreach (iterator_to_array($this->ds) as $section => $config) {
    +            if ($filter !== null && !$filter->matches($config)) {
                     continue;
                 }
     
    @@ -102,7 +111,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
                 }
     
                 foreach ($newData as $column => $value) {
    -                if ($keyColumn && $column === $keyColumn) {
    +                if ($column === $keyColumn) {
                         $newSection = $value;
                     } else {
                         $config->$column = $value;
    @@ -130,9 +139,6 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
         /**
          * Delete entries in the given target, optionally limiting the affected entries by using a filter
          *
    -     * The section(s) to delete are either identified by $filter or $target, in order. If neither of both
    -     * is given, all sections provided by the data source are going to be deleted.
    -     *
          * @param   string  $target
          * @param   Filter  $filter
          *
    @@ -140,21 +146,13 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
          */
         public function delete($target, Filter $filter = null)
         {
    -        if ($target && !$filter) {
    -            if (! $this->ds->hasSection($target)) {
    -                return; // Nothing to do
    -            }
    +        if ($filter !== null) {
    +            $this->requireFilter($target, $filter);
    +        }
     
    -            $this->ds->removeSection($target);
    -        } else {
    -            if ($filter) {
    -                $this->requireFilter($target, $filter);
    -            }
    -
    -            foreach (iterator_to_array($this->ds) as $section => $config) {
    -                if (! $filter || $filter->matches($config)) {
    -                    $this->ds->removeSection($section);
    -                }
    +        foreach (iterator_to_array($this->ds) as $section => $config) {
    +            if ($filter === null || $filter->matches($config)) {
    +                $this->ds->removeSection($section);
                 }
             }
     
    @@ -166,32 +164,23 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
         }
     
         /**
    -     * Extract and return the section name off of the given $data, if available, or validate $target
    +     * Extract and return the section name off of the given $data
          *
    -     * @param   string  $target
          * @param   array   $data
          *
          * @return  string
          *
          * @throws  ProgrammingError    In case no valid section name is available
          */
    -    protected function extractSectionName($target, array & $data)
    +    protected function extractSectionName(array & $data)
         {
    -        if (($keyColumn = $this->ds->getConfigObject()->getKeyColumn())) {
    -            if (! isset($data[$keyColumn])) {
    -                throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn);
    -            }
    -
    -            $target = $data[$keyColumn];
    -            unset($data[$keyColumn]);
    +        $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
    +        if (! isset($data[$keyColumn])) {
    +            throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn);
             }
     
    -        if (! is_string($target)) {
    -            throw new ProgrammingError(
    -                'Neither the data source nor the $target parameter provide a valid section name'
    -            );
    -        }
    -
    -        return $target;
    +        $section = $data[$keyColumn];
    +        unset($data[$keyColumn]);
    +        return $section;
         }
     }
    
    From 7d982068a58dd2a35ce28217385297baee617efb Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 09:52:29 +0200
    Subject: [PATCH 097/239] DbRepository: Ensure that we'll work with a instance
     of DbConnection
    
    refs #8826
    ---
     library/Icinga/Repository/DbRepository.php | 18 ++++++++++++++++++
     1 file changed, 18 insertions(+)
    
    diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
    index 80788ed73..0e5a4c665 100644
    --- a/library/Icinga/Repository/DbRepository.php
    +++ b/library/Icinga/Repository/DbRepository.php
    @@ -3,6 +3,7 @@
     
     namespace Icinga\Repository;
     
    +use Icinga\Data\Db\DbConnection;
     use Icinga\Data\Extensible;
     use Icinga\Data\Filter\Filter;
     use Icinga\Data\Reducible;
    @@ -23,6 +24,13 @@ use Icinga\Exception\StatementException;
      */
     abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible
     {
    +    /**
    +     * The datasource being used
    +     *
    +     * @var DbConnection
    +     */
    +    protected $ds;
    +
         /**
          * The statement columns being provided
          *
    @@ -56,6 +64,16 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
          */
         protected $statementColumnMap;
     
    +    /**
    +     * Create a new DB repository object
    +     *
    +     * @param   DbConnection    $ds     The datasource to use
    +     */
    +    public function __construct(DbConnection $ds)
    +    {
    +        parent::__construct($ds);
    +    }
    +
         /**
          * Return the base table name this repository is responsible for
          *
    
    From 47dfcf5e1d584227c67e6f77a0a0e3fdec41b175 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 10:34:00 +0200
    Subject: [PATCH 098/239] DbUserGroupBackend: Do not use the repository
     abstraction internally
    
    That's overhead which is not necessary.
    
    refs #8826
    ---
     .../UserGroup/DbUserGroupBackend.php          | 22 +++++++++++--------
     1 file changed, 13 insertions(+), 9 deletions(-)
    
    diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    index 1e4ad395e..8275421ff 100644
    --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    @@ -102,19 +102,23 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
          */
         public function getMemberships(User $user)
         {
    +        $groupStmt = $this->ds->select()
    +            ->from($this->prependTablePrefix('group'), array('name', 'parent'))
    +            ->getSelectQuery()
    +            ->query();
             $groups = array();
    -        $groupsStmt = $this->select(array('group_name', 'parent_name'))->getQuery()->getSelectQuery()->query();
    -        foreach ($groupsStmt as $group) {
    -            $groups[$group->group_name] = $group->parent_name;
    +        foreach ($groupStmt as $group) {
    +            $groups[$group->name] = $group->parent;
             }
     
    -        $memberships = array();
    -        $membershipsStmt = $this->ds->getDbAdapter() // TODO: Use the join feature, once available
    -            ->select()
    -            ->from($this->ds->getTablePrefix() . 'group_membership', array('group_name'))
    -            ->where('username = ?', $user->getUsername())
    +        $membershipStmt = $this->ds->select() // TODO: Join this table
    +            ->from($this->prependTablePrefix('group_membership'), array('group_name'))
    +            ->where('username', $user->getUsername())
    +            ->getSelectQuery()
                 ->query();
    -        foreach ($membershipsStmt as $membership) {
    +        $memberships = array();
    +
    +        foreach ($membershipStmt as $membership) {
                 $memberships[] = $membership->group_name;
                 $parent = $groups[$membership->group_name];
                 while ($parent !== null) {
    
    From 223ecab991980c4cea0ffbe63e02f63d6d490d6d Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 10:34:39 +0200
    Subject: [PATCH 099/239] DbUserGroupBackend: Make it possible to handle
     memberships
    
    refs #8826
    ---
     .../UserGroup/DbUserGroupBackend.php               | 14 +++++++++++++-
     1 file changed, 13 insertions(+), 1 deletion(-)
    
    diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    index 8275421ff..3abe44348 100644
    --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
    @@ -22,6 +22,14 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
                 'parent_name'   => 'parent',
                 'created_at'    => 'UNIX_TIMESTAMP(ctime)',
                 'last_modified' => 'UNIX_TIMESTAMP(mtime)'
    +        ),
    +        'group_membership' => array(
    +            'group'         => 'group_name COLLATE utf8_general_ci',
    +            'group_name',
    +            'user'          => 'username COLLATE utf8_general_ci',
    +            'user_name'     => 'username',
    +            'created_at'    => 'UNIX_TIMESTAMP(ctime)',
    +            'last_modified' => 'UNIX_TIMESTAMP(mtime)'
             )
         );
     
    @@ -34,6 +42,10 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
             'group' => array(
                 'created_at'    => 'ctime',
                 'last_modified' => 'mtime'
    +        ),
    +        'group_membership' => array(
    +            'created_at'    => 'ctime',
    +            'last_modified' => 'mtime'
             )
         );
     
    @@ -42,7 +54,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
          *
          * @var array
          */
    -    protected $filterColumns = array('group', 'parent');
    +    protected $filterColumns = array('group', 'parent', 'user');
     
         /**
          * The default sort rules to be applied on a query
    
    From f93c2de6be4e04fa2117e0fbe8b0f15c0a4d9864 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 10:45:54 +0200
    Subject: [PATCH 100/239] UserGroupBackend: Disable default backend type `ini'
    
    We're not going to support this until a proper membership implementation
    exists (or is required at all).
    
    refs #8826
    ---
     library/Icinga/Authentication/UserGroup/UserGroupBackend.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
    index a6382c8e5..dd4900ea8 100644
    --- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
    +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
    @@ -21,7 +21,7 @@ class UserGroupBackend
          */
         protected static $defaultBackends = array(
             'db',
    -        'ini'
    +        //'ini'
         );
     
         /**
    
    From 0a387573f34127ae36d310c0636a7243a5ab7f2c Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 10:46:34 +0200
    Subject: [PATCH 101/239] Logger: Fix substitution of exception messages
    
    ---
     library/Icinga/Application/Logger.php     | 4 +++-
     library/Icinga/Authentication/Manager.php | 6 +++---
     2 files changed, 6 insertions(+), 4 deletions(-)
    
    diff --git a/library/Icinga/Application/Logger.php b/library/Icinga/Application/Logger.php
    index 925695dad..26241b3d5 100644
    --- a/library/Icinga/Application/Logger.php
    +++ b/library/Icinga/Application/Logger.php
    @@ -243,7 +243,9 @@ class Logger
             return vsprintf(
                 array_shift($arguments),
                 array_map(
    -                function ($a) { return is_string($a) ? $a : json_encode($a); },
    +                function ($a) {
    +                    return is_string($a) ? $a : ($a instanceof Exception ? $a->getMessage() : json_encode($a));
    +                },
                     $arguments
                 )
             );
    diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php
    index 8a150c8b0..5becbe6d3 100644
    --- a/library/Icinga/Authentication/Manager.php
    +++ b/library/Icinga/Authentication/Manager.php
    @@ -56,7 +56,7 @@ class Manager
             } catch (NotReadableError $e) {
                 Logger::error(
                     new IcingaException(
    -                    'Cannot load preferences for user "%s". An exception was thrown',
    +                    'Cannot load preferences for user "%s". An exception was thrown: %s',
                         $username,
                         $e
                     )
    @@ -74,7 +74,7 @@ class Manager
                 } catch (NotReadableError $e) {
                     Logger::error(
                         new IcingaException(
    -                        'Cannot load preferences for user "%s". An exception was thrown',
    +                        'Cannot load preferences for user "%s". An exception was thrown: %s',
                             $username,
                             $e
                         )
    @@ -92,7 +92,7 @@ class Manager
                     $groupsFromBackend = $groupBackend->getMemberships($user);
                 } catch (Exception $e) {
                     Logger::error(
    -                    'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown:',
    +                    'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
                         $username,
                         $name,
                         $e
    
    From e9fee2dad68d13d0d3ef2de97da64ee273abefdf Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 13:27:08 +0200
    Subject: [PATCH 102/239] Repository: Handle column name ambiguousness
     automatically
    
    refs #8826
    ---
     library/Icinga/Repository/DbRepository.php | 103 ++++++++++++++++++---
     library/Icinga/Repository/Repository.php   |  90 ++++++++++++------
     2 files changed, 153 insertions(+), 40 deletions(-)
    
    diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
    index 0e5a4c665..1e511abe7 100644
    --- a/library/Icinga/Repository/DbRepository.php
    +++ b/library/Icinga/Repository/DbRepository.php
    @@ -252,19 +252,66 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
          */
         protected function initializeStatementMaps()
         {
    +        $this->statementTableMap = array();
    +        $this->statementColumnMap = array();
             foreach ($this->getStatementColumns() as $table => $columns) {
                 foreach ($columns as $alias => $column) {
    -                if (! is_string($alias)) {
    -                    $this->statementTableMap[$column] = $table;
    -                    $this->statementColumnMap[$column] = $column;
    +                $key = is_string($alias) ? $alias : $column;
    +                if (array_key_exists($key, $this->statementTableMap)) {
    +                    if ($this->statementTableMap[$key] !== null) {
    +                        $existingTable = $this->statementTableMap[$key];
    +                        $existingColumn = $this->statementColumnMap[$key];
    +                        $this->statementTableMap[$existingTable . '.' . $key] = $existingTable;
    +                        $this->statementColumnMap[$existingTable . '.' . $key] = $existingColumn;
    +                        $this->statementTableMap[$key] = null;
    +                        $this->statementColumnMap[$key] = null;
    +                    }
    +
    +                    $this->statementTableMap[$table . '.' . $key] = $table;
    +                    $this->statementColumnMap[$table . '.' . $key] = $column;
                     } else {
    -                    $this->statementTableMap[$alias] = $table;
    -                    $this->statementColumnMap[$alias] = $column;
    +                    $this->statementTableMap[$key] = $table;
    +                    $this->statementColumnMap[$key] = $column;
                     }
                 }
             }
         }
     
    +    /**
    +     * Return this repository's query columns of the given table mapped to their respective aliases
    +     *
    +     * @param   mixed   $table
    +     *
    +     * @return  array
    +     *
    +     * @throws  ProgrammingError    In case $table does not exist
    +     */
    +    public function requireAllQueryColumns($table)
    +    {
    +        if (is_array($table)) {
    +            $table = array_shift($table);
    +        }
    +
    +        return parent::requireAllQueryColumns($this->removeTablePrefix($table));
    +    }
    +
    +    /**
    +     * Return the query column name for the given alias or null in case the alias does not exist
    +     *
    +     * @param   mixed   $table
    +     * @param   string  $alias
    +     *
    +     * @return  string|null
    +     */
    +    public function resolveQueryColumnAlias($table, $alias)
    +    {
    +        if (is_array($table)) {
    +            $table = array_shift($table);
    +        }
    +
    +        return parent::resolveQueryColumnAlias($this->removeTablePrefix($table), $alias);
    +    }
    +
         /**
          * Return whether the given query column name or alias is available in the given table
          *
    @@ -283,21 +330,51 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
         }
     
         /**
    -     * Return whether the given statement column name or alias is available in the given table
    +     * Return the statement column name for the given alias or null in case the alias does not exist
          *
          * @param   mixed   $table
    -     * @param   string  $column
    +     * @param   string  $alias
    +     *
    +     * @return  string|null
    +     */
    +    public function resolveStatementColumnAlias($table, $alias)
    +    {
    +        if (is_array($table)) {
    +            $table = array_shift($table);
    +        }
    +
    +        $statementColumnMap = $this->getStatementColumnMap();
    +        if (isset($statementColumnMap[$alias])) {
    +            return $statementColumnMap[$alias];
    +        }
    +
    +        $prefixedAlias = $table . '.' . $alias;
    +        if (isset($statementColumnMap[$prefixedAlias])) {
    +            return $statementColumnMap[$prefixedAlias];
    +        }
    +    }
    +
    +    /**
    +     * Return whether the given alias or statement column name is available in the given table
    +     *
    +     * @param   mixed   $table
    +     * @param   string  $alias
          *
          * @return  bool
          */
    -    public function validateStatementColumnAssociation($table, $column)
    +    public function validateStatementColumnAssociation($table, $alias)
         {
             if (is_array($table)) {
                 $table = array_shift($table);
             }
     
             $statementTableMap = $this->getStatementTableMap();
    -        return $statementTableMap[$column] === $this->removeTablePrefix($table);
    +        if (isset($statementTableMap[$alias])) {
    +            return $statementTableMap[$alias] === $this->removeTablePrefix($table);
    +        }
    +
    +        $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
    +        return isset($statementTableMap[$prefixedAlias]);
         }
     
         /**
    @@ -310,9 +387,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
          */
         public function hasStatementColumn($table, $name)
         {
    -        $statementColumnMap = $this->getStatementColumnMap();
             if (
    -            ! array_key_exists($name, $statementColumnMap)
    +            $this->resolveStatementColumnAlias($table, $name) === null
                 || !$this->validateStatementColumnAssociation($table, $name)
             ) {
                 return parent::hasStatementColumn($table, $name);
    @@ -333,8 +409,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
          */
         public function requireStatementColumn($table, $name)
         {
    -        $statementColumnMap = $this->getStatementColumnMap();
    -        if (! array_key_exists($name, $statementColumnMap)) {
    +        if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) {
                 return parent::requireStatementColumn($table, $name);
             }
     
    @@ -342,6 +417,6 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
                 throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
             }
     
    -        return $statementColumnMap[$name];
    +        return $column;
         }
     }
    diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
    index 24cd48161..58f199f1b 100644
    --- a/library/Icinga/Repository/Repository.php
    +++ b/library/Icinga/Repository/Repository.php
    @@ -362,11 +362,27 @@ abstract class Repository implements Selectable
             foreach ($queryColumns as $table => $columns) {
                 foreach ($columns as $alias => $column) {
                     if (! is_string($alias)) {
    -                    $this->aliasTableMap[$column] = $table;
    -                    $this->aliasColumnMap[$column] = $column;
    +                    $key = $column;
                     } else {
    -                    $this->aliasTableMap[$alias] = $table;
    -                    $this->aliasColumnMap[$alias] = preg_replace('~\n\s*~', ' ', $column);
    +                    $key = $alias;
    +                    $column = preg_replace('~\n\s*~', ' ', $column);
    +                }
    +
    +                if (array_key_exists($key, $this->aliasTableMap)) {
    +                    if ($this->aliasTableMap[$key] !== null) {
    +                        $existingTable = $this->aliasTableMap[$key];
    +                        $existingColumn = $this->aliasColumnMap[$key];
    +                        $this->aliasTableMap[$existingTable . '.' . $key] = $existingTable;
    +                        $this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
    +                        $this->aliasTableMap[$key] = null;
    +                        $this->aliasColumnMap[$key] = null;
    +                    }
    +
    +                    $this->aliasTableMap[$table . '.' . $key] = $table;
    +                    $this->aliasColumnMap[$table . '.' . $key] = $column;
    +                } else {
    +                    $this->aliasTableMap[$key] = $table;
    +                    $this->aliasColumnMap[$key] = $column;
                     }
                 }
             }
    @@ -598,32 +614,57 @@ abstract class Repository implements Selectable
          * @param   string  $table
          *
          * @return  array
    +     *
    +     * @throws  ProgrammingError    In case $table does not exist
          */
         public function requireAllQueryColumns($table)
         {
    -        $map = array();
    -        foreach ($this->getAliasColumnMap() as $alias => $_) {
    -            if ($this->hasQueryColumn($table, $alias)) {
    -                // Just in case $this->requireQueryColumn has been overwritten and there is some magic going on
    -                $map[$alias] = $this->requireQueryColumn($table, $alias);
    -            }
    +        $queryColumns = $this->getQueryColumns();
    +        if (! array_key_exists($table, $queryColumns)) {
    +            throw new ProgrammingError('Table name "%s" not found', $table);
             }
     
    -        return $map;
    +        return $queryColumns[$table];
         }
     
         /**
    -     * Return whether the given query column name or alias is available in the given table
    +     * Return the query column name for the given alias or null in case the alias does not exist
          *
          * @param   string  $table
    -     * @param   string  $column
    +     * @param   string  $alias
    +     *
    +     * @return  string|null
    +     */
    +    public function resolveQueryColumnAlias($table, $alias)
    +    {
    +        $aliasColumnMap = $this->getAliasColumnMap();
    +        if (isset($aliasColumnMap[$alias])) {
    +            return $aliasColumnMap[$alias];
    +        }
    +
    +        $prefixedAlias = $table . '.' . $alias;
    +        if (isset($aliasColumnMap[$prefixedAlias])) {
    +            return $aliasColumnMap[$prefixedAlias];
    +        }
    +    }
    +
    +    /**
    +     * Return whether the given alias or query column name is available in the given table
    +     *
    +     * @param   string  $table
    +     * @param   string  $alias
          *
          * @return  bool
          */
    -    public function validateQueryColumnAssociation($table, $column)
    +    public function validateQueryColumnAssociation($table, $alias)
         {
             $aliasTableMap = $this->getAliasTableMap();
    -        return $aliasTableMap[$column] === $table;
    +        if (isset($aliasTableMap[$alias])) {
    +            return $aliasTableMap[$alias] === $table;
    +        }
    +
    +        $prefixedAlias = $table . '.' . $alias;
    +        return isset($aliasTableMap[$prefixedAlias]);
         }
     
         /**
    @@ -640,7 +681,7 @@ abstract class Repository implements Selectable
                 return false;
             }
     
    -        return array_key_exists($name, $this->getAliasColumnMap())
    +        return $this->resolveQueryColumnAlias($table, $name) !== null
                 && $this->validateQueryColumnAssociation($table, $name);
         }
     
    @@ -660,8 +701,7 @@ abstract class Repository implements Selectable
                 throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
             }
     
    -        $aliasColumnMap = $this->getAliasColumnMap();
    -        if (! array_key_exists($name, $aliasColumnMap)) {
    +        if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
                 throw new QueryException(t('Query column "%s" not found'), $name);
             }
     
    @@ -669,7 +709,7 @@ abstract class Repository implements Selectable
                 throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
             }
     
    -        return $aliasColumnMap[$name];
    +        return $column;
         }
     
         /**
    @@ -682,7 +722,7 @@ abstract class Repository implements Selectable
          */
         public function hasFilterColumn($table, $name)
         {
    -        return array_key_exists($name, $this->getAliasColumnMap())
    +        return $this->resolveQueryColumnAlias($table, $name) !== null
                 && $this->validateQueryColumnAssociation($table, $name);
         }
     
    @@ -698,8 +738,7 @@ abstract class Repository implements Selectable
          */
         public function requireFilterColumn($table, $name)
         {
    -        $aliasColumnMap = $this->getAliasColumnMap();
    -        if (! array_key_exists($name, $aliasColumnMap)) {
    +        if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
                 throw new QueryException(t('Filter column "%s" not found'), $name);
             }
     
    @@ -707,7 +746,7 @@ abstract class Repository implements Selectable
                 throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
             }
     
    -        return $aliasColumnMap[$name];
    +        return $column;
         }
     
         /**
    @@ -739,8 +778,7 @@ abstract class Repository implements Selectable
                 throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
             }
     
    -        $aliasColumnMap = $this->getAliasColumnMap();
    -        if (! array_key_exists($name, $aliasColumnMap)) {
    +        if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
                 throw new StatementException('Statement column "%s" not found', $name);
             }
     
    @@ -748,7 +786,7 @@ abstract class Repository implements Selectable
                 throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
             }
     
    -        return $aliasColumnMap[$name];
    +        return $column;
         }
     
         /**
    
    From 892712126636103322a51475ecc61f5eaba040ac Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 13:50:19 +0200
    Subject: [PATCH 103/239] UserController: Behave nicely when it's not possible
     to fetch any users
    
    refs #8826
    ---
     application/controllers/UserController.php | 13 ++++++++++---
     application/views/scripts/user/list.phtml  |  5 +++++
     2 files changed, 15 insertions(+), 3 deletions(-)
    
    diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
    index 587bd4107..9c4a298e1 100644
    --- a/application/controllers/UserController.php
    +++ b/application/controllers/UserController.php
    @@ -1,12 +1,14 @@
     applyFilter($filterEditor->getFilter());
             $this->setupFilterControl($filterEditor);
     
    -        $this->getTabs()->activate('user/list');
    +        try {
    +            $this->view->users = $query->paginate();
    +            $this->setupPaginationControl($this->view->users);
    +        } catch (Exception $e) {
    +            Notification::error($e->getMessage());
    +        }
    +
             $this->view->backend = $backend;
    -        $this->view->users = $query->paginate();
    +        $this->getTabs()->activate('user/list');
     
             $this->setupLimitControl();
    -        $this->setupPaginationControl($this->view->users);
             $this->setupSortControl(
                 array(
                     'user_name'     => $this->translate('Username'),
    diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml
    index a914312c0..c7904f9b4 100644
    --- a/application/views/scripts/user/list.phtml
    +++ b/application/views/scripts/user/list.phtml
    @@ -18,6 +18,11 @@ if ($backend === null) {
         return;
     }
     
    +if (! isset($users)) {
    +    echo $this->translate('Failed to fetch any users') . '';
    +    return;
    +}
    +
     if (count($users) === 0) {
         echo $this->translate('No users found matching the filter') . '';
         return;
    
    From 5db6fc9ba9694c130b16af84fbd9634e8b6cace9 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 13:50:32 +0200
    Subject: [PATCH 104/239] GroupController: Behave nicely when it's not possible
     to fetch any groups
    
    refs #8826
    ---
     application/controllers/GroupController.php | 13 ++++++++++---
     application/views/scripts/group/list.phtml  |  5 +++++
     2 files changed, 15 insertions(+), 3 deletions(-)
    
    diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
    index 0f45802cb..bf027c836 100644
    --- a/application/controllers/GroupController.php
    +++ b/application/controllers/GroupController.php
    @@ -1,12 +1,14 @@
     applyFilter($filterEditor->getFilter());
             $this->setupFilterControl($filterEditor);
     
    -        $this->getTabs()->activate('group/list');
    +        try {
    +            $this->view->groups = $query->paginate();
    +            $this->setupPaginationControl($this->view->groups);
    +        } catch (Exception $e) {
    +            Notification::error($e->getMessage());
    +        }
    +
             $this->view->backend = $backend;
    -        $this->view->groups = $query->paginate();
    +        $this->getTabs()->activate('group/list');
     
             $this->setupLimitControl();
    -        $this->setupPaginationControl($this->view->groups);
             $this->setupSortControl(
                 array(
                     'group_name'    => $this->translate('Group'),
    diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml
    index 6e63fafdb..9b1b545d2 100644
    --- a/application/views/scripts/group/list.phtml
    +++ b/application/views/scripts/group/list.phtml
    @@ -18,6 +18,11 @@ if ($backend === null) {
         return;
     }
     
    +if (! isset($groups)) {
    +    echo $this->translate('Failed to fetch any groups') . '';
    +    return;
    +}
    +
     if (count($groups) === 0) {
         echo $this->translate('No groups found matching the filter') . '';
         return;
    
    From 07a54736169ff75e30a9a0afbe9b465c7f3186ef Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 13:58:40 +0200
    Subject: [PATCH 105/239] UserController: Since logging errors as well is
     usually a good idea, log errors
    
    refs #8826
    ---
     application/controllers/UserController.php | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
    index 9c4a298e1..487cfc920 100644
    --- a/application/controllers/UserController.php
    +++ b/application/controllers/UserController.php
    @@ -4,6 +4,7 @@
     use \Exception;
     use \Zend_Controller_Action_Exception;
     use Icinga\Application\Config;
    +use Icinga\Application\Logger;
     use Icinga\Authentication\User\UserBackend;
     use Icinga\Authentication\User\UserBackendInterface;
     use Icinga\Web\Controller;
    @@ -81,6 +82,7 @@ class UserController extends Controller
                 $this->setupPaginationControl($this->view->users);
             } catch (Exception $e) {
                 Notification::error($e->getMessage());
    +            Logger::error($e);
             }
     
             $this->view->backend = $backend;
    
    From 9eaa231c4f55ed7dbf889f5067d87bae36bd4f7b Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Wed, 13 May 2015 13:58:48 +0200
    Subject: [PATCH 106/239] GroupController: Since logging errors as well is
     usually a good idea, log errors
    
    refs #8826
    ---
     application/controllers/GroupController.php | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
    index bf027c836..bbe97b64f 100644
    --- a/application/controllers/GroupController.php
    +++ b/application/controllers/GroupController.php
    @@ -4,6 +4,7 @@
     use \Exception;
     use \Zend_Controller_Action_Exception;
     use Icinga\Application\Config;
    +use Icinga\Application\Logger;
     use Icinga\Authentication\UserGroup\UserGroupBackend;
     use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
     use Icinga\Web\Controller;
    @@ -81,6 +82,7 @@ class GroupController extends Controller
                 $this->setupPaginationControl($this->view->groups);
             } catch (Exception $e) {
                 Notification::error($e->getMessage());
    +            Logger::error($e);
             }
     
             $this->view->backend = $backend;
    
    From 130fea314687196b10c2bfe1381236c98f09327e Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 18 May 2015 11:25:02 +0200
    Subject: [PATCH 107/239] Revert "Merge Queryable into QueryInterface"
    
    This reverts commit ca5ef2da2bb9ce21701a32a668c149c0608fc8bb.
    A perfect example of a change as a result of being mentally deranged.
    ---
     library/Icinga/Data/QueryInterface.php | 2 +-
     library/Icinga/Data/SimpleQuery.php    | 7 ++++---
     2 files changed, 5 insertions(+), 4 deletions(-)
    
    diff --git a/library/Icinga/Data/QueryInterface.php b/library/Icinga/Data/QueryInterface.php
    index 7f82c8c09..4d43d1059 100644
    --- a/library/Icinga/Data/QueryInterface.php
    +++ b/library/Icinga/Data/QueryInterface.php
    @@ -5,4 +5,4 @@ namespace Icinga\Data;
     
     use Countable;
     
    -interface QueryInterface extends Queryable, Browsable, Fetchable, Filterable, Limitable, Sortable, Countable {};
    +interface QueryInterface extends Browsable, Fetchable, Filterable, Limitable, Sortable, Countable {};
    diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php
    index 875f5222a..c38cf9c53 100644
    --- a/library/Icinga/Data/SimpleQuery.php
    +++ b/library/Icinga/Data/SimpleQuery.php
    @@ -3,13 +3,14 @@
     
     namespace Icinga\Data;
     
    -use Zend_Paginator;
     use Icinga\Application\Icinga;
     use Icinga\Data\Filter\Filter;
    -use Icinga\Exception\IcingaException;
     use Icinga\Web\Paginator\Adapter\QueryAdapter;
    +use Zend_Paginator;
    +use Exception;
    +use Icinga\Exception\IcingaException;
     
    -class SimpleQuery implements QueryInterface
    +class SimpleQuery implements QueryInterface, Queryable
     {
         /**
          * Query data source
    
    From 7a6837de0e331e19d09a22a8565ab0ea2e11a0cf Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 18 May 2015 13:59:16 +0200
    Subject: [PATCH 108/239] Fetchable: Add method fetch() which returns an
     iterator
    
    ---
     library/Icinga/Data/Fetchable.php                |  9 +++++++++
     library/Icinga/Data/SimpleQuery.php              | 15 +++++++++++++--
     .../library/Monitoring/DataView/DataView.php     | 16 +++++++++++++---
     3 files changed, 35 insertions(+), 5 deletions(-)
    
    diff --git a/library/Icinga/Data/Fetchable.php b/library/Icinga/Data/Fetchable.php
    index 0992be933..2164bb1b2 100644
    --- a/library/Icinga/Data/Fetchable.php
    +++ b/library/Icinga/Data/Fetchable.php
    @@ -3,11 +3,20 @@
     
     namespace Icinga\Data;
     
    +use Iterator;
    +
     /**
      * Interface for retrieving data
      */
     interface Fetchable
     {
    +    /**
    +     * Fetch and return all rows of the result set using an iterator
    +     *
    +     * @return  Iterator
    +     */
    +    public function fetch();
    +
         /**
          * Retrieve an array containing all rows of the result set
          *
    diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php
    index 8da712598..ebcaa61a2 100644
    --- a/library/Icinga/Data/SimpleQuery.php
    +++ b/library/Icinga/Data/SimpleQuery.php
    @@ -4,6 +4,7 @@
     namespace Icinga\Data;
     
     use ArrayIterator;
    +use Iterator;
     use IteratorAggregate;
     use Icinga\Data\Filter\Filter;
     use Icinga\Exception\IcingaException;
    @@ -103,11 +104,11 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
         /**
          * Return a iterable for this query's result
          *
    -     * @return  ArrayIterator
    +     * @return  Iterator
          */
         public function getIterator()
         {
    -        return new ArrayIterator($this->fetchAll());
    +        return $this->fetch();
         }
     
         /**
    @@ -351,6 +352,16 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
             return $this->limitOffset;
         }
     
    +    /**
    +     * Fetch and return all rows of the result set using an iterator
    +     *
    +     * @return  ArrayIterator
    +     */
    +    public function fetch()
    +    {
    +        return new ArrayIterator($this->fetchAll());
    +    }
    +
         /**
          * Retrieve an array containing all rows of the result set
          *
    diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php
    index 6bed36fe1..dfbdd8a14 100644
    --- a/modules/monitoring/library/Monitoring/DataView/DataView.php
    +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php
    @@ -3,7 +3,7 @@
     
     namespace Icinga\Module\Monitoring\DataView;
     
    -use ArrayIterator;
    +use Iterator;
     use IteratorAggregate;
     use Icinga\Data\QueryInterface;
     use Icinga\Data\Filter\Filter;
    @@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate
         /**
          * Return a iterator for all rows of the result set
          *
    -     * @return  ArrayIterator
    +     * @return  Iterator
          */
         public function getIterator()
         {
    -        return new ArrayIterator($this->fetchAll());
    +        return $this->fetch();
         }
     
         /**
    @@ -469,6 +469,16 @@ abstract class DataView implements QueryInterface, IteratorAggregate
             return $this->query->hasOffset();
         }
     
    +    /**
    +     * Fetch and return all rows of the result set using an iterator
    +     *
    +     * @return  Iterator
    +     */
    +    public function fetch()
    +    {
    +        return $this->getQuery()->fetch();
    +    }
    +
         /**
          * Retrieve an array containing all rows of the result set
          *
    
    From d39c697d0e839e299143584e83ba58263e19d2e7 Mon Sep 17 00:00:00 2001
    From: Johannes Meyer 
    Date: Mon, 18 May 2015 14:01:17 +0200
    Subject: [PATCH 109/239] Repository: QueryInterface is _not_ queryable
     anymore...
    
    refs #8826
    ---
     library/Icinga/Repository/Repository.php      | 2 +-
     library/Icinga/Repository/RepositoryQuery.php | 6 ++++--
     2 files changed, 5 insertions(+), 3 deletions(-)
    
    diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
    index 58f199f1b..9a40a3d6f 100644
    --- a/library/Icinga/Repository/Repository.php
    +++ b/library/Icinga/Repository/Repository.php
    @@ -19,7 +19,7 @@ use Icinga\Util\String;
      * 
      *
    • Concrete implementations need to initialize Repository::$queryColumns
    • *
    • The datasource passed to a repository must implement the Selectable interface
    • - *
    • The datasource must yield an instance of QueryInterface when its select() method is called
    • + *
    • The datasource must yield an instance of Queryable when its select() method is called
    • *
    */ abstract class Repository implements Selectable diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 86527a66e..d70afd457 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -43,7 +43,6 @@ class RepositoryQuery implements QueryInterface public function __construct(Repository $repository) { $this->repository = $repository; - $this->query = $repository->getDataSource()->select(); } /** @@ -68,7 +67,10 @@ class RepositoryQuery implements QueryInterface */ public function from($target, array $columns = null) { - $this->query->from($target, $this->prepareQueryColumns($target, $columns)); + $this->query = $this->repository + ->getDataSource() + ->select() + ->from($target, $this->prepareQueryColumns($target, $columns)); $this->target = $target; return $this; } From 3f25cf560ed30c08770f86958ce96fd780bc5d14 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 18 May 2015 14:03:22 +0200 Subject: [PATCH 110/239] RepositoryQuery: Remove method paginate() refs #8826 --- application/controllers/GroupController.php | 4 ++-- application/controllers/UserController.php | 4 ++-- library/Icinga/Repository/RepositoryQuery.php | 18 ------------------ 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index bbe97b64f..aefcb6eac 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -78,8 +78,8 @@ class GroupController extends Controller $this->setupFilterControl($filterEditor); try { - $this->view->groups = $query->paginate(); - $this->setupPaginationControl($this->view->groups); + $this->setupPaginationControl($query); + $this->view->groups = $query; } catch (Exception $e) { Notification::error($e->getMessage()); Logger::error($e); diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 487cfc920..b88898a54 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -78,8 +78,8 @@ class UserController extends Controller $this->setupFilterControl($filterEditor); try { - $this->view->users = $query->paginate(); - $this->setupPaginationControl($this->view->users); + $this->setupPaginationControl($query); + $this->view->users = $query; } catch (Exception $e) { Notification::error($e->getMessage()); Logger::error($e); diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index d70afd457..a2f40ae8e 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -3,7 +3,6 @@ namespace Icinga\Repository; -use Zend_Paginator; use Icinga\Application\Logger; use Icinga\Data\QueryInterface; use Icinga\Data\Filter\Filter; @@ -364,23 +363,6 @@ class RepositoryQuery implements QueryInterface return $this->query->getOffset(); } - /** - * Return a paginator object for this query - * - * If not given, $itemsPerPage and $pageNumber will be set to their URL parameter counterparts. - * - * @param int $itemsPerPage Number of items per page - * @param int $pageNumber Current page number - * - * @return Zend_Paginator - */ - public function paginate($itemsPerPage = null, $pageNumber = null) - { - $paginator = $this->query->paginate($itemsPerPage, $pageNumber); - $paginator->getAdapter()->setQuery($this); - return $paginator; - } - /** * Fetch and return the first column of this query's first row * From be36809552272e42a219f301a07d261a283368a4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 18 May 2015 14:05:02 +0200 Subject: [PATCH 111/239] RepositoryQuery: Implement interface Iterator refs #8826 --- library/Icinga/Repository/RepositoryQuery.php | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index a2f40ae8e..2186573f0 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -3,6 +3,8 @@ namespace Icinga\Repository; +use Iterator; +use IteratorAggregate; use Icinga\Application\Logger; use Icinga\Data\QueryInterface; use Icinga\Data\Filter\Filter; @@ -11,7 +13,7 @@ use Icinga\Exception\QueryException; /** * Query class supposed to mediate between a repository and its datasource's query */ -class RepositoryQuery implements QueryInterface +class RepositoryQuery implements QueryInterface, Iterator { /** * The repository being used @@ -34,6 +36,13 @@ class RepositoryQuery implements QueryInterface */ protected $target; + /** + * The real query's iterator + * + * @var Iterator + */ + protected $iterator; + /** * Create a new repository query * @@ -363,6 +372,16 @@ class RepositoryQuery implements QueryInterface return $this->query->getOffset(); } + /** + * Fetch and return all rows of the result set using an iterator + * + * @return Iterator + */ + public function fetch() + { + return $this; + } + /** * Fetch and return the first column of this query's first row * @@ -503,4 +522,74 @@ class RepositoryQuery implements QueryInterface { return $this->query->count(); } + + /** + * Start or rewind the iteration + */ + public function rewind() + { + if ($this->iterator === null) { + if (! $this->hasOrder()) { + $this->order(); + } + + $iterator = $this->query->fetch(); + if ($iterator instanceof IteratorAggregate) { + $this->iterator = $iterator->getIterator(); + } else { + $this->iterator = $iterator; + } + } + + $this->iterator->rewind(); + } + + /** + * Fetch and return the current row of this query's result + * + * @return object + */ + public function current() + { + $row = $this->iterator->current(); + if ($this->repository->providesValueConversion()) { + foreach ($this->getColumns() as $alias => $column) { + if (! is_string($alias)) { + $alias = $column; + } + + $row->$alias = $this->repository->retrieveColumn($alias, $row->$alias); + } + } + + return $row; + } + + /** + * Return whether the current row of this query's result is valid + * + * @return bool + */ + public function valid() + { + return $this->iterator->valid(); + } + + /** + * Return the key for the current row of this query's result + * + * @return mixed + */ + public function key() + { + return $this->iterator->key(); + } + + /** + * Advance to the next row of this query's result + */ + public function next() + { + $this->iterator->next(); + } } From 742dfcaf41507407955d70e810245c63309c6e74 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 18 May 2015 15:17:22 +0200 Subject: [PATCH 112/239] Revert "Fetchable: Add method fetch() which returns an iterator" This reverts commit 7a6837de0e331e19d09a22a8565ab0ea2e11a0cf. --- library/Icinga/Data/Fetchable.php | 9 --------- library/Icinga/Data/SimpleQuery.php | 15 ++------------- .../library/Monitoring/DataView/DataView.php | 16 +++------------- 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/library/Icinga/Data/Fetchable.php b/library/Icinga/Data/Fetchable.php index 2164bb1b2..0992be933 100644 --- a/library/Icinga/Data/Fetchable.php +++ b/library/Icinga/Data/Fetchable.php @@ -3,20 +3,11 @@ namespace Icinga\Data; -use Iterator; - /** * Interface for retrieving data */ interface Fetchable { - /** - * Fetch and return all rows of the result set using an iterator - * - * @return Iterator - */ - public function fetch(); - /** * Retrieve an array containing all rows of the result set * diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index ebcaa61a2..8da712598 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -4,7 +4,6 @@ namespace Icinga\Data; use ArrayIterator; -use Iterator; use IteratorAggregate; use Icinga\Data\Filter\Filter; use Icinga\Exception\IcingaException; @@ -104,11 +103,11 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate /** * Return a iterable for this query's result * - * @return Iterator + * @return ArrayIterator */ public function getIterator() { - return $this->fetch(); + return new ArrayIterator($this->fetchAll()); } /** @@ -352,16 +351,6 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate return $this->limitOffset; } - /** - * Fetch and return all rows of the result set using an iterator - * - * @return ArrayIterator - */ - public function fetch() - { - return new ArrayIterator($this->fetchAll()); - } - /** * Retrieve an array containing all rows of the result set * diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index dfbdd8a14..6bed36fe1 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -3,7 +3,7 @@ namespace Icinga\Module\Monitoring\DataView; -use Iterator; +use ArrayIterator; use IteratorAggregate; use Icinga\Data\QueryInterface; use Icinga\Data\Filter\Filter; @@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate /** * Return a iterator for all rows of the result set * - * @return Iterator + * @return ArrayIterator */ public function getIterator() { - return $this->fetch(); + return new ArrayIterator($this->fetchAll()); } /** @@ -469,16 +469,6 @@ abstract class DataView implements QueryInterface, IteratorAggregate return $this->query->hasOffset(); } - /** - * Fetch and return all rows of the result set using an iterator - * - * @return Iterator - */ - public function fetch() - { - return $this->getQuery()->fetch(); - } - /** * Retrieve an array containing all rows of the result set * From 0e0341f78aa9dae06b124a091c4843f3747267a4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 18 May 2015 16:01:58 +0200 Subject: [PATCH 113/239] It's the connection which provides a cursor, not the query --- library/Icinga/Data/DataArray/ArrayDatasource.php | 13 +++++++++++++ library/Icinga/Data/Db/DbConnection.php | 13 +++++++++++++ library/Icinga/Data/SimpleQuery.php | 5 ++--- library/Icinga/Protocol/Ldap/Connection.php | 6 ++++++ library/Icinga/Repository/RepositoryQuery.php | 12 +----------- .../library/Monitoring/DataView/DataView.php | 8 ++++---- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index 11f8e59e8..ef5b4e3ed 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -3,6 +3,7 @@ namespace Icinga\Data\DataArray; +use ArrayIterator; use Icinga\Data\Selectable; use Icinga\Data\SimpleQuery; @@ -82,6 +83,18 @@ class ArrayDatasource implements Selectable return new SimpleQuery($this); } + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param SimpleQuery $query + * + * @return ArrayIterator + */ + public function query(SimpleQuery $query) + { + return new ArrayIterator($this->fetchAll($query)); + } + /** * Fetch and return a column of all rows of the result set as an array * diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index ebe6e5743..f6398831d 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -4,6 +4,7 @@ namespace Icinga\Data\Db; use PDO; +use Iterator; use Zend_Db; use Icinga\Application\Benchmark; use Icinga\Data\ConfigObject; @@ -87,6 +88,18 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible return new DbQuery($this); } + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param DbQuery $query + * + * @return Iterator + */ + public function query(DbQuery $query) + { + return $query->getSelectQuery()->query(); + } + /** * Getter for database type * diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index 8da712598..c2dd5c1d9 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -3,7 +3,6 @@ namespace Icinga\Data; -use ArrayIterator; use IteratorAggregate; use Icinga\Data\Filter\Filter; use Icinga\Exception\IcingaException; @@ -103,11 +102,11 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate /** * Return a iterable for this query's result * - * @return ArrayIterator + * @return Iterator */ public function getIterator() { - return new ArrayIterator($this->fetchAll()); + return $this->ds->query($this); } /** diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 9296c5f88..2f7cccd41 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -3,6 +3,7 @@ namespace Icinga\Protocol\Ldap; +use ArrayIterator; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Application\Platform; @@ -133,6 +134,11 @@ class Connection implements Selectable return new Query($this); } + public function query(Query $query) + { + return new ArrayIterator($this->fetchAll($query)); + } + public function fetchOne($query, $fields = array()) { $row = (array) $this->fetchRow($query, $fields); diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 2186573f0..220e85632 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -372,16 +372,6 @@ class RepositoryQuery implements QueryInterface, Iterator return $this->query->getOffset(); } - /** - * Fetch and return all rows of the result set using an iterator - * - * @return Iterator - */ - public function fetch() - { - return $this; - } - /** * Fetch and return the first column of this query's first row * @@ -533,7 +523,7 @@ class RepositoryQuery implements QueryInterface, Iterator $this->order(); } - $iterator = $this->query->fetch(); + $iterator = $this->repository->getDataSource()->query($this->query); if ($iterator instanceof IteratorAggregate) { $this->iterator = $iterator->getIterator(); } else { diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index 6bed36fe1..442e1e7c2 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -3,7 +3,6 @@ namespace Icinga\Module\Monitoring\DataView; -use ArrayIterator; use IteratorAggregate; use Icinga\Data\QueryInterface; use Icinga\Data\Filter\Filter; @@ -13,6 +12,7 @@ use Icinga\Data\ConnectionInterface; use Icinga\Exception\QueryException; use Icinga\Web\Request; use Icinga\Web\Url; +use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery; use Icinga\Module\Monitoring\Backend\MonitoringBackend; /** @@ -23,7 +23,7 @@ abstract class DataView implements QueryInterface, IteratorAggregate /** * The query used to populate the view * - * @var QueryInterface + * @var IdoQuery */ protected $query; @@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate /** * Return a iterator for all rows of the result set * - * @return ArrayIterator + * @return IdoQuery */ public function getIterator() { - return new ArrayIterator($this->fetchAll()); + return $this->getQuery(); } /** From 9af25acf3877e38e3f2ca2c365665fa5c5e801f7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 18 May 2015 16:02:55 +0200 Subject: [PATCH 114/239] RepositoryQuery: Benchmark when iterating a query's result refs #8826 --- library/Icinga/Repository/RepositoryQuery.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 220e85632..91988605c 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -5,6 +5,7 @@ namespace Icinga\Repository; use Iterator; use IteratorAggregate; +use Icinga\Application\Benchmark; use Icinga\Application\Logger; use Icinga\Data\QueryInterface; use Icinga\Data\Filter\Filter; @@ -532,6 +533,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $this->iterator->rewind(); + Benchmark::measure('Query result iteration started'); } /** @@ -562,7 +564,12 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function valid() { - return $this->iterator->valid(); + if (! $this->iterator->valid()) { + Benchmark::measure('Query result iteration finished'); + return false; + } + + return true; } /** From 7b6ca0826bf78c69807595db33325c9cfda8c364 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 19 May 2015 09:34:22 +0200 Subject: [PATCH 115/239] DbQuery: Let the DbConnection do the count query --- library/Icinga/Data/Db/DbConnection.php | 12 ++++++++++++ library/Icinga/Data/Db/DbQuery.php | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index f6398831d..9960fd491 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -212,6 +212,18 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible return $this; } + /** + * Count all rows of the result set + * + * @param DbQuery $query + * + * @return int + */ + public function count(DbQuery $query) + { + return $this->dbAdapter->fetchOne($query->getCountQuery()); + } + /** * Retrieve an array containing all rows of the result set * diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php index 45a586b9a..1149fbeec 100644 --- a/library/Icinga/Data/Db/DbQuery.php +++ b/library/Icinga/Data/Db/DbQuery.php @@ -297,7 +297,7 @@ class DbQuery extends SimpleQuery { if ($this->count === null) { Benchmark::measure('DB is counting'); - $this->count = $this->db->fetchOne($this->getCountQuery()); + $this->count = parent::count(); Benchmark::measure('DB finished count'); } return $this->count; From a1276fd709069e31f2ab3c33cdf06d0b80854ee7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 19 May 2015 09:41:18 +0200 Subject: [PATCH 116/239] Benchmark all queries by default, not only db queries --- library/Icinga/Data/Db/DbConnection.php | 11 ++------- library/Icinga/Data/Db/DbQuery.php | 5 +--- library/Icinga/Data/SimpleQuery.php | 31 ++++++++++++++++++++----- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index 9960fd491..6167a23db 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -6,7 +6,6 @@ namespace Icinga\Data\Db; use PDO; use Iterator; use Zend_Db; -use Icinga\Application\Benchmark; use Icinga\Data\ConfigObject; use Icinga\Data\Db\DbQuery; use Icinga\Data\Extensible; @@ -233,10 +232,7 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible */ public function fetchAll(DbQuery $query) { - Benchmark::measure('DB is fetching All'); - $result = $this->dbAdapter->fetchAll($query->getSelectQuery()); - Benchmark::measure('DB fetch done'); - return $result; + return $this->dbAdapter->fetchAll($query->getSelectQuery()); } /** @@ -248,10 +244,7 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible */ public function fetchRow(DbQuery $query) { - Benchmark::measure('DB is fetching row'); - $result = $this->dbAdapter->fetchRow($query->getSelectQuery()); - Benchmark::measure('DB row done'); - return $result; + return $this->dbAdapter->fetchRow($query->getSelectQuery()); } /** diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php index 1149fbeec..ee0128608 100644 --- a/library/Icinga/Data/Db/DbQuery.php +++ b/library/Icinga/Data/Db/DbQuery.php @@ -4,9 +4,7 @@ namespace Icinga\Data\Db; use Icinga\Data\SimpleQuery; -use Icinga\Application\Benchmark; use Icinga\Data\Filter\FilterChain; -use Icinga\Data\Filter\FilterExpression; use Icinga\Data\Filter\FilterOr; use Icinga\Data\Filter\FilterAnd; use Icinga\Data\Filter\FilterNot; @@ -296,10 +294,9 @@ class DbQuery extends SimpleQuery public function count() { if ($this->count === null) { - Benchmark::measure('DB is counting'); $this->count = parent::count(); - Benchmark::measure('DB finished count'); } + return $this->count; } diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index c2dd5c1d9..876e76b55 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -4,6 +4,7 @@ namespace Icinga\Data; use IteratorAggregate; +use Icinga\Application\Benchmark; use Icinga\Data\Filter\Filter; use Icinga\Exception\IcingaException; @@ -357,7 +358,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ public function fetchAll() { - return $this->ds->fetchAll($this); + Benchmark::measure('Fetching all results started'); + $results = $this->ds->fetchAll($this); + Benchmark::measure('Fetching all results finished'); + return $results; } /** @@ -367,7 +371,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ public function fetchRow() { - return $this->ds->fetchRow($this); + Benchmark::measure('Fetching one row started'); + $row = $this->ds->fetchRow($this); + Benchmark::measure('Fetching one row finished'); + return $row; } /** @@ -379,7 +386,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ public function fetchColumn($columnIndex = 0) { - return $this->ds->fetchColumn($this, $columnIndex); + Benchmark::measure('Fetching one column started'); + $values = $this->ds->fetchColumn($this, $columnIndex); + Benchmark::measure('Fetching one column finished'); + return $values; } /** @@ -389,7 +399,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ public function fetchOne() { - return $this->ds->fetchOne($this); + Benchmark::measure('Fetching one value started'); + $value = $this->ds->fetchOne($this); + Benchmark::measure('Fetching one value finished'); + return $value; } /** @@ -401,7 +414,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ public function fetchPairs() { - return $this->ds->fetchPairs($this); + Benchmark::measure('Fetching pairs started'); + $pairs = $this->ds->fetchPairs($this); + Benchmark::measure('Fetching pairs finished'); + return $pairs; } /** @@ -413,7 +429,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate { $query = clone $this; $query->limit(0, 0); - return $this->ds->count($query); + Benchmark::measure('Counting all results started'); + $count = $this->ds->count($query); + Benchmark::measure('Counting all results finished'); + return $count; } /** From cf989a0f7f51fb197bac9a92c72ba04b42cb0a94 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 19 May 2015 09:41:55 +0200 Subject: [PATCH 117/239] SimpleQuery: Implement interface Iterator to benchmark result iteration --- library/Icinga/Data/SimpleQuery.php | 81 +++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index 876e76b55..dd0a80d04 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -3,12 +3,13 @@ namespace Icinga\Data; +use Iterator; use IteratorAggregate; use Icinga\Application\Benchmark; use Icinga\Data\Filter\Filter; use Icinga\Exception\IcingaException; -class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate +class SimpleQuery implements QueryInterface, Queryable, Iterator { /** * Query data source @@ -17,6 +18,13 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ protected $ds; + /** + * This query's iterator + * + * @var Iterator + */ + protected $iterator; + /** * The target you are going to query * @@ -100,16 +108,6 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate */ protected function init() {} - /** - * Return a iterable for this query's result - * - * @return Iterator - */ - public function getIterator() - { - return $this->ds->query($this); - } - /** * Get the data source * @@ -120,6 +118,67 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate return $this->ds; } + /** + * Start or rewind the iteration + */ + public function rewind() + { + if ($this->iterator === null) { + $iterator = $this->ds->query($this); + if ($iterator instanceof IteratorAggregate) { + $this->iterator = $iterator->getIterator(); + } else { + $this->iterator = $iterator; + } + } + + $this->iterator->rewind(); + Benchmark::measure('Query result iteration started'); + } + + /** + * Fetch and return the current row of this query's result + * + * @return object + */ + public function current() + { + return $this->iterator->current(); + } + + /** + * Return whether the current row of this query's result is valid + * + * @return bool + */ + public function valid() + { + if (! $this->iterator->valid()) { + Benchmark::measure('Query result iteration finished'); + return false; + } + + return true; + } + + /** + * Return the key for the current row of this query's result + * + * @return mixed + */ + public function key() + { + return $this->iterator->key(); + } + + /** + * Advance to the next row of this query's result + */ + public function next() + { + $this->iterator->next(); + } + /** * Choose a table and the columns you are interested in * From f305a334d5ad2c293f984e77f8a98a6332ed4ebc Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 19 May 2015 09:48:20 +0200 Subject: [PATCH 118/239] DbConnection: Drop param $columnIndex in fetchColumn(), it's unused --- library/Icinga/Data/Db/DbConnection.php | 5 ++--- library/Icinga/Data/Fetchable.php | 6 ++---- library/Icinga/Data/SimpleQuery.php | 8 +++----- library/Icinga/Repository/RepositoryQuery.php | 10 ++++------ .../library/Monitoring/DataView/DataView.php | 8 +++----- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index 6167a23db..a7f933f88 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -248,14 +248,13 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible } /** - * Fetch a column of all rows of the result set as an array + * Fetch the first column of all rows of the result set as an array * * @param DbQuery $query - * @param int $columnIndex Index of the column to fetch * * @return array */ - public function fetchColumn(DbQuery $query, $columnIndex = 0) + public function fetchColumn(DbQuery $query) { return $this->dbAdapter->fetchCol($query->getSelectQuery()); } diff --git a/library/Icinga/Data/Fetchable.php b/library/Icinga/Data/Fetchable.php index 0992be933..17ab7c359 100644 --- a/library/Icinga/Data/Fetchable.php +++ b/library/Icinga/Data/Fetchable.php @@ -23,13 +23,11 @@ interface Fetchable public function fetchRow(); /** - * Fetch a column of all rows of the result set as an array - * - * @param int $columnIndex Index of the column to fetch + * Fetch the first column of all rows of the result set as an array * * @return array */ - public function fetchColumn($columnIndex = 0); + public function fetchColumn(); /** * Fetch the first column of the first row of the result set diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php index dd0a80d04..7dec095d2 100644 --- a/library/Icinga/Data/SimpleQuery.php +++ b/library/Icinga/Data/SimpleQuery.php @@ -437,16 +437,14 @@ class SimpleQuery implements QueryInterface, Queryable, Iterator } /** - * Fetch a column of all rows of the result set as an array - * - * @param int $columnIndex Index of the column to fetch + * Fetch the first column of all rows of the result set as an array * * @return array */ - public function fetchColumn($columnIndex = 0) + public function fetchColumn() { Benchmark::measure('Fetching one column started'); - $values = $this->ds->fetchColumn($this, $columnIndex); + $values = $this->ds->fetchColumn($this); Benchmark::measure('Fetching one column finished'); return $values; } diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 91988605c..8a86829fd 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -420,23 +420,21 @@ class RepositoryQuery implements QueryInterface, Iterator } /** - * Fetch and return a column of all rows of the result set as an array - * - * @param int $columnIndex Index of the column to fetch + * Fetch and return the first column of all rows of the result set as an array * * @return array */ - public function fetchColumn($columnIndex = 0) + public function fetchColumn() { if (! $this->hasOrder()) { $this->order(); } - $results = $this->query->fetchColumn($columnIndex); + $results = $this->query->fetchColumn(); if ($this->repository->providesValueConversion()) { $columns = $this->getColumns(); $aliases = array_keys($columns); - $column = is_int($aliases[$columnIndex]) ? $columns[$columnIndex] : $aliases[$columnIndex]; + $column = is_int($aliases[0]) ? $columns[0] : $aliases[0]; foreach ($results as & $value) { $value = $this->repository->retrieveColumn($column, $value); } diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index 442e1e7c2..a5d8fffe2 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -490,15 +490,13 @@ abstract class DataView implements QueryInterface, IteratorAggregate } /** - * Fetch a column of all rows of the result set as an array - * - * @param int $columnIndex Index of the column to fetch + * Fetch the first column of all rows of the result set as an array * * @return array */ - public function fetchColumn($columnIndex = 0) + public function fetchColumn() { - return $this->getQuery()->fetchColumn($columnIndex); + return $this->getQuery()->fetchColumn(); } /** From a3d5cfc28a926a345c225482ba02034f0b67e2c7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 09:11:46 +0200 Subject: [PATCH 119/239] RepositoryQuery: Properly handle queries returning no results refs #8826 --- library/Icinga/Repository/RepositoryQuery.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 8a86829fd..60848d69c 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -385,7 +385,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $result = $this->query->fetchOne(); - if ($this->repository->providesValueConversion()) { + if ($result !== false && $this->repository->providesValueConversion()) { $columns = $this->getColumns(); $column = isset($columns[0]) ? $columns[0] : key($columns); return $this->repository->retrieveColumn($column, $result); @@ -406,7 +406,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $result = $this->query->fetchRow(); - if ($this->repository->providesValueConversion()) { + if ($result !== false && $this->repository->providesValueConversion()) { foreach ($this->getColumns() as $alias => $column) { if (! is_string($alias)) { $alias = $column; @@ -431,7 +431,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchColumn(); - if ($this->repository->providesValueConversion()) { + if ($results !== false && $this->repository->providesValueConversion()) { $columns = $this->getColumns(); $aliases = array_keys($columns); $column = is_int($aliases[0]) ? $columns[0] : $aliases[0]; @@ -457,7 +457,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchPairs(); - if ($this->repository->providesValueConversion()) { + if (! empty($results) && $this->repository->providesValueConversion()) { $columns = $this->getColumns(); $aliases = array_keys($columns); $newResults = array(); @@ -486,7 +486,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchAll(); - if ($this->repository->providesValueConversion()) { + if (! empty($results) && $this->repository->providesValueConversion()) { $columns = $this->getColumns(); foreach ($results as $row) { foreach ($columns as $alias => $column) { From 093857641a8ded3ccdf88eb307cbd9ad6dd6a029 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 09:30:23 +0200 Subject: [PATCH 120/239] DbConnection: Cast a queries count to integer forcefully --- library/Icinga/Data/Db/DbConnection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index a7f933f88..1a79a0f2c 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -220,7 +220,7 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible */ public function count(DbQuery $query) { - return $this->dbAdapter->fetchOne($query->getCountQuery()); + return (int) $this->dbAdapter->fetchOne($query->getCountQuery()); } /** From 7dff1ca2b81b3dff71c365b42763d4322b9684cf Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 10:52:50 +0200 Subject: [PATCH 121/239] UserController: Do not throw Zend_Controller_Action_Exception (404) Use Controller::httpNotFound() instead. refs #8826 --- application/controllers/UserController.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index b88898a54..3481b7e8a 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -135,10 +135,7 @@ class UserController extends Controller if ($name !== null) { $config = Config::app('authentication'); if (! $config->hasSection($name)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Authentication backend "%s" not found'), $name), - 404 - ); + $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name)); } else { $backend = UserBackend::create($name, $config->getSection($name)); if ($interface && !$backend instanceof $interface) { From 8ea3cd0a133c0248fceadebf3c07e71466eed304 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 10:53:31 +0200 Subject: [PATCH 122/239] Introduce class RepositoryForm refs #8826 --- application/forms/RepositoryForm.php | 336 +++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 application/forms/RepositoryForm.php diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php new file mode 100644 index 000000000..89b8940a4 --- /dev/null +++ b/application/forms/RepositoryForm.php @@ -0,0 +1,336 @@ +repository = $repository; + return $this; + } + + /** + * Return the name of the entry to handle + * + * @return string + */ + protected function getIdentifier() + { + return $this->identifier; + } + + /** + * Return the current data of the entry being handled + * + * @return array + */ + protected function getData() + { + return $this->data; + } + + /** + * Return whether an entry should be inserted + * + * @return bool + */ + public function shouldInsert() + { + return $this->mode === self::MODE_INSERT; + } + + /** + * Return whether an entry should be udpated + * + * @return bool + */ + public function shouldUpdate() + { + return $this->mode === self::MODE_UPDATE; + } + + /** + * Return whether an entry should be deleted + * + * @return bool + */ + public function shouldDelete() + { + return $this->mode === self::MODE_DELETE; + } + + /** + * Add a new entry + * + * @param array $data The defaults to use, if any + * + * @return $this + */ + public function add(array $data = array()) + { + $this->mode = static::MODE_INSERT; + $this->data = $data; + return $this; + } + + /** + * Edit an entry + * + * @param string $name The entry's name + * @param array $data The entry's current data + * + * @return $this + */ + public function edit($name, array $data = array()) + { + $this->mode = static::MODE_UPDATE; + $this->identifier = $name; + $this->data = $data; + return $this; + } + + /** + * Remove an entry + * + * @param string $name The entry's name + * + * @return $this + */ + public function remove($name) + { + $this->mode = static::MODE_DELETE; + $this->identifier = $name; + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData The data sent by the user + */ + public function createElements(array $formData) + { + if ($this->shouldInsert()) { + $this->createInsertElements($formData); + } elseif ($this->shouldUpdate()) { + $this->createUpdateElements($formData); + } elseif ($this->shouldDelete()) { + $this->createDeleteElements($formData); + } + } + + /** + * Populate the data of the entry being handled + */ + public function onRequest() + { + $data = $this->getData(); + if (! empty($data)) { + $this->populate($data); + } + } + + /** + * Apply the requested mode on the repository + * + * @return bool + */ + public function onSuccess() + { + if ($this->shouldInsert()) { + return $this->onInsertSuccess(); + } elseif ($this->shouldUpdate()) { + return $this->onUpdateSuccess(); + } elseif ($this->shouldDelete()) { + return $this->onDeleteSuccess(); + } + } + + /** + * Apply mode insert on the repository + * + * @return bool + */ + protected function onInsertSuccess() + { + try { + $this->repository->insert( + $this->repository->getBaseTable(), + $this->getValues() + ); + } catch (Exception $e) { + Notification::error($this->getInsertMessage(false)); + $this->error($e->getMessage()); + return false; + } + + Notification::success($this->getInsertMessage(true)); + return true; + } + + /** + * Apply mode update on the repository + * + * @return bool + */ + protected function onUpdateSuccess() + { + try { + $this->repository->update( + $this->repository->getBaseTable(), + $this->getValues(), + $this->createFilter() + ); + } catch (Exception $e) { + Notification::error($this->getUpdateMessage(false)); + $this->error($e->getMessage()); + return false; + } + + Notification::success($this->getUpdateMessage(true)); + return true; + } + + /** + * Apply mode delete on the repository + * + * @return bool + */ + protected function onDeleteSuccess() + { + try { + $this->repository->delete( + $this->repository->getBaseTable(), + $this->createFilter() + ); + } catch (Exception $e) { + Notification::error($this->getDeleteMessage(false)); + $this->error($e->getMessage()); + return false; + } + + Notification::success($this->getDeleteMessage(true)); + return true; + } + + /** + * Create and add elements to this form to insert an entry + * + * @param array $formData The data sent by the user + */ + abstract protected function createInsertElements(array $formData); + + /** + * Create and add elements to this form to update an entry + * + * Calls createInsertElements() by default. Overwrite this to add different elements when in mode update. + * + * @param array $formData The data sent by the user + */ + protected function createUpdateElements(array $formData) + { + $this->createInsertElements($formData); + } + + /** + * Create and add elements to this form to delete an entry + * + * @param array $formData The data sent by the user + */ + abstract protected function createDeleteElements(array $formData); + + /** + * Create and return a filter to use when selecting, updating or deleting an entry + * + * @return Filter + */ + abstract protected function createFilter(); + + /** + * Return a notification message to use when inserting an entry + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + abstract protected function getInsertMessage($success); + + /** + * Return a notification message to use when updating an entry + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + abstract protected function getUpdateMessage($success); + + /** + * Return a notification message to use when deleting an entry + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + abstract protected function getDeleteMessage($success); +} From 32d15695201b50111ad6e9afb43079c4b8e7d2b3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 10:54:06 +0200 Subject: [PATCH 123/239] Introduce class UserForm refs #8826 --- application/forms/Config/UserForm.php | 157 ++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 application/forms/Config/UserForm.php diff --git a/application/forms/Config/UserForm.php b/application/forms/Config/UserForm.php new file mode 100644 index 000000000..fb20ab8e3 --- /dev/null +++ b/application/forms/Config/UserForm.php @@ -0,0 +1,157 @@ +addElement( + 'checkbox', + 'is_active', + array( + 'required' => true, + 'value' => true, + 'label' => $this->translate('Active'), + 'description' => $this->translate('Prevents the user from logging in if unchecked') + ) + ); + $this->addElement( + 'text', + 'user_name', + array( + 'required' => true, + 'label' => $this->translate('Username') + ) + ); + $this->addElement( + 'text', + 'password', + array( + 'required' => true, + 'label' => $this->translate('Password') + ) + ); + + $this->setTitle($this->translate('Add a new user')); + $this->setSubmitLabel($this->translate('Add')); + } + + /** + * Create and add elements to this form to update a user + * + * @param array $formData The data sent by the user + */ + protected function createUpdateElements(array $formData) + { + $this->createInsertElements($formData); + + $this->addElement( + 'text', + 'password', + array( + 'label' => $this->translate('Password') + ) + ); + + $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier())); + $this->setSubmitLabel($this->translate('Save')); + } + + /** + * Retrieve all form element values + * + * Strips off the password if null or the empty string. + * + * @param bool $suppressArrayNotation + * + * @return array + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues($suppressArrayNotation); + if (! $values['password']) { + unset($values['password']); + } + + return $values; + } + + /** + * Create and add elements to this form to delete a user + * + * @param array $formData The data sent by the user + */ + protected function createDeleteElements(array $formData) + { + $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier())); + $this->setSubmitLabel($this->translate('Yes')); + } + + /** + * Create and return a filter to use when updating or deleting a user + * + * @return Filter + */ + protected function createFilter() + { + return Filter::where('user_name', $this->getIdentifier()); + } + + /** + * Return a notification message to use when inserting a user + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + protected function getInsertMessage($success) + { + if ($success) { + return $this->translate('User added successfully'); + } else { + return $this->translate('Failed to add user'); + } + } + + /** + * Return a notification message to use when updating a user + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + protected function getUpdateMessage($success) + { + if ($success) { + return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier()); + } else { + return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier()); + } + } + + /** + * Return a notification message to use when deleting a user + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + protected function getDeleteMessage($success) + { + if ($success) { + return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier()); + } else { + return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier()); + } + } +} From 605ae3d8028c388edfaf9a39097fab12dd243ae1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 10:54:42 +0200 Subject: [PATCH 124/239] UserController: Introduce addAction, editAction and removeAction refs #8826 --- application/controllers/UserController.php | 55 ++++++++++++++++++++++ application/views/scripts/user/form.phtml | 6 +++ 2 files changed, 61 insertions(+) create mode 100644 application/views/scripts/user/form.phtml diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 3481b7e8a..03cc06b20 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -7,6 +7,7 @@ use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Authentication\User\UserBackend; use Icinga\Authentication\User\UserBackendInterface; +use Icinga\Forms\Config\UserForm; use Icinga\Web\Controller; use Icinga\Web\Form; use Icinga\Web\Notification; @@ -100,6 +101,60 @@ class UserController extends Controller ); } + /** + * Add a user + */ + public function addAction() + { + $form = new UserForm(); + $form->setRepository($this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible')); + $form->add()->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Edit a user + */ + public function editAction() + { + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); + + $row = $backend->select(array('user_name', 'is_active'))->where('user_name', $userName)->fetchRow(); + if ($row === false) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $form = new UserForm(); + $form->setRepository($backend); + $form->edit($userName, get_object_vars($row))->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Remove a user + */ + public function removeAction() + { + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); + + if ($backend->select()->where('user_name', $userName)->count() === 0) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $form = new UserForm(); + $form->setRepository($backend); + $form->remove($userName)->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + /** * Return all user backends implementing the given interface * diff --git a/application/views/scripts/user/form.phtml b/application/views/scripts/user/form.phtml new file mode 100644 index 000000000..cbf06590d --- /dev/null +++ b/application/views/scripts/user/form.phtml @@ -0,0 +1,6 @@ +
    + showOnlyCloseButton(); ?> +
    +
    + +
    \ No newline at end of file From bd07f78d94ac591c2b273aa8812c1be5a799c690 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 10:59:41 +0200 Subject: [PATCH 125/239] GroupController: Do not throw Zend_Controller_Action_Exception (404) Use Controller::httpNotFound() instead. refs #8826 --- application/controllers/GroupController.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index aefcb6eac..ffaadd76a 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -135,10 +135,7 @@ class GroupController extends Controller if ($name !== null) { $config = Config::app('groups'); if (! $config->hasSection($name)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('User group backend "%s" not found'), $name), - 404 - ); + $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name)); } else { $backend = UserGroupBackend::create($name, $config->getSection($name)); if ($interface && !$backend instanceof $interface) { From 0dda19dc7a67b023c8aa4fde8e3e13fa5dd9ac64 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 11:50:09 +0200 Subject: [PATCH 126/239] DbRepository: Remove the table prefix when resolving statement columns refs #8826 --- library/Icinga/Repository/DbRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 1e511abe7..aec2a4639 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -348,7 +348,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $statementColumnMap[$alias]; } - $prefixedAlias = $table . '.' . $alias; + $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias; if (isset($statementColumnMap[$prefixedAlias])) { return $statementColumnMap[$prefixedAlias]; } From 9c6a8898fdd98e29b05ab3e1427af5217c3fa2e4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 11:53:04 +0200 Subject: [PATCH 127/239] Introduce class UserGroupForm refs #8826 --- application/forms/Config/UserGroupForm.php | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 application/forms/Config/UserGroupForm.php diff --git a/application/forms/Config/UserGroupForm.php b/application/forms/Config/UserGroupForm.php new file mode 100644 index 000000000..80a48643f --- /dev/null +++ b/application/forms/Config/UserGroupForm.php @@ -0,0 +1,104 @@ +addElement( + 'text', + 'group_name', + array( + 'required' => true, + 'label' => $this->translate('Group Name') + ) + ); + + if ($this->shouldInsert()) { + $this->setTitle($this->translate('Add a new group')); + $this->setSubmitLabel($this->translate('Add')); + } else { // $this->shouldUpdate() + $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier())); + $this->setSubmitLabel($this->translate('Save')); + } + } + + /** + * Create and add elements to this form to delete a group + * + * @param array $formData The data sent by the user + */ + protected function createDeleteElements(array $formData) + { + $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier())); + $this->setSubmitLabel($this->translate('Yes')); + } + + /** + * Create and return a filter to use when updating or deleting a group + * + * @return Filter + */ + protected function createFilter() + { + return Filter::where('group_name', $this->getIdentifier()); + } + + /** + * Return a notification message to use when inserting a group + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + protected function getInsertMessage($success) + { + if ($success) { + return $this->translate('Group added successfully'); + } else { + return $this->translate('Failed to add group'); + } + } + + /** + * Return a notification message to use when updating a group + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + protected function getUpdateMessage($success) + { + if ($success) { + return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier()); + } else { + return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier()); + } + } + + /** + * Return a notification message to use when deleting a group + * + * @param bool $success true or false, whether the operation was successful + * + * @return string + */ + protected function getDeleteMessage($success) + { + if ($success) { + return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier()); + } else { + return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier()); + } + } +} From 539b824470c5e6c9b1b437ecf910cfcf2b1ce0de Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 11:54:28 +0200 Subject: [PATCH 128/239] GroupController: Introduce addAction, editAction and removeAction refs #8826 --- application/controllers/GroupController.php | 57 +++++++++++++++++++++ application/views/scripts/group/form.phtml | 6 +++ 2 files changed, 63 insertions(+) create mode 100644 application/views/scripts/group/form.phtml diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index ffaadd76a..7e30f7ccd 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -7,6 +7,7 @@ use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Authentication\UserGroup\UserGroupBackendInterface; +use Icinga\Forms\Config\UserGroupForm; use Icinga\Web\Controller; use Icinga\Web\Form; use Icinga\Web\Notification; @@ -100,6 +101,62 @@ class GroupController extends Controller ); } + /** + * Add a group + */ + public function addAction() + { + $form = new UserGroupForm(); + $form->setRepository( + $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible') + ); + $form->add()->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Edit a group + */ + public function editAction() + { + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); + + $row = $backend->select(array('group_name'))->where('group_name', $groupName)->fetchRow(); + if ($row === false) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $form = new UserGroupForm(); + $form->setRepository($backend); + $form->edit($groupName, get_object_vars($row))->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Remove a group + */ + public function removeAction() + { + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); + + if ($backend->select()->where('group_name', $groupName)->count() === 0) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $form = new UserGroupForm(); + $form->setRepository($backend); + $form->remove($groupName)->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + /** * Return all user group backends implementing the given interface * diff --git a/application/views/scripts/group/form.phtml b/application/views/scripts/group/form.phtml new file mode 100644 index 000000000..cbf06590d --- /dev/null +++ b/application/views/scripts/group/form.phtml @@ -0,0 +1,6 @@ +
    + showOnlyCloseButton(); ?> +
    +
    + +
    \ No newline at end of file From fecf7a52b03bcbf6d4675bfa61f10ff4c63a92b3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 13:53:19 +0200 Subject: [PATCH 129/239] UserController: Add links to add and delete users to the list action's view refs #8826 --- application/views/scripts/user/list.phtml | 36 ++++++++++++++++++++++- public/css/icinga/main-content.less | 4 +-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index c7904f9b4..5824f7681 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -1,4 +1,9 @@ -compact): ?> +compact): ?>
    tabs; ?> sortBox; ?> @@ -16,6 +21,9 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list users') . '
    '; return; +} else { + $extensible = $backend instanceof Extensible; + $reducible = $backend instanceof Reducible; } if (! isset($users)) { @@ -36,6 +44,9 @@ if (count($users) === 0) { translate('State'); ?> translate('Created at'); ?> translate('Last modified'); ?> + + translate('Remove'); ?> + @@ -51,8 +62,31 @@ if (count($users) === 0) { last_modified === null ? $this->translate('Never') : date('d/m/Y g:i A', $user->last_modified); ?> + + + qlink( + null, + 'user/remove', + array( + 'backend' => $backend->getName(), + 'user' => $user->user_name + ), + array( + 'title' => sprintf($this->translate('Remove user %s'), $user->user_name), + 'icon' => 'trash' + ) + ); ?> + + + +qlink($this->translate('Add a new user'), 'user/add', array('backend' => $backend->getName()), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'user-add' +)); ?> + \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index a3e8cb69c..ac4549d1e 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -205,7 +205,7 @@ table.benchmark { table.user-list { th { - &.user-state { + &.user-state, &.user-remove { width: 6%; padding-right: 0.5em; text-align: right; @@ -218,7 +218,7 @@ table.user-list { } } - td.user-state, td.user-created, td.user-modified { + td.user-state, td.user-created, td.user-modified, td.user-remove { text-align: right; } } From f86a05e0c332586f93afda02a01b483b8a7416a7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 13:54:05 +0200 Subject: [PATCH 130/239] UserController: Use proper redirect urls when adding and removing users refs #8826 --- application/controllers/UserController.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 03cc06b20..bb318c7f7 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -11,6 +11,7 @@ use Icinga\Forms\Config\UserForm; use Icinga\Web\Controller; use Icinga\Web\Form; use Icinga\Web\Notification; +use Icinga\Web\Url; use Icinga\Web\Widget; class UserController extends Controller @@ -106,8 +107,10 @@ class UserController extends Controller */ public function addAction() { + $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); $form = new UserForm(); - $form->setRepository($this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible')); + $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); + $form->setRepository($backend); $form->add()->handleRequest(); $this->view->form = $form; @@ -148,6 +151,7 @@ class UserController extends Controller } $form = new UserForm(); + $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); $form->setRepository($backend); $form->remove($userName)->handleRequest(); From 66611d4887f0fc801230c89fce7c9a4cbf2d796c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 14:06:59 +0200 Subject: [PATCH 131/239] GroupController: Use proper redirect urls when adding and removing groups refs #8826 --- application/controllers/GroupController.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 7e30f7ccd..4a0652e4e 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -11,6 +11,7 @@ use Icinga\Forms\Config\UserGroupForm; use Icinga\Web\Controller; use Icinga\Web\Form; use Icinga\Web\Notification; +use Icinga\Web\Url; use Icinga\Web\Widget; class GroupController extends Controller @@ -106,10 +107,10 @@ class GroupController extends Controller */ public function addAction() { + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); $form = new UserGroupForm(); - $form->setRepository( - $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible') - ); + $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); + $form->setRepository($backend); $form->add()->handleRequest(); $this->view->form = $form; @@ -150,6 +151,7 @@ class GroupController extends Controller } $form = new UserGroupForm(); + $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); $form->setRepository($backend); $form->remove($groupName)->handleRequest(); From 4a48997f479c8768ba1e390f2ea7b2cb27b97bc1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 14:07:24 +0200 Subject: [PATCH 132/239] GroupController: Add links to add and remove groups to the list action's view refs #8826 --- application/views/scripts/group/list.phtml | 48 ++++++++++++++++++---- public/css/icinga/main-content.less | 28 ++++++++----- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index 9b1b545d2..3d9afd9b4 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -1,4 +1,9 @@ -compact): ?> +compact): ?>
    tabs; ?> sortBox; ?> @@ -10,12 +15,15 @@
    -
    +
    translate('No backend found which is able to list groups') . '
    '; return; +} else { + $extensible = $backend instanceof Extensible; + $reducible = $backend instanceof Reducible; } if (! isset($groups)) { @@ -23,12 +31,7 @@ if (! isset($groups)) { return; } -if (count($groups) === 0) { - echo $this->translate('No groups found matching the filter') . '
    '; - return; -} -?> - +if (count($groups) > 0): ?> @@ -36,6 +39,9 @@ if (count($groups) === 0) { + + + @@ -51,8 +57,34 @@ if (count($groups) === 0) { + + +
    translate('Parent'); ?> translate('Created at'); ?> translate('Last modified'); ?>translate('Remove'); ?>
    last_modified === null ? $this->translate('Never') : date('d/m/Y g:i A', $group->last_modified); ?> + qlink( + null, + 'group/remove', + array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), + array( + 'title' => sprintf($this->translate('Remove group %s'), $group->group_name), + 'icon' => 'trash' + ) + ); ?> +
    + +

    translate('No groups found matching the filter'); ?>

    + + +qlink($this->translate('Add a new group'), 'group/add', array('backend' => $backend->getName()), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'group-add' +)); ?> + \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index ac4549d1e..032e7df24 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -223,23 +223,29 @@ table.user-list { } } -table.group-list { - th { - &.group-parent { - width: 6%; - padding-right: 0.5em; - text-align: right; +div.content.groups { + table.group-list { + th { + &.group-parent, &.group-remove { + width: 6%; + padding-right: 0.5em; + text-align: right; + } + + &.group-created, &.group-modified { + width: 12%; + padding-right: 0.5em; + text-align: right; + } } - &.group-created, &.group-modified { - width: 12%; - padding-right: 0.5em; + td.group-parent, td.group-created, td.group-modified, td.group-remove { text-align: right; } } - td.group-parent, td.group-created, td.group-modified { - text-align: right; + p { + margin-top: 0; } } From 9fdcadaa97620f12d72a35ba06aa96dd4be43617 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 14:13:55 +0200 Subject: [PATCH 133/239] UserController: Allow to add new users in case no users were found refs #8826 --- application/views/scripts/user/list.phtml | 12 ++++------ public/css/icinga/main-content.less | 28 ++++++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 5824f7681..1373868fe 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -15,7 +15,7 @@ if (! $this->compact): ?> -
    +
    translate('No users found matching the filter') . '
    '; - return; -} -?> - +if (count($users) > 0): ?> @@ -82,6 +77,9 @@ if (count($users) === 0) {
    + +

    translate('No users found matching the filter'); ?>

    + qlink($this->translate('Add a new user'), 'user/add', array('backend' => $backend->getName()), array( 'icon' => 'plus', diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 032e7df24..1a7c82f54 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -203,23 +203,29 @@ table.benchmark { background-color: #fbfcc5; } -table.user-list { - th { - &.user-state, &.user-remove { - width: 6%; - padding-right: 0.5em; - text-align: right; +div.content.users { + table.user-list { + th { + &.user-state, &.user-remove { + width: 6%; + padding-right: 0.5em; + text-align: right; + } + + &.user-created, &.user-modified { + width: 12%; + padding-right: 0.5em; + text-align: right; + } } - &.user-created, &.user-modified { - width: 12%; - padding-right: 0.5em; + td.user-state, td.user-created, td.user-modified, td.user-remove { text-align: right; } } - td.user-state, td.user-created, td.user-modified, td.user-remove { - text-align: right; + p { + margin-top: 0; } } From 9891dc4491df98f30ed717173efa71a4bb67bf3c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 15:00:29 +0200 Subject: [PATCH 134/239] UserController: Just show a user's name when listing users Any additional information will be shown in the detail view soon. refs #8826 --- application/controllers/UserController.php | 8 +------- application/views/scripts/user/list.phtml | 12 ----------- public/css/icinga/main-content.less | 23 ++++++++++------------ 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index bb318c7f7..c9e9dbb0d 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -64,13 +64,7 @@ class UserController extends Controller return; } - $query = $backend->select(array( - 'user_name', - 'is_active', - 'created_at', - 'last_modified' - )); - + $query = $backend->select(array('user_name')); $filterEditor = Widget::create('filterEditor') ->setQuery($query) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend') diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 1373868fe..b550d4533 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -36,9 +36,6 @@ if (count($users) > 0): ?> translate('Username'); ?> - translate('State'); ?> - translate('Created at'); ?> - translate('Last modified'); ?> translate('Remove'); ?> @@ -48,15 +45,6 @@ if (count($users) > 0): ?> escape($user->user_name); ?> - is_active === null ? $this->translate('N/A') : ( - $user->is_active ? $this->translate('Active') : $this->translate('Inactive') - ); ?> - - created_at === null ? $this->translate('N/A') : date('d/m/Y g:i A', $user->created_at); ?> - - - last_modified === null ? $this->translate('Never') : date('d/m/Y g:i A', $user->last_modified); ?> - qlink( diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 1a7c82f54..6c829cacb 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -205,21 +205,13 @@ table.benchmark { div.content.users { table.user-list { - th { - &.user-state, &.user-remove { - width: 6%; - padding-right: 0.5em; - text-align: right; - } - - &.user-created, &.user-modified { - width: 12%; - padding-right: 0.5em; - text-align: right; - } + th.user-remove { + width: 8em; + padding-right: 0.5em; + text-align: right; } - td.user-state, td.user-created, td.user-modified, td.user-remove { + td.user-remove { text-align: right; } } @@ -227,6 +219,11 @@ div.content.users { p { margin-top: 0; } + + a.user-add { + display: block; + margin-top: 1em; + } } div.content.groups { From 2cec4a6d67de02aebb01c6c992fc3479d631bc29 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 15:41:02 +0200 Subject: [PATCH 135/239] UserController: Add showAction refs #8826 --- application/controllers/UserController.php | 23 ++++++++++++++ application/views/scripts/user/list.phtml | 7 ++++- application/views/scripts/user/show.phtml | 35 ++++++++++++++++++++++ public/css/icinga/main-content.less | 15 ++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 application/views/scripts/user/show.phtml diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index c9e9dbb0d..217859574 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -96,6 +96,28 @@ class UserController extends Controller ); } + /** + * Show a user + */ + public function showAction() + { + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend')); + + $user = $backend->select(array( + 'user_name', + 'is_active', + 'created_at', + 'last_modified' + ))->where('user_name', $userName)->fetchRow(); + if ($user === false) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $this->view->user = $user; + $this->view->backend = $backend; + } + /** * Add a user */ @@ -125,6 +147,7 @@ class UserController extends Controller } $form = new UserForm(); + $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName))); $form->setRepository($backend); $form->edit($userName, get_object_vars($row))->handleRequest(); diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index b550d4533..093270d4b 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -44,7 +44,12 @@ if (count($users) > 0): ?> - escape($user->user_name); ?> + qlink($user->user_name, 'user/show', array( + 'backend' => $backend->getName(), + 'user' => $user->user_name + ), array( + 'title' => sprintf($this->translate('Show detailed information about %s'), $user->user_name) + )); ?> qlink( diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml new file mode 100644 index 000000000..00b242512 --- /dev/null +++ b/application/views/scripts/user/show.phtml @@ -0,0 +1,35 @@ +qlink( + null, + 'user/edit', + array( + 'backend' => $backend->getName(), + 'user' => $user->user_name + ), + array( + 'title' => sprintf($this->translate('Edit user %s'), $user->user_name), + 'class' => 'user-edit', + 'icon' => 'edit' + ) + ); +} + +?> +
    + compact): ?> + showOnlyCloseButton(); ?> + +
    +

    escape($user->user_name); ?>

    +

    translate('State'); ?>: is_active ? $this->translate('Active') : $this->translate('Inactive'); ?>

    +

    translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($user->created_at); ?>

    +

    translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?>

    +
    +
    +
    +
    \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 6c829cacb..82c58674c 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -226,6 +226,21 @@ div.content.users { } } +div.controls div.user-header { + border-bottom: 2px solid @colorPetrol; + + .user-name { + display: inline-block; + margin: 0 0 0.3em; + font-size: 2em; + } + + .user-state, .user-created, .user-modified { + margin: 0 0 0.2em; + font-size: 0.8em; + } +} + div.content.groups { table.group-list { th { From 94cd4b91e545c4b79a703802e2c7f05fcde28211 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 15:54:47 +0200 Subject: [PATCH 136/239] GroupController: Just show a group's name when listing groups refs #8826 --- application/controllers/GroupController.php | 8 +------ application/views/scripts/group/list.phtml | 12 ----------- public/css/icinga/main-content.less | 23 +++++++++------------ 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 4a0652e4e..45d677e50 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -64,13 +64,7 @@ class GroupController extends Controller return; } - $query = $backend->select(array( - 'group_name', - 'parent_name', - 'created_at', - 'last_modified' - )); - + $query = $backend->select(array('group_name')); $filterEditor = Widget::create('filterEditor') ->setQuery($query) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend') diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index 3d9afd9b4..05b79d5f7 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -36,9 +36,6 @@ if (count($groups) > 0): ?> translate('Group'); ?> - translate('Parent'); ?> - translate('Created at'); ?> - translate('Last modified'); ?> translate('Remove'); ?> @@ -48,15 +45,6 @@ if (count($groups) > 0): ?> escape($group->group_name); ?> - - parent_name === null ? $this->translate('None', 'user.group.parent') : $this->escape($group->parent_name); ?> - - - created_at === null ? $this->translate('N/A') : date('d/m/Y g:i A', $group->created_at); ?> - - - last_modified === null ? $this->translate('Never') : date('d/m/Y g:i A', $group->last_modified); ?> - qlink( diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 82c58674c..bea9b4a0f 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -243,21 +243,13 @@ div.controls div.user-header { div.content.groups { table.group-list { - th { - &.group-parent, &.group-remove { - width: 6%; - padding-right: 0.5em; - text-align: right; - } - - &.group-created, &.group-modified { - width: 12%; - padding-right: 0.5em; - text-align: right; - } + th.group-remove { + width: 8em; + padding-right: 0.5em; + text-align: right; } - td.group-parent, td.group-created, td.group-modified, td.group-remove { + td.group-remove { text-align: right; } } @@ -265,6 +257,11 @@ div.content.groups { p { margin-top: 0; } + + a.group-add { + display: block; + margin-top: 1em; + } } form.backend-selection { From ed166d6eb7c939ff02714219d0019cdda7a5cc9b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 20 May 2015 16:17:37 +0200 Subject: [PATCH 137/239] GroupController: Add showAction refs #8826 --- application/controllers/GroupController.php | 25 ++++++++++++ application/views/scripts/group/list.phtml | 7 +++- application/views/scripts/group/show.phtml | 45 +++++++++++++++++++++ public/css/icinga/main-content.less | 15 +++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 application/views/scripts/group/show.phtml diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 45d677e50..f0addcd51 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -96,6 +96,28 @@ class GroupController extends Controller ); } + /** + * Show a group + */ + public function showAction() + { + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend')); + + $group = $backend->select(array( + 'group_name', + 'parent_name', + 'created_at', + 'last_modified' + ))->where('group_name', $groupName)->fetchRow(); + if ($group === false) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $this->view->group = $group; + $this->view->backend = $backend; + } + /** * Add a group */ @@ -125,6 +147,9 @@ class GroupController extends Controller } $form = new UserGroupForm(); + $form->setRedirectUrl( + Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) + ); $form->setRepository($backend); $form->edit($groupName, get_object_vars($row))->handleRequest(); diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index 05b79d5f7..be2dfae1a 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -44,7 +44,12 @@ if (count($groups) > 0): ?> - escape($group->group_name); ?> + qlink($group->group_name, 'group/show', array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), array( + 'title' => sprintf($this->translate('Show detailed information for group %s'), $group->group_name) + )); ?> qlink( diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml new file mode 100644 index 000000000..302917b2a --- /dev/null +++ b/application/views/scripts/group/show.phtml @@ -0,0 +1,45 @@ +qlink( + null, + 'group/edit', + array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), + array( + 'title' => sprintf($this->translate('Edit group %s'), $group->group_name), + 'class' => 'group-edit', + 'icon' => 'edit' + ) + ); +} + +?> +
    + compact): ?> + showOnlyCloseButton(); ?> + +
    +

    escape($group->group_name); ?>

    +

    translate('Parent'); ?>: parent_name === null ? '-' : $this->qlink( + $group->parent_name, + 'group/show', + array( + 'backend' => $backend->getName(), + 'group' => $group->parent_name + ), + array( + 'title' => sprintf($this->translate('Show detailed information for group %s'), $group->parent_name) + ) + ); ?>

    +

    translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($group->created_at); ?>

    +

    translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?>

    +
    +
    +
    +
    \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index bea9b4a0f..d7082d5fd 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -264,6 +264,21 @@ div.content.groups { } } +div.controls div.group-header { + border-bottom: 2px solid @colorPetrol; + + .group-name { + display: inline-block; + margin: 0 0 0.3em; + font-size: 2em; + } + + .group-parent, .group-created, .group-modified { + margin: 0 0 0.2em; + font-size: 0.8em; + } +} + form.backend-selection { float: right; From 6369643145c484e97856dca7e7e44e153a1d9bab Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 13:51:15 +0200 Subject: [PATCH 138/239] DbUserGroupBackend: Do not sort by parent when sorting by group_name refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index 3abe44348..36aa61a1d 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -56,20 +56,6 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ protected $filterColumns = array('group', 'parent', 'user'); - /** - * The default sort rules to be applied on a query - * - * @var array - */ - protected $sortRules = array( - 'group_name' => array( - 'columns' => array( - 'group_name', - 'parent_name' - ) - ) - ); - /** * Initialize this database user group backend */ From 9278d708d7570e4c162375c6de9abd2a2c37f088 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 13:51:24 +0200 Subject: [PATCH 139/239] IniUserGroupBackend: Do not sort by parent when sorting by group_name refs #8826 --- .../UserGroup/IniUserGroupBackend.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index e70c9953c..a9d73038c 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -35,20 +35,6 @@ class IniUserGroupBackend extends IniRepository implements UserGroupBackendInter */ protected $filterColumns = array('group', 'parent'); - /** - * The default sort rules to be applied on a query - * - * @var array - */ - protected $sortRules = array( - 'group_name' => array( - 'columns' => array( - 'group_name', - 'parent_name' - ) - ) - ); - /** * The value conversion rules to apply on a query * From ba31be96952c8dc4c2daaadb7f2964e0336d2382 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 13:52:43 +0200 Subject: [PATCH 140/239] RepositoryQuery: Disim.. Fix sort order handling refs #8826 --- library/Icinga/Repository/RepositoryQuery.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 60848d69c..dc13ec68c 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -266,7 +266,9 @@ class RepositoryQuery implements QueryInterface, Iterator try { $this->query->order( $this->repository->requireFilterColumn($this->target, $column), - $direction ? $baseDirection : ($specificDirection ?: $baseDirection) + $specificDirection ?: $baseDirection + // I would have liked the following solution, but hey, a coder should be allowed to produce crap... + // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection ); } catch (QueryException $_) { Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName()); From 4d79731646d3b54c387791f41e781a2e0da40022 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 13:53:18 +0200 Subject: [PATCH 141/239] DbUserBackend: Fix sorting when sorting by user_name refs #8826 --- library/Icinga/Authentication/User/DbUserBackend.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 362b2fb02..7a3de8966 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -69,8 +69,8 @@ class DbUserBackend extends DbRepository implements UserBackendInterface protected $sortRules = array( 'user_name' => array( 'columns' => array( - 'user_name asc', - 'is_active desc' + 'is_active desc', + 'user_name' ) ) ); From 10b158a1822957b7ff2a2357fd326eb98e718309 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 13:53:27 +0200 Subject: [PATCH 142/239] LdapUserBackend: Fix sorting when sorting by user_name refs #8826 --- library/Icinga/Authentication/User/LdapUserBackend.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 7b9ffa253..7fc61dbc1 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -57,8 +57,8 @@ class LdapUserBackend extends Repository implements UserBackendInterface protected $sortRules = array( 'user_name' => array( 'columns' => array( - 'user_name asc', - 'is_active desc' + 'is_active desc', + 'user_name' ) ) ); From 4833ff109c6fcfb2dbd3b836d024ca89a0fac331 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 15:01:13 +0200 Subject: [PATCH 143/239] RepositoryQuery: Validate the table passed when calling from() refs #8826 --- library/Icinga/Repository/DbRepository.php | 34 +++++++++++-------- library/Icinga/Repository/Repository.php | 19 +++++++++++ library/Icinga/Repository/RepositoryQuery.php | 1 + 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index aec2a4639..a1fb800ee 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -74,21 +74,6 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, parent::__construct($ds); } - /** - * Return the base table name this repository is responsible for - * - * This prepends the datasource's table prefix, if available and required. - * - * @return mixed - * - * @throws ProgrammingError In case no base table name has been set and - * $this->queryColumns does not provide one either - */ - public function getBaseTable() - { - return $this->prependTablePrefix(parent::getBaseTable()); - } - /** * Return the given table with the datasource's prefix being prepended * @@ -277,6 +262,25 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, } } + /** + * Validate that the requested table exists + * + * @param string $table + * + * @return string The table's name, with the table prefix being prepended + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table) + { + $statementColumns = $this->getStatementColumns(); + if (! isset($statementColumns[$table])) { + $table = parent::requireTable($table); + } + + return $this->prependTablePrefix($table); + } + /** * Return this repository's query columns of the given table mapped to their respective aliases * diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 9a40a3d6f..8bc6517af 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -589,6 +589,25 @@ abstract class Repository implements Selectable return $value; } + /** + * Validate that the requested table exists + * + * @param string $table + * + * @return string The table's name, may differ from the given one + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table) + { + $queryColumns = $this->getQueryColumns(); + if (! isset($queryColumns[$table])) { + throw new ProgrammingError('Table "%s" not found', $table); + } + + return $table; + } + /** * Recurse the given filter, require each column for the given table and convert all values * diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index dc13ec68c..bdd51f53f 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -76,6 +76,7 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function from($target, array $columns = null) { + $target = $this->repository->requireTable($target); $this->query = $this->repository ->getDataSource() ->select() From fac2ebce8051d56144d79930bb58a220b0cbac1a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 15:02:56 +0200 Subject: [PATCH 144/239] RepositoryQuery: Fix handling of queries returning no results in fetchColumn() refs #8826 --- library/Icinga/Repository/RepositoryQuery.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index bdd51f53f..b25f8d00c 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -379,7 +379,7 @@ class RepositoryQuery implements QueryInterface, Iterator /** * Fetch and return the first column of this query's first row * - * @return mixed + * @return mixed|false False in case of no result */ public function fetchOne() { @@ -400,7 +400,7 @@ class RepositoryQuery implements QueryInterface, Iterator /** * Fetch and return the first row of this query's result * - * @return object + * @return object|false False in case of no result */ public function fetchRow() { @@ -434,7 +434,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchColumn(); - if ($results !== false && $this->repository->providesValueConversion()) { + if (! empty($results) && $this->repository->providesValueConversion()) { $columns = $this->getColumns(); $aliases = array_keys($columns); $column = is_int($aliases[0]) ? $columns[0] : $aliases[0]; From 0686bc1d86da5b90a4af72d724dcee0db647bae1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 21 May 2015 16:38:47 +0200 Subject: [PATCH 145/239] GroupController: List members when showing a group refs #8826 --- application/controllers/GroupController.php | 45 +++++++++++++++++ application/views/scripts/group/show.phtml | 55 +++++++++++++++++++-- public/css/icinga/main-content.less | 28 +++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index f0addcd51..26bcec54b 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -7,6 +7,7 @@ use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Authentication\UserGroup\UserGroupBackendInterface; +use Icinga\Data\Reducible; use Icinga\Forms\Config\UserGroupForm; use Icinga\Web\Controller; use Icinga\Web\Form; @@ -114,8 +115,52 @@ class GroupController extends Controller $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); } + $members = $backend + ->select() + ->from('group_membership', array('user_name')) + ->where('group_name', $groupName); + + $filterEditor = Widget::create('filterEditor') + ->setQuery($members) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'group') + ->ignoreParams('page') + ->handleRequest($this->getRequest()); + $members->applyFilter($filterEditor->getFilter()); + + $this->setupFilterControl($filterEditor); + $this->setupPaginationControl($members); + $this->setupLimitControl(); + $this->setupSortControl( + array( + 'user_name' => $this->translate('Username'), + 'created_at' => $this->translate('Created at'), + 'last_modified' => $this->translate('Last modified') + ), + $members + ); + $this->view->group = $group; $this->view->backend = $backend; + $this->view->members = $members; + + if ($backend instanceof Reducible) { + $removeForm = new Form(); + $removeForm->setName('removemember'); + $removeForm->setAction( + Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName)) + ); + $removeForm->addElement('hidden', 'user_name', array('decorators' => array('ViewHelper'))); + $removeForm->addElement('button', 'btn_submit', array( + 'escape' => false, + 'type' => 'submit', + 'class' => 'link-like', + 'value' => 'btn_submit', + 'decorators' => array('ViewHelper'), + 'label' => $this->view->icon('trash'), + 'title' => $this->translate('Remove this member') + )); + $this->view->removeForm = $removeForm; + } } /** diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 302917b2a..2b783796c 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -1,7 +1,10 @@ qlink( @@ -21,9 +24,9 @@ if ($backend instanceof Updatable) { ?>
    - compact): ?> - showOnlyCloseButton(); ?> - + compact): ?> + showOnlyCloseButton(); ?> +

    escape($group->group_name); ?>

    translate('Parent'); ?>: parent_name === null ? '-' : $this->qlink( @@ -40,6 +43,50 @@ if ($backend instanceof Updatable) {

    translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($group->created_at); ?>

    translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?>

    + compact): ?> + sortBox; ?> + + limiter; ?> + paginator; ?> + compact): ?> + filterEditor; ?> +
    -
    +
    + 0): ?> + + + + + + + + + + + + + + + + + + + +
    translate('Username'); ?>translate('Remove'); ?>
    escape($member->user_name); ?> + getElement('user_name')->setValue($member->user_name); echo $removeForm; ?> +
    + +

    translate('No group member found matching the filter'); ?>

    + + + qlink($this->translate('Add a new member'), 'group/addmember', array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'member-add' + )); ?> +
    \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index d7082d5fd..603466f67 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -266,6 +266,7 @@ div.content.groups { div.controls div.group-header { border-bottom: 2px solid @colorPetrol; + margin-bottom: 1em; .group-name { display: inline-block; @@ -279,6 +280,33 @@ div.controls div.group-header { } } +div.content.members { + table.member-list { + th.member-remove { + width: 8em; + padding-right: 0.5em; + text-align: right; + } + + td.member-remove { + text-align: right; + + form button.link-like { + color: inherit; + } + } + } + + p { + margin-top: 0; + } + + a.member-add { + display: block; + margin-top: 1em; + } +} + form.backend-selection { float: right; From 59ff4221f615af6d3ef8caa0a30210d479f2b1d8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 14:01:11 +0200 Subject: [PATCH 146/239] CommandTransport: Make sure to reset the config before calling current() --- .../Monitoring/Command/Transport/CommandTransport.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php index a9839565b..f39b0b4c3 100644 --- a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php +++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php @@ -108,7 +108,8 @@ abstract class CommandTransport */ public static function first() { - $config = self::getConfig()->current(); - return self::fromConfig($config); + $config = self::getConfig(); + $config->rewind(); + return self::fromConfig($config->current()); } } From 4f0b4e55efd236f955fe216750a6b4adfb6ade99 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 14:01:26 +0200 Subject: [PATCH 147/239] ErrorLabeller: Use an element's name if no label is set --- library/Icinga/Web/Form/ErrorLabeller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Web/Form/ErrorLabeller.php b/library/Icinga/Web/Form/ErrorLabeller.php index 455f5f81e..f66260149 100644 --- a/library/Icinga/Web/Form/ErrorLabeller.php +++ b/library/Icinga/Web/Form/ErrorLabeller.php @@ -39,7 +39,7 @@ class ErrorLabeller extends Zend_Translate_Adapter protected function createMessages($element) { - $label = $element->getLabel(); + $label = $element->getLabel() ?: $element->getName(); return array( Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label), From 030db8c8da3b252adbca2eae55cb4a6bc43c692f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 14:35:34 +0200 Subject: [PATCH 148/239] GroupController: Add removememberAction() refs #8826 --- application/controllers/GroupController.php | 64 ++++++++++++++++++++- application/views/scripts/group/show.phtml | 2 +- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 26bcec54b..e4a9f6bb7 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -8,6 +8,7 @@ use Icinga\Application\Logger; use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Authentication\UserGroup\UserGroupBackendInterface; use Icinga\Data\Reducible; +use Icinga\Data\Filter\Filter; use Icinga\Forms\Config\UserGroupForm; use Icinga\Web\Controller; use Icinga\Web\Form; @@ -145,11 +146,21 @@ class GroupController extends Controller if ($backend instanceof Reducible) { $removeForm = new Form(); - $removeForm->setName('removemember'); + $removeForm->setUidDisabled(); $removeForm->setAction( Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName)) ); - $removeForm->addElement('hidden', 'user_name', array('decorators' => array('ViewHelper'))); + $removeForm->addElement('hidden', 'user_name', array( + 'isArray' => true, + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('hidden', 'redirect', array( + 'value' => Url::fromPath('group/show', array( + 'backend' => $backend->getName(), + 'group' => $groupName + )), + 'decorators' => array('ViewHelper') + )); $removeForm->addElement('button', 'btn_submit', array( 'escape' => false, 'type' => 'submit', @@ -223,6 +234,55 @@ class GroupController extends Controller $this->render('form'); } + /** + * Remove a group member + */ + public function removememberAction() + { + $this->assertHttpMethod('POST'); + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); + + if ($backend->select()->where('group_name', $groupName)->count() === 0) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $form = new Form(array( + 'onSuccess' => function ($form) use ($groupName, $backend) { + foreach ($form->getValue('user_name') as $userName) { + try { + $backend->delete( + 'group_membership', + Filter::matchAll( + Filter::where('group_name', $groupName), + Filter::where('user_name', $userName) + ) + ); + Notification::success(sprintf( + t('User "%s" has been removed from group "%s"'), + $userName, + $groupName + )); + } catch (Exception $e) { + Notification::error($e->getMessage()); + } + } + + $redirect = $form->getValue('redirect'); + if (! empty($redirect)) { + $form->setRedirectUrl(htmlspecialchars_decode($redirect)); + } + + return true; + } + )); + $form->setUidDisabled(); + $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called + $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true)); + $form->addElement('hidden', 'redirect'); + $form->handleRequest(); + } + /** * Return all user group backends implementing the given interface * diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 2b783796c..647f31eab 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -68,7 +68,7 @@ if ($backend instanceof Updatable) { escape($member->user_name); ?> - + getElement('user_name')->setValue($member->user_name); echo $removeForm; ?> From 705bb665a520ea7b161b7c51b69c97221bb27cce Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 15:53:47 +0200 Subject: [PATCH 149/239] UserController: List memberships when showing a user refs #8826 --- application/controllers/UserController.php | 71 ++++++++++++++++++++++ application/views/scripts/user/show.phtml | 60 ++++++++++++++++-- public/css/icinga/main-content.less | 28 +++++++++ 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 217859574..3fe3dd604 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -7,7 +7,10 @@ use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Authentication\User\UserBackend; use Icinga\Authentication\User\UserBackendInterface; +use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Forms\Config\UserForm; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\User; use Icinga\Web\Controller; use Icinga\Web\Form; use Icinga\Web\Notification; @@ -114,8 +117,53 @@ class UserController extends Controller $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); } + $memberships = $this->loadMemberships(new User($userName))->select(); + + $filterEditor = Widget::create('filterEditor') + ->setQuery($memberships) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'user') + ->ignoreParams('page') + ->handleRequest($this->getRequest()); + $memberships->applyFilter($filterEditor->getFilter()); + + $this->setupFilterControl($filterEditor); + $this->setupPaginationControl($memberships); + $this->setupLimitControl(); + $this->setupSortControl( + array( + 'group_name' => $this->translate('Group') + ), + $memberships + ); + $this->view->user = $user; $this->view->backend = $backend; + $this->view->memberships = $memberships; + + $removeForm = new Form(); + $removeForm->setUidDisabled(); + $removeForm->addElement('hidden', 'user_name', array( + 'isArray' => true, + 'value' => $userName, + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('hidden', 'redirect', array( + 'value' => Url::fromPath('user/show', array( + 'backend' => $backend->getName(), + 'user' => $userName + )), + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('button', 'btn_submit', array( + 'escape' => false, + 'type' => 'submit', + 'class' => 'link-like', + 'value' => 'btn_submit', + 'decorators' => array('ViewHelper'), + 'label' => $this->view->icon('trash'), + 'title' => $this->translate('Cancel this membership') + )); + $this->view->removeForm = $removeForm; } /** @@ -176,6 +224,29 @@ class UserController extends Controller $this->render('form'); } + /** + * Fetch and return the given user's groups from all user group backends + * + * @param User $user + * + * @return ArrayDatasource + */ + protected function loadMemberships(User $user) + { + $groups = array(); + foreach (Config::app('groups') as $backendName => $backendConfig) { + $backend = UserGroupBackend::create($backendName, $backendConfig); + foreach ($backend->getMemberships($user) as $groupName) { + $groups[] = (object) array( + 'group_name' => $groupName, + 'backend' => $backend + ); + } + } + + return new ArrayDatasource($groups); + } + /** * Return all user backends implementing the given interface * diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 00b242512..cd8801b1a 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -1,6 +1,7 @@ -
    - compact): ?> - showOnlyCloseButton(); ?> - + compact): ?> + showOnlyCloseButton(); ?> +

    escape($user->user_name); ?>

    translate('State'); ?>: is_active ? $this->translate('Active') : $this->translate('Inactive'); ?>

    translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($user->created_at); ?>

    translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?>

    + compact): ?> + sortBox; ?> + + limiter; ?> + paginator; ?> + compact): ?> + filterEditor; ?> +
    -
    +
    + 0): ?> + + + + + + + + + + + + + + + +
    translate('Group'); ?>translate('Cancel', 'group.membership'); ?>
    qlink($membership->group_name, 'group/show', array( + 'backend' => $membership->backend->getName(), + 'group' => $membership->group_name + ), array( + 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name) + )); ?> + backend instanceof Reducible): ?> + setAction($this->url('group/removemember', array( + 'backend' => $membership->backend->getName(), + 'group' => $membership->group_name + ))); ?> + + - + +
    + +

    translate('No memberships found matching the filter'); ?>

    + +qlink($this->translate('Create new membership'), 'user/createmembership', array( + 'user' => $user->user_name +), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'membership-create' +)); ?>
    \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 603466f67..4d5c4c896 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -228,6 +228,7 @@ div.content.users { div.controls div.user-header { border-bottom: 2px solid @colorPetrol; + margin-bottom: 1em; .user-name { display: inline-block; @@ -241,6 +242,33 @@ div.controls div.user-header { } } +div.content.memberships { + table.membership-list { + th.membership-cancel { + width: 8em; + padding-right: 0.5em; + text-align: right; + } + + td.membership-cancel { + text-align: right; + + form button.link-like { + color: inherit; + } + } + } + + p { + margin-top: 0; + } + + a.membership-create { + display: block; + margin-top: 1em; + } +} + div.content.groups { table.group-list { th.group-remove { From 5c6d5f51c422226bfac1246c00d83570ad0aec4a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 16:13:20 +0200 Subject: [PATCH 150/239] UserController: Display a tab when showing a user refs #8826 --- application/controllers/UserController.php | 44 +++++++++++++++------- application/views/scripts/user/show.phtml | 2 +- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 3fe3dd604..bd7534c7d 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -19,15 +19,6 @@ use Icinga\Web\Widget; class UserController extends Controller { - /** - * Initialize this controller - */ - public function init() - { - parent::init(); - $this->createTabs(); - } - /** * Redirect to this controller's list action */ @@ -85,7 +76,7 @@ class UserController extends Controller } $this->view->backend = $backend; - $this->getTabs()->activate('user/list'); + $this->createListTabs()->activate('user/list'); $this->setupLimitControl(); $this->setupSortControl( @@ -139,6 +130,7 @@ class UserController extends Controller $this->view->user = $user; $this->view->backend = $backend; $this->view->memberships = $memberships; + $this->createShowTabs($backend->getName(), $userName)->activate('user/show'); $removeForm = new Form(); $removeForm->setUidDisabled(); @@ -306,9 +298,9 @@ class UserController extends Controller } /** - * Create the tabs + * Create the tabs to list users and groups */ - protected function createTabs() + protected function createListTabs() { $tabs = $this->getTabs(); $tabs->add( @@ -316,7 +308,7 @@ class UserController extends Controller array( 'title' => $this->translate('List users of authentication backends'), 'label' => $this->translate('Users'), - 'icon' => 'users', + 'icon' => 'user', 'url' => 'user/list' ) ); @@ -325,9 +317,33 @@ class UserController extends Controller array( 'title' => $this->translate('List groups of user group backends'), 'label' => $this->translate('Groups'), - 'icon' => 'cubes', + 'icon' => 'users', 'url' => 'group/list' ) ); + + return $tabs; + } + + /** + * Create the tabs to display when showing a user + * + * @param string $backendName + * @param string $userName + */ + protected function createShowTabs($backendName, $userName) + { + $tabs = $this->getTabs(); + $tabs->add( + 'user/show', + array( + 'title' => sprintf($this->translate('Show user %s'), $userName), + 'label' => $this->translate('User'), + 'icon' => 'user', + 'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName)) + ) + ); + + return $tabs; } } diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index cd8801b1a..733d3309a 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -23,7 +23,7 @@ if ($backend instanceof Updatable) { ?>
    compact): ?> - showOnlyCloseButton(); ?> +

    escape($user->user_name); ?>

    From e5819ef1b2cc4428461308bd342d3d7c95895440 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 16:13:38 +0200 Subject: [PATCH 151/239] GroupController: Display a tab when showing a group refs #8826 --- application/controllers/GroupController.php | 44 ++++++++++++++------- application/views/scripts/group/show.phtml | 2 +- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index e4a9f6bb7..ddb4d1e84 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -18,15 +18,6 @@ use Icinga\Web\Widget; class GroupController extends Controller { - /** - * Initialize this controller - */ - public function init() - { - parent::init(); - $this->createTabs(); - } - /** * Redirect to this controller's list action */ @@ -84,7 +75,7 @@ class GroupController extends Controller } $this->view->backend = $backend; - $this->getTabs()->activate('group/list'); + $this->createListTabs()->activate('group/list'); $this->setupLimitControl(); $this->setupSortControl( @@ -143,6 +134,7 @@ class GroupController extends Controller $this->view->group = $group; $this->view->backend = $backend; $this->view->members = $members; + $this->createShowTabs($backend->getName(), $groupName)->activate('group/show'); if ($backend instanceof Reducible) { $removeForm = new Form(); @@ -342,9 +334,9 @@ class GroupController extends Controller } /** - * Create the tabs + * Create the tabs to list users and groups */ - protected function createTabs() + protected function createListTabs() { $tabs = $this->getTabs(); $tabs->add( @@ -352,7 +344,7 @@ class GroupController extends Controller array( 'title' => $this->translate('List users of authentication backends'), 'label' => $this->translate('Users'), - 'icon' => 'users', + 'icon' => 'user', 'url' => 'user/list' ) ); @@ -361,9 +353,33 @@ class GroupController extends Controller array( 'title' => $this->translate('List groups of user group backends'), 'label' => $this->translate('Groups'), - 'icon' => 'cubes', + 'icon' => 'users', 'url' => 'group/list' ) ); + + return $tabs; + } + + /** + * Create the tabs to display when showing a group + * + * @param string $backendName + * @param string $groupName + */ + protected function createShowTabs($backendName, $groupName) + { + $tabs = $this->getTabs(); + $tabs->add( + 'group/show', + array( + 'title' => sprintf($this->translate('Show group %s'), $groupName), + 'label' => $this->translate('Group'), + 'icon' => 'users', + 'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName)) + ) + ); + + return $tabs; } } diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 647f31eab..718dde255 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -25,7 +25,7 @@ if ($backend instanceof Updatable) { ?>
    compact): ?> - showOnlyCloseButton(); ?> +

    escape($group->group_name); ?>

    From 88f5bb83689b92289c06d9a7e4bdc326ea271a92 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 16:54:59 +0200 Subject: [PATCH 152/239] js: Relax check whether to switch to single column layout upon redirect This fixes the problem when redirecting from a form using the same route as in the leftmost column but with a different querystring, that one gets the same route shown twice. --- public/js/icinga/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 99667ac1c..082b41e87 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -310,7 +310,7 @@ } else { if (req.$target.attr('id') === 'col2') { // TODO: multicol - if ($('#col1').data('icingaUrl') === redirect) { + if ($('#col1').data('icingaUrl').split('?')[0] === redirect.split('?')[0]) { icinga.ui.layout1col(); req.$target = $('#col1'); delete(this.requests['col2']); From 18e413d15a82936396f6aa8ca82bdc934b03f727 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 17:01:34 +0200 Subject: [PATCH 153/239] UserForm: Fix redirect when renaming a user refs #8826 --- application/forms/Config/UserForm.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/application/forms/Config/UserForm.php b/application/forms/Config/UserForm.php index fb20ab8e3..cf3b7f083 100644 --- a/application/forms/Config/UserForm.php +++ b/application/forms/Config/UserForm.php @@ -67,6 +67,24 @@ class UserForm extends RepositoryForm $this->setSubmitLabel($this->translate('Save')); } + /** + * Update a user + * + * @return bool + */ + protected function onUpdateSuccess() + { + if (parent::onUpdateSuccess()) { + if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) { + $this->getRedirectUrl()->setParam('user', $newName); + } + + return true; + } + + return false; + } + /** * Retrieve all form element values * From f3124ffd591a3eb285be9e6d45df6fea360ee54f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 22 May 2015 17:03:02 +0200 Subject: [PATCH 154/239] UserGroupForm: Fix redirect when renaming a group refs #8826 --- application/forms/Config/UserGroupForm.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/application/forms/Config/UserGroupForm.php b/application/forms/Config/UserGroupForm.php index 80a48643f..b2ac015e4 100644 --- a/application/forms/Config/UserGroupForm.php +++ b/application/forms/Config/UserGroupForm.php @@ -33,6 +33,24 @@ class UserGroupForm extends RepositoryForm } } + /** + * Update a group + * + * @return bool + */ + protected function onUpdateSuccess() + { + if (parent::onUpdateSuccess()) { + if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) { + $this->getRedirectUrl()->setParam('group', $newName); + } + + return true; + } + + return false; + } + /** * Create and add elements to this form to delete a group * From 54354b17bf2d2af545dac57b79e3ff325d890726 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 09:26:55 +0200 Subject: [PATCH 155/239] DbConnection: Replicate the fix for #9211 --- library/Icinga/Data/Db/DbConnection.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index 1a79a0f2c..471905907 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -397,11 +397,15 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible } } - if ($level > 0) { - $where .= ' (' . implode($operator, $parts) . ') '; - } else { - $where .= implode($operator, $parts); + if (! empty($parts)) { + if ($level > 0) { + $where .= ' (' . implode($operator, $parts) . ') '; + } else { + $where .= implode($operator, $parts); + } } + } else { + return ''; // Explicitly return the empty string due to the FilterNot case } } else { $where .= $this->renderFilterExpression($filter); From 20f0b46574ebc0e12214e21f309aaf244d549477 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 10:11:40 +0200 Subject: [PATCH 156/239] Introduce class AuthBackendController refs #8826 --- .../Web/Controller/AuthBackendController.php | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 library/Icinga/Web/Controller/AuthBackendController.php diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php new file mode 100644 index 000000000..1f93de1c7 --- /dev/null +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -0,0 +1,162 @@ + $backendConfig) { + $candidate = UserBackend::create($backendName, $backendConfig); + if (! $interface || $candidate instanceof $interface) { + $backends[] = $candidate; + } + } + + return $backends; + } + + /** + * Return the given user backend or the first match in order + * + * @param string $name The name of the backend, or null in case the first match should be returned + * @param string $interface The interface the backend should implement, no interface check if null + * + * @return UserBackendInterface + * + * @throws Zend_Controller_Action_Exception In case the given backend name is invalid + */ + protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable') + { + if ($name !== null) { + $config = Config::app('authentication'); + if (! $config->hasSection($name)) { + $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name)); + } else { + $backend = UserBackend::create($name, $config->getSection($name)); + if ($interface && !$backend instanceof $interface) { + $interfaceParts = explode('\\', strtolower($interface)); + throw new Zend_Controller_Action_Exception( + sprintf( + $this->translate('Authentication backend "%s" is not %s'), + $name, + array_pop($interfaceParts) + ), + 400 + ); + } + } + } else { + $backends = $this->loadUserBackends($interface); + $backend = array_shift($backends); + } + + return $backend; + } + + /** + * Return all user group backends implementing the given interface + * + * @param string $interface The class path of the interface, or null if no interface check should be made + * + * @return array + */ + protected function loadUserGroupBackends($interface = null) + { + $backends = array(); + foreach (Config::app('groups') as $backendName => $backendConfig) { + $candidate = UserGroupBackend::create($backendName, $backendConfig); + if (! $interface || $candidate instanceof $interface) { + $backends[] = $candidate; + } + } + + return $backends; + } + + /** + * Return the given user group backend or the first match in order + * + * @param string $name The name of the backend, or null in case the first match should be returned + * @param string $interface The interface the backend should implement, no interface check if null + * + * @return UserGroupBackendInterface + * + * @throws Zend_Controller_Action_Exception In case the given backend name is invalid + */ + protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable') + { + if ($name !== null) { + $config = Config::app('groups'); + if (! $config->hasSection($name)) { + $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name)); + } else { + $backend = UserGroupBackend::create($name, $config->getSection($name)); + if ($interface && !$backend instanceof $interface) { + $interfaceParts = explode('\\', strtolower($interface)); + throw new Zend_Controller_Action_Exception( + sprintf( + $this->translate('User group backend "%s" is not %s'), + $name, + array_pop($interfaceParts) + ), + 400 + ); + } + } + } else { + $backends = $this->loadUserGroupBackends($interface); + $backend = array_shift($backends); + } + + return $backend; + } + + /** + * Create the tabs to list users and groups + */ + protected function createListTabs() + { + $tabs = $this->getTabs(); + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'icon' => 'user', + 'url' => 'user/list' + ) + ); + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('Groups'), + 'icon' => 'users', + 'url' => 'group/list' + ) + ); + + return $tabs; + } +} From a4f38f23348485fab3070e3c486e6a9b7187361b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 10:12:10 +0200 Subject: [PATCH 157/239] UserController: Extend AuthBackendController refs #8826 --- application/controllers/UserController.php | 98 +--------------------- 1 file changed, 3 insertions(+), 95 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index bd7534c7d..6dc54abde 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -2,22 +2,17 @@ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ use \Exception; -use \Zend_Controller_Action_Exception; -use Icinga\Application\Config; use Icinga\Application\Logger; -use Icinga\Authentication\User\UserBackend; -use Icinga\Authentication\User\UserBackendInterface; -use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Forms\Config\UserForm; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\User; -use Icinga\Web\Controller; +use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Form; use Icinga\Web\Notification; use Icinga\Web\Url; use Icinga\Web\Widget; -class UserController extends Controller +class UserController extends AuthBackendController { /** * Redirect to this controller's list action @@ -226,8 +221,7 @@ class UserController extends Controller protected function loadMemberships(User $user) { $groups = array(); - foreach (Config::app('groups') as $backendName => $backendConfig) { - $backend = UserGroupBackend::create($backendName, $backendConfig); + foreach ($this->loadUserGroupBackends() as $backend) { foreach ($backend->getMemberships($user) as $groupName) { $groups[] = (object) array( 'group_name' => $groupName, @@ -239,92 +233,6 @@ class UserController extends Controller return new ArrayDatasource($groups); } - /** - * Return all user backends implementing the given interface - * - * @param string $interface The class path of the interface, or null if no interface check should be made - * - * @return array - */ - protected function loadUserBackends($interface = null) - { - $backends = array(); - foreach (Config::app('authentication') as $backendName => $backendConfig) { - $candidate = UserBackend::create($backendName, $backendConfig); - if (! $interface || $candidate instanceof $interface) { - $backends[] = $candidate; - } - } - - return $backends; - } - - /** - * Return the given user backend or the first match in order - * - * @param string $name The name of the backend, or null in case the first match should be returned - * @param string $interface The interface the backend should implement, no interface check if null - * - * @return UserBackendInterface - * - * @throws Zend_Controller_Action_Exception In case the given backend name is invalid - */ - protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable') - { - if ($name !== null) { - $config = Config::app('authentication'); - if (! $config->hasSection($name)) { - $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name)); - } else { - $backend = UserBackend::create($name, $config->getSection($name)); - if ($interface && !$backend instanceof $interface) { - $interfaceParts = explode('\\', strtolower($interface)); - throw new Zend_Controller_Action_Exception( - sprintf( - $this->translate('Authentication backend "%s" is not %s'), - $name, - array_pop($interfaceParts) - ), - 400 - ); - } - } - } else { - $backends = $this->loadUserBackends($interface); - $backend = array_shift($backends); - } - - return $backend; - } - - /** - * Create the tabs to list users and groups - */ - protected function createListTabs() - { - $tabs = $this->getTabs(); - $tabs->add( - 'user/list', - array( - 'title' => $this->translate('List users of authentication backends'), - 'label' => $this->translate('Users'), - 'icon' => 'user', - 'url' => 'user/list' - ) - ); - $tabs->add( - 'group/list', - array( - 'title' => $this->translate('List groups of user group backends'), - 'label' => $this->translate('Groups'), - 'icon' => 'users', - 'url' => 'group/list' - ) - ); - - return $tabs; - } - /** * Create the tabs to display when showing a user * From 2164fcf4b4944f74a940f57e7ad39e4dbc156c6f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 10:12:25 +0200 Subject: [PATCH 158/239] GroupController: Extend AuthBackendController refs #8826 --- application/controllers/GroupController.php | 94 +-------------------- 1 file changed, 2 insertions(+), 92 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index ddb4d1e84..eb5d5df8f 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -2,21 +2,17 @@ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ use \Exception; -use \Zend_Controller_Action_Exception; -use Icinga\Application\Config; use Icinga\Application\Logger; -use Icinga\Authentication\UserGroup\UserGroupBackend; -use Icinga\Authentication\UserGroup\UserGroupBackendInterface; use Icinga\Data\Reducible; use Icinga\Data\Filter\Filter; use Icinga\Forms\Config\UserGroupForm; -use Icinga\Web\Controller; +use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Form; use Icinga\Web\Notification; use Icinga\Web\Url; use Icinga\Web\Widget; -class GroupController extends Controller +class GroupController extends AuthBackendController { /** * Redirect to this controller's list action @@ -275,92 +271,6 @@ class GroupController extends Controller $form->handleRequest(); } - /** - * Return all user group backends implementing the given interface - * - * @param string $interface The class path of the interface, or null if no interface check should be made - * - * @return array - */ - protected function loadUserGroupBackends($interface = null) - { - $backends = array(); - foreach (Config::app('groups') as $backendName => $backendConfig) { - $candidate = UserGroupBackend::create($backendName, $backendConfig); - if (! $interface || $candidate instanceof $interface) { - $backends[] = $candidate; - } - } - - return $backends; - } - - /** - * Return the given user group backend or the first match in order - * - * @param string $name The name of the backend, or null in case the first match should be returned - * @param string $interface The interface the backend should implement, no interface check if null - * - * @return UserGroupBackendInterface - * - * @throws Zend_Controller_Action_Exception In case the given backend name is invalid - */ - protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable') - { - if ($name !== null) { - $config = Config::app('groups'); - if (! $config->hasSection($name)) { - $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name)); - } else { - $backend = UserGroupBackend::create($name, $config->getSection($name)); - if ($interface && !$backend instanceof $interface) { - $interfaceParts = explode('\\', strtolower($interface)); - throw new Zend_Controller_Action_Exception( - sprintf( - $this->translate('User group backend "%s" is not %s'), - $name, - array_pop($interfaceParts) - ), - 400 - ); - } - } - } else { - $backends = $this->loadUserGroupBackends($interface); - $backend = array_shift($backends); - } - - return $backend; - } - - /** - * Create the tabs to list users and groups - */ - protected function createListTabs() - { - $tabs = $this->getTabs(); - $tabs->add( - 'user/list', - array( - 'title' => $this->translate('List users of authentication backends'), - 'label' => $this->translate('Users'), - 'icon' => 'user', - 'url' => 'user/list' - ) - ); - $tabs->add( - 'group/list', - array( - 'title' => $this->translate('List groups of user group backends'), - 'label' => $this->translate('Groups'), - 'icon' => 'users', - 'url' => 'group/list' - ) - ); - - return $tabs; - } - /** * Create the tabs to display when showing a group * From e2c250ca77fab5606f621decd9b2c82245a436e8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 10:23:40 +0200 Subject: [PATCH 159/239] Move UserForm to the Icinga\Forms\Config\User namespace refs #8826 --- application/controllers/UserController.php | 2 +- application/forms/Config/{ => User}/UserForm.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename application/forms/Config/{ => User}/UserForm.php (99%) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 6dc54abde..b24f3b76e 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -3,7 +3,7 @@ use \Exception; use Icinga\Application\Logger; -use Icinga\Forms\Config\UserForm; +use Icinga\Forms\Config\User\UserForm; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\User; use Icinga\Web\Controller\AuthBackendController; diff --git a/application/forms/Config/UserForm.php b/application/forms/Config/User/UserForm.php similarity index 99% rename from application/forms/Config/UserForm.php rename to application/forms/Config/User/UserForm.php index cf3b7f083..765b95882 100644 --- a/application/forms/Config/UserForm.php +++ b/application/forms/Config/User/UserForm.php @@ -1,7 +1,7 @@ Date: Tue, 26 May 2015 10:24:13 +0200 Subject: [PATCH 160/239] Move UserGroupForm to the Icinga\Forms\Config\UserGroup namespace refs #8826 --- application/controllers/GroupController.php | 2 +- application/forms/Config/{ => UserGroup}/UserGroupForm.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename application/forms/Config/{ => UserGroup}/UserGroupForm.php (98%) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index eb5d5df8f..4d5aa3cc3 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -5,7 +5,7 @@ use \Exception; use Icinga\Application\Logger; use Icinga\Data\Reducible; use Icinga\Data\Filter\Filter; -use Icinga\Forms\Config\UserGroupForm; +use Icinga\Forms\Config\UserGroup\UserGroupForm; use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Form; use Icinga\Web\Notification; diff --git a/application/forms/Config/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php similarity index 98% rename from application/forms/Config/UserGroupForm.php rename to application/forms/Config/UserGroup/UserGroupForm.php index b2ac015e4..eb3c2846d 100644 --- a/application/forms/Config/UserGroupForm.php +++ b/application/forms/Config/UserGroup/UserGroupForm.php @@ -1,7 +1,7 @@ Date: Tue, 26 May 2015 14:30:55 +0200 Subject: [PATCH 161/239] Introduce form AddMemberForm refs #8826 --- .../forms/Config/UserGroup/AddMemberForm.php | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 application/forms/Config/UserGroup/AddMemberForm.php diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php new file mode 100644 index 000000000..6a57df3b4 --- /dev/null +++ b/application/forms/Config/UserGroup/AddMemberForm.php @@ -0,0 +1,179 @@ +ds = $ds; + return $this; + } + + /** + * Set the user group backend to use + * + * @param Extensible $backend + * + * @return $this + */ + public function setBackend(Extensible $backend) + { + $this->backend = $backend; + return $this; + } + + /** + * Set the group to add members for + * + * @param string $groupName + * + * @return $this + */ + public function setGroupName($groupName) + { + $this->groupName = $groupName; + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData The data sent by the user + */ + public function createElements(array $formData) + { + // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate + // memberships (no matter whether the data source permits it or not, a member does never need to be + // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does + // not work currently as our ldap protocol stuff is unable to handle our filter implementation.. + $members = $this->backend + ->select() + ->from('group_membership', array('user_name')) + ->where('group_name', $this->groupName) + ->fetchColumn(); + $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members)); + + $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn(); + if (! empty($users)) { + $this->addElement( + 'multiselect', + 'user_name', + array( + 'multiOptions' => array_combine($users, $users), + 'label' => $this->translate('Backend Users'), + 'description' => $this->translate( + 'Select one or more users (fetched from your user backends) to add as group member' + ), + 'class' => 'grant-permissions' + ) + ); + } + + $this->addElement( + 'textarea', + 'users', + array( + 'required' => empty($users), + 'label' => $this->translate('Users'), + 'description' => $this->translate( + 'Provide one or more usernames separated by comma to add as group member' + ) + ) + ); + + $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName)); + $this->setSubmitLabel($this->translate('Add')); + } + + /** + * Insert the members for the group + * + * @return bool + */ + public function onSuccess() + { + $userNames = $this->getValue('user_name') ?: array(); + if (($users = $this->getValue('users'))) { + $userNames = array_merge($userNames, array_map('trim', explode(',', $users))); + } + + if (empty($userNames)) { + $this->info($this->translate( + 'Please provide at least one username, either by choosing one ' + . 'in the list or by manually typing one in the text box below' + )); + return false; + } + + $single = null; + foreach ($userNames as $userName) { + try { + $this->backend->insert( + 'group_membership', + array( + 'group_name' => $this->groupName, + 'user_name' => $userName + ) + ); + } catch (Exception $e) { + Notification::error(sprintf( + $this->translate('Failed to add "%s" as group member for "%s"'), + $userName, + $this->groupName + )); + $this->error($e->getMessage()); + return false; + } + + $single = $single === null; + } + + if ($single) { + Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName)); + } else { + Notification::success($this->translate('Group members added successfully')); + } + + return true; + } +} From 899a00e98339da557140a8132d3c81fbb4955a6f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 14:32:14 +0200 Subject: [PATCH 162/239] GroupController: Add addmemberAction() refs #8826 --- application/controllers/GroupController.php | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 4d5aa3cc3..3704c038e 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -3,8 +3,10 @@ use \Exception; use Icinga\Application\Logger; +use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Data\Reducible; use Icinga\Data\Filter\Filter; +use Icinga\Forms\Config\UserGroup\AddMemberForm; use Icinga\Forms\Config\UserGroup\UserGroupForm; use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Form; @@ -222,6 +224,32 @@ class GroupController extends AuthBackendController $this->render('form'); } + /** + * Add a group member + */ + public function addmemberAction() + { + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); + + if ($backend->select()->where('group_name', $groupName)->count() === 0) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $form = new AddMemberForm(); + $form->setDataSource($this->fetchUsers()) + ->setBackend($backend) + ->setGroupName($groupName) + ->setRedirectUrl( + Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) + ) + ->setUidDisabled() + ->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + /** * Remove a group member */ @@ -271,6 +299,23 @@ class GroupController extends AuthBackendController $form->handleRequest(); } + /** + * Fetch and return all users from all user backends + * + * @return ArrayDatasource + */ + protected function fetchUsers() + { + $users = array(); + foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) { + foreach ($backend->select(array('user_name')) as $row) { + $users[] = $row; + } + } + + return new ArrayDatasource($users); + } + /** * Create the tabs to display when showing a group * From 237b50f953f7e4a139ef60b6ad7b5186d9beb91a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 14:32:47 +0200 Subject: [PATCH 163/239] RepositoryQuery: Ensure that we'll adjust a copy of a filter --- library/Icinga/Repository/RepositoryQuery.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index b25f8d00c..4d87e5026 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -185,6 +185,7 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function setFilter(Filter $filter) { + $filter = clone $filter; $this->repository->requireFilter($this->target, $filter); $this->query->setFilter($filter); return $this; @@ -201,6 +202,7 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function addFilter(Filter $filter) { + $filter = clone $filter; $this->repository->requireFilter($this->target, $filter); $this->query->addFilter($filter); return $this; From 6e382bd1270dff31b73e1307607d3c135e65e873 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 16:53:47 +0200 Subject: [PATCH 164/239] Provide a css rule for informational notifications aka Notification::info() --- public/css/icinga/main-content.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index 4d5c4c896..e1a305509 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -41,6 +41,10 @@ img.icon { background-position: 1em center; } +#notifications > li.info { + background-color: @colorFormNotificationInfo; +} + #notifications > li.warning { background-color: @colorWarningHandled; } From a75c74eae1868d5bc8ee21e090499220863d56c3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 17:02:27 +0200 Subject: [PATCH 165/239] Introduce form CreateMembershipForm refs #8826 --- .../Config/User/CreateMembershipForm.php | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 application/forms/Config/User/CreateMembershipForm.php diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php new file mode 100644 index 000000000..a957edf6a --- /dev/null +++ b/application/forms/Config/User/CreateMembershipForm.php @@ -0,0 +1,173 @@ +backends = $backends; + return $this; + } + + /** + * Set the username to create memberships for + * + * @param string $userName + * + * @return $this + */ + public function setUsername($userName) + { + $this->userName = $userName; + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData The data sent by the user + */ + public function createElements(array $formData) + { + $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name')); + + $options = array(); + foreach ($query as $row) { + $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')'; + } + + $this->addElement( + 'multiselect', + 'groups', + array( + 'required' => true, + 'multiOptions' => $options, + 'label' => $this->translate('Groups'), + 'description' => sprintf( + $this->translate('Select one or more groups where to add %s as member'), + $this->userName + ), + 'class' => 'grant-permissions' + ) + ); + + $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName)); + $this->setSubmitLabel($this->translate('Create')); + } + + /** + * Instantly redirect back in case the user is already a member of all groups + */ + public function onRequest() + { + if ($this->createDataSource()->select()->from('group')->count() === 0) { + Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName)); + $this->getResponse()->redirectAndExit($this->getRedirectUrl()); + } + } + + /** + * Create the memberships for the user + * + * @return bool + */ + public function onSuccess() + { + $backendMap = array(); + foreach ($this->backends as $backend) { + $backendMap[$backend->getName()] = $backend; + } + + $single = null; + foreach ($this->getValue('groups') as $backendAndGroup) { + list($backendName, $groupName) = explode(';', $backendAndGroup, 2); + try { + $backendMap[$backendName]->insert( + 'group_membership', + array( + 'group_name' => $groupName, + 'user_name' => $this->userName + ) + ); + } catch (Exception $e) { + Notification::error(sprintf( + $this->translate('Failed to add "%s" as group member for "%s"'), + $this->userName, + $groupName + )); + $this->error($e->getMessage()); + return false; + } + + $single = $single === null; + } + + if ($single) { + Notification::success( + sprintf($this->translate('Membership for group %s created successfully'), $groupName) + ); + } else { + Notification::success($this->translate('Memberships created successfully')); + } + + return true; + } + + /** + * Create and return a data source to fetch all groups from all backends where the user is not already a member of + * + * @return ArrayDatasource + */ + protected function createDataSource() + { + $groups = array(); + foreach ($this->backends as $backend) { + $memberships = $backend + ->select() + ->from('group_membership', array('group_name')) + ->where('user_name', $this->userName) + ->fetchColumn(); + foreach ($backend->select(array('group_name')) as $row) { + if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter + $row->backend_name = $backend->getName(); + $groups[] = $row; + } + } + } + + return new ArrayDatasource($groups); + } +} From 02afa9fd554ecd6e18d0a3522553e8044e34786a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 26 May 2015 17:03:10 +0200 Subject: [PATCH 166/239] UserController: Add createmembershipAction() refs #8826 --- application/controllers/UserController.php | 34 ++++++++++++++++++++++ application/views/scripts/user/show.phtml | 11 ++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index b24f3b76e..90979a3a7 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -3,6 +3,8 @@ use \Exception; use Icinga\Application\Logger; +use Icinga\Exception\ConfigurationError; +use Icinga\Forms\Config\User\CreateMembershipForm; use Icinga\Forms\Config\User\UserForm; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\User; @@ -122,6 +124,9 @@ class UserController extends AuthBackendController $memberships ); + $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); + $this->view->showCreateMembershipLink = ! empty($extensibleBackends); + $this->view->user = $user; $this->view->backend = $backend; $this->view->memberships = $memberships; @@ -211,6 +216,35 @@ class UserController extends AuthBackendController $this->render('form'); } + /** + * Create a membership for a user + */ + public function createmembershipAction() + { + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend')); + + if ($backend->select()->where('user_name', $userName)->count() === 0) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $backends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); + if (empty($backends)) { + throw new ConfigurationError($this->translate( + 'You\'ll need to configure at least one user group backend first that allows to create new memberships' + )); + } + + $form = new CreateMembershipForm(); + $form->setBackends($backends) + ->setUsername($userName) + ->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName))) + ->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + /** * Fetch and return the given user's groups from all user group backends * diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 733d3309a..1049a426a 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -75,11 +75,14 @@ if ($backend instanceof Updatable) {

    translate('No memberships found matching the filter'); ?>

    + qlink($this->translate('Create new membership'), 'user/createmembership', array( - 'user' => $user->user_name + 'backend' => $backend->getName(), + 'user' => $user->user_name ), array( - 'icon' => 'plus', - 'data-base-target' => '_next', - 'class' => 'membership-create' + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'membership-create' )); ?> +
    \ No newline at end of file From e0f0fbf1cc2e3b661ffa3f15bea8400f68e97b9c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 08:53:13 +0200 Subject: [PATCH 167/239] GroupController: Remove redundant error handling refs #8826 --- application/controllers/GroupController.php | 10 ++-------- application/views/scripts/group/list.phtml | 5 ----- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 3704c038e..f400e5bae 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -64,17 +64,11 @@ class GroupController extends AuthBackendController $query->applyFilter($filterEditor->getFilter()); $this->setupFilterControl($filterEditor); - try { - $this->setupPaginationControl($query); - $this->view->groups = $query; - } catch (Exception $e) { - Notification::error($e->getMessage()); - Logger::error($e); - } - + $this->view->groups = $query; $this->view->backend = $backend; $this->createListTabs()->activate('group/list'); + $this->setupPaginationControl($query); $this->setupLimitControl(); $this->setupSortControl( array( diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index be2dfae1a..c737af078 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -26,11 +26,6 @@ if ($backend === null) { $reducible = $backend instanceof Reducible; } -if (! isset($groups)) { - echo $this->translate('Failed to fetch any groups') . '
    '; - return; -} - if (count($groups) > 0): ?> From 676d20920b4fc80d7253c1f667e2a216630e7e7e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 08:53:34 +0200 Subject: [PATCH 168/239] UserController: Remove redundant error handling refs #8826 --- application/controllers/UserController.php | 10 ++-------- application/views/scripts/user/list.phtml | 5 ----- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 90979a3a7..fc13c5455 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -64,17 +64,11 @@ class UserController extends AuthBackendController $query->applyFilter($filterEditor->getFilter()); $this->setupFilterControl($filterEditor); - try { - $this->setupPaginationControl($query); - $this->view->users = $query; - } catch (Exception $e) { - Notification::error($e->getMessage()); - Logger::error($e); - } - + $this->view->users = $query; $this->view->backend = $backend; $this->createListTabs()->activate('user/list'); + $this->setupPaginationControl($query); $this->setupLimitControl(); $this->setupSortControl( array( diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 093270d4b..a1e4a6994 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -26,11 +26,6 @@ if ($backend === null) { $reducible = $backend instanceof Reducible; } -if (! isset($users)) { - echo $this->translate('Failed to fetch any users') . ''; - return; -} - if (count($users) > 0): ?>
    From adc2d338149d1449bb8b4f41b0c9a45a95a0b1d0 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 08:55:02 +0200 Subject: [PATCH 169/239] GroupController: Properly handle errors when fetching users refs #8826 --- application/controllers/GroupController.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index f400e5bae..0df9da8f2 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -302,8 +302,16 @@ class GroupController extends AuthBackendController { $users = array(); foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) { - foreach ($backend->select(array('user_name')) as $row) { - $users[] = $row; + try { + foreach ($backend->select(array('user_name')) as $row) { + $users[] = $row; + } + } catch (Exception $e) { + Logger::error($e); + Notification::warning(sprintf( + $this->translate('Failed to fetch any users from backend %s. Please check your log'), + $backend->getName() + )); } } From 45fd1b78f14d72fde16ce8cfa20a195478bdb718 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 08:55:14 +0200 Subject: [PATCH 170/239] UserGroupController: Properly handle errors when fetching memberships refs #8826 --- application/controllers/UserController.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index fc13c5455..c96e94dab 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -250,11 +250,19 @@ class UserController extends AuthBackendController { $groups = array(); foreach ($this->loadUserGroupBackends() as $backend) { - foreach ($backend->getMemberships($user) as $groupName) { - $groups[] = (object) array( - 'group_name' => $groupName, - 'backend' => $backend - ); + try { + foreach ($backend->getMemberships($user) as $groupName) { + $groups[] = (object) array( + 'group_name' => $groupName, + 'backend' => $backend + ); + } + } catch (Exception $e) { + Logger::error($e); + Notification::warning(sprintf( + $this->translate('Failed to fetch memberships from backend %s. Please check your log'), + $backend->getName() + )); } } From 170379b74308d76f37f9f8911a0142836d962f20 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 08:55:53 +0200 Subject: [PATCH 171/239] CreateMembershipForm: Properly handle errors when fetching groups refs #8826 --- .../Config/User/CreateMembershipForm.php | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php index a957edf6a..a0b40b4ee 100644 --- a/application/forms/Config/User/CreateMembershipForm.php +++ b/application/forms/Config/User/CreateMembershipForm.php @@ -4,6 +4,7 @@ namespace Icinga\Forms\Config\User; use Exception; +use Icinga\Application\Logger; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Web\Form; use Icinga\Web\Notification; @@ -153,18 +154,35 @@ class CreateMembershipForm extends Form */ protected function createDataSource() { - $groups = array(); + $groups = $failures = array(); foreach ($this->backends as $backend) { - $memberships = $backend - ->select() - ->from('group_membership', array('group_name')) - ->where('user_name', $this->userName) - ->fetchColumn(); - foreach ($backend->select(array('group_name')) as $row) { - if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter - $row->backend_name = $backend->getName(); - $groups[] = $row; + try { + $memberships = $backend + ->select() + ->from('group_membership', array('group_name')) + ->where('user_name', $this->userName) + ->fetchColumn(); + foreach ($backend->select(array('group_name')) as $row) { + if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter + $row->backend_name = $backend->getName(); + $groups[] = $row; + } } + } catch (Exception $e) { + $failures[] = array($backend->getName(), $e); + } + } + + if (empty($groups) && !empty($failures)) { + // In case there are only failures, throw the very first exception again + throw $failures[0][1]; + } elseif (! empty($failures)) { + foreach ($failures as $failure) { + Logger::error($failure[1]); + Notification::warning(sprintf( + $this->translate('Failed to fetch any groups from backend %s. Please check your log'), + $failure[0] + )); } } From 32c1a844b529c47387a45cfd8d5642c0831ce1c4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 09:03:00 +0200 Subject: [PATCH 172/239] UserController: Do not show duplicate memberships when showing a user refs #8826 --- application/controllers/UserController.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index c96e94dab..788ee7708 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -248,10 +248,15 @@ class UserController extends AuthBackendController */ protected function loadMemberships(User $user) { - $groups = array(); + $groups = $alreadySeen = array(); foreach ($this->loadUserGroupBackends() as $backend) { try { foreach ($backend->getMemberships($user) as $groupName) { + if (array_key_exists($groupName, $alreadySeen)) { + continue; // Ignore duplicate memberships + } + + $alreadySeen[$groupName] = null; $groups[] = (object) array( 'group_name' => $groupName, 'backend' => $backend From 2cbea558effd29ba7318747adab2b5f6d9ee750c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:23:59 +0200 Subject: [PATCH 173/239] UserController: Apply permission config/application/users/show refs #8826 --- application/controllers/UserController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 788ee7708..571cc633e 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -29,6 +29,7 @@ class UserController extends AuthBackendController */ public function listAction() { + $this->assertPermission('config/application/users/show'); $backendNames = array_map( function ($b) { return $b->getName(); }, $this->loadUserBackends('Icinga\Data\Selectable') @@ -86,6 +87,7 @@ class UserController extends AuthBackendController */ public function showAction() { + $this->assertPermission('config/application/users/show'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend')); From 88ba718ffbacb55b63ba114da8c02f35dfefeed7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:26:43 +0200 Subject: [PATCH 174/239] UserController: Apply permission config/application/users/add refs #8826 --- application/controllers/UserController.php | 1 + application/views/scripts/user/list.phtml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 571cc633e..ac0afc9a0 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -159,6 +159,7 @@ class UserController extends AuthBackendController */ public function addAction() { + $this->assertPermission('config/application/users/add'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); $form = new UserForm(); $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index a1e4a6994..a6ba5f601 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -22,8 +22,8 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list users') . ''; return; } else { - $extensible = $backend instanceof Extensible; $reducible = $backend instanceof Reducible; + $extensible = $this->hasPermission('config/application/users/add') && $backend instanceof Extensible; } if (count($users) > 0): ?> From 01b790cf186f47021d152d0aa61f63313bcba287 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:27:48 +0200 Subject: [PATCH 175/239] UserController: Apply permission config/application/users/edit refs #8826 --- application/controllers/UserController.php | 1 + application/views/scripts/user/show.phtml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index ac0afc9a0..cff627b6b 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -175,6 +175,7 @@ class UserController extends AuthBackendController */ public function editAction() { + $this->assertPermission('config/application/users/edit'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 1049a426a..4db1b0635 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -4,7 +4,7 @@ use Icinga\Data\Updatable; use Icinga\Data\Reducible; $editLink = null; -if ($backend instanceof Updatable) { +if ($this->hasPermission('config/application/users/edit') && $backend instanceof Updatable) { $editLink = $this->qlink( null, 'user/edit', From 0e37aad6cef695a0066c3fe8af9a89a80a318807 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:29:21 +0200 Subject: [PATCH 176/239] UserController: Apply permission config/application/users/remove refs #8826 --- application/controllers/UserController.php | 1 + application/views/scripts/user/list.phtml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index cff627b6b..f2c2ba2d4 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -198,6 +198,7 @@ class UserController extends AuthBackendController */ public function removeAction() { + $this->assertPermission('config/application/users/remove'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index a6ba5f601..51c0eb7b7 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -22,8 +22,8 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list users') . ''; return; } else { - $reducible = $backend instanceof Reducible; $extensible = $this->hasPermission('config/application/users/add') && $backend instanceof Extensible; + $reducible = $this->hasPermission('config/application/users/remove') && $backend instanceof Reducible; } if (count($users) > 0): ?> From e31c99be1cc6d88c937b43a2224be0ecdc5dd3c9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:30:42 +0200 Subject: [PATCH 177/239] GroupController: Apply permission config/application/groups/show refs #8826 --- application/controllers/GroupController.php | 2 ++ application/views/scripts/user/show.phtml | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 0df9da8f2..53bfa204c 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -29,6 +29,7 @@ class GroupController extends AuthBackendController */ public function listAction() { + $this->assertPermission('config/application/groups/show'); $backendNames = array_map( function ($b) { return $b->getName(); }, $this->loadUserGroupBackends('Icinga\Data\Selectable') @@ -86,6 +87,7 @@ class GroupController extends AuthBackendController */ public function showAction() { + $this->assertPermission('config/application/groups/show'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend')); diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 4db1b0635..1c8c41c8b 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -2,6 +2,7 @@ use Icinga\Data\Updatable; use Icinga\Data\Reducible; +use Icinga\Data\Selectable; $editLink = null; if ($this->hasPermission('config/application/users/edit') && $backend instanceof Updatable) { @@ -52,12 +53,18 @@ if ($this->hasPermission('config/application/users/edit') && $backend instanceof - +
    qlink($membership->group_name, 'group/show', array( - 'backend' => $membership->backend->getName(), - 'group' => $membership->group_name - ), array( - 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name) - )); ?> + hasPermission('config/application/groups/show') && $membership->backend instanceof Selectable): ?> + qlink($membership->group_name, 'group/show', array( + 'backend' => $membership->backend->getName(), + 'group' => $membership->group_name + ), array( + 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name) + )); ?> + + escape($membership->group_name); ?> + + backend instanceof Reducible): ?> setAction($this->url('group/removemember', array( From fd2ecf395db1641754366e850c921b02097eb2a8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:32:09 +0200 Subject: [PATCH 178/239] GroupController: Apply permission config/application/groups/add refs #8826 --- application/controllers/GroupController.php | 1 + application/views/scripts/group/list.phtml | 2 +- application/views/scripts/group/show.phtml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 53bfa204c..fe8853a06 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -165,6 +165,7 @@ class GroupController extends AuthBackendController */ public function addAction() { + $this->assertPermission('config/application/groups/add'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); $form = new UserGroupForm(); $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index c737af078..3ad65f624 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -22,8 +22,8 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list groups') . ''; return; } else { - $extensible = $backend instanceof Extensible; $reducible = $backend instanceof Reducible; + $extensible = $this->hasPermission('config/application/groups/add') && $backend instanceof Extensible; } if (count($groups) > 0): ?> diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 718dde255..61f793cb7 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -3,7 +3,7 @@ use Icinga\Data\Extensible; use Icinga\Data\Updatable; -$extensible = $backend instanceof Extensible; +$extensible = $this->hasPermission('config/application/groups/add') && $backend instanceof Extensible; $editLink = null; if ($backend instanceof Updatable) { From b58cd4747c103b7da0b25a196d75eec188bbf476 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:32:41 +0200 Subject: [PATCH 179/239] GroupController: Apply permission config/application/groups/edit refs #8826 --- application/controllers/GroupController.php | 1 + application/views/scripts/group/show.phtml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index fe8853a06..0b752a854 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -181,6 +181,7 @@ class GroupController extends AuthBackendController */ public function editAction() { + $this->assertPermission('config/application/groups/edit'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 61f793cb7..75895b908 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -6,7 +6,7 @@ use Icinga\Data\Updatable; $extensible = $this->hasPermission('config/application/groups/add') && $backend instanceof Extensible; $editLink = null; -if ($backend instanceof Updatable) { +if ($this->hasPermission('config/application/groups/edit') && $backend instanceof Updatable) { $editLink = $this->qlink( null, 'group/edit', From 0c9bac06865ffcafe4907dc5c80fd00b7f382dd3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:33:20 +0200 Subject: [PATCH 180/239] GroupController: Apply permission config/application/groups/remove refs #8826 --- application/controllers/GroupController.php | 3 ++- application/views/scripts/group/list.phtml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 0b752a854..7c7b11da3 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -130,7 +130,7 @@ class GroupController extends AuthBackendController $this->view->members = $members; $this->createShowTabs($backend->getName(), $groupName)->activate('group/show'); - if ($backend instanceof Reducible) { + if ($this->hasPermission('config/application/groups/remove') && $backend instanceof Reducible) { $removeForm = new Form(); $removeForm->setUidDisabled(); $removeForm->setAction( @@ -206,6 +206,7 @@ class GroupController extends AuthBackendController */ public function removeAction() { + $this->assertPermission('config/application/groups/remove'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index 3ad65f624..49d8dc4c9 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -22,8 +22,8 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list groups') . ''; return; } else { - $reducible = $backend instanceof Reducible; $extensible = $this->hasPermission('config/application/groups/add') && $backend instanceof Extensible; + $reducible = $this->hasPermission('config/application/groups/remove') && $backend instanceof Reducible; } if (count($groups) > 0): ?> From 1517c72be17d7a4dc6247db6d3184211806ba544 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:34:10 +0200 Subject: [PATCH 181/239] GroupController: Apply permission config/application/groups/member/add refs #8826 --- application/controllers/GroupController.php | 1 + application/controllers/UserController.php | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 7c7b11da3..9b16c9880 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -228,6 +228,7 @@ class GroupController extends AuthBackendController */ public function addmemberAction() { + $this->assertPermission('config/application/groups/member/add'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index f2c2ba2d4..8b926a44f 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -120,8 +120,12 @@ class UserController extends AuthBackendController $memberships ); - $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); - $this->view->showCreateMembershipLink = ! empty($extensibleBackends); + if ($this->hasPermission('config/application/groups/member/add')) { + $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); + $this->view->showCreateMembershipLink = ! empty($extensibleBackends); + } else { + $this->view->showCreateMembershipLink = false; + } $this->view->user = $user; $this->view->backend = $backend; @@ -220,6 +224,7 @@ class UserController extends AuthBackendController */ public function createmembershipAction() { + $this->assertPermission('config/application/groups/member/add'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend')); From d157dec13b22f11f47be6af328be9ce7722943f8 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:38:02 +0200 Subject: [PATCH 182/239] GroupController: Apply permission config/application/groups/member/remove refs #8826 --- application/controllers/GroupController.php | 3 +- application/controllers/UserController.php | 50 +++++++++++---------- application/views/scripts/user/show.phtml | 2 +- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 9b16c9880..2ae15c967 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -130,7 +130,7 @@ class GroupController extends AuthBackendController $this->view->members = $members; $this->createShowTabs($backend->getName(), $groupName)->activate('group/show'); - if ($this->hasPermission('config/application/groups/remove') && $backend instanceof Reducible) { + if ($this->hasPermission('config/application/groups/member/remove') && $backend instanceof Reducible) { $removeForm = new Form(); $removeForm->setUidDisabled(); $removeForm->setAction( @@ -255,6 +255,7 @@ class GroupController extends AuthBackendController */ public function removememberAction() { + $this->assertPermission('config/application/groups/member/remove'); $this->assertHttpMethod('POST'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 8b926a44f..db60d4e96 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -132,30 +132,32 @@ class UserController extends AuthBackendController $this->view->memberships = $memberships; $this->createShowTabs($backend->getName(), $userName)->activate('user/show'); - $removeForm = new Form(); - $removeForm->setUidDisabled(); - $removeForm->addElement('hidden', 'user_name', array( - 'isArray' => true, - 'value' => $userName, - 'decorators' => array('ViewHelper') - )); - $removeForm->addElement('hidden', 'redirect', array( - 'value' => Url::fromPath('user/show', array( - 'backend' => $backend->getName(), - 'user' => $userName - )), - 'decorators' => array('ViewHelper') - )); - $removeForm->addElement('button', 'btn_submit', array( - 'escape' => false, - 'type' => 'submit', - 'class' => 'link-like', - 'value' => 'btn_submit', - 'decorators' => array('ViewHelper'), - 'label' => $this->view->icon('trash'), - 'title' => $this->translate('Cancel this membership') - )); - $this->view->removeForm = $removeForm; + if ($this->hasPermission('config/application/groups/member/remove')) { + $removeForm = new Form(); + $removeForm->setUidDisabled(); + $removeForm->addElement('hidden', 'user_name', array( + 'isArray' => true, + 'value' => $userName, + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('hidden', 'redirect', array( + 'value' => Url::fromPath('user/show', array( + 'backend' => $backend->getName(), + 'user' => $userName + )), + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('button', 'btn_submit', array( + 'escape' => false, + 'type' => 'submit', + 'class' => 'link-like', + 'value' => 'btn_submit', + 'decorators' => array('ViewHelper'), + 'label' => $this->view->icon('trash'), + 'title' => $this->translate('Cancel this membership') + )); + $this->view->removeForm = $removeForm; + } } /** diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 1c8c41c8b..96af9224c 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -66,7 +66,7 @@ if ($this->hasPermission('config/application/users/edit') && $backend instanceof - backend instanceof Reducible): ?> + backend instanceof Reducible): ?> setAction($this->url('group/removemember', array( 'backend' => $membership->backend->getName(), 'group' => $membership->group_name From 8713f59e66642b68e6016b69beb22a66fcab6f52 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:38:35 +0200 Subject: [PATCH 183/239] AuthBackendController: Only show tabs the user is permitted to view refs #8826 --- .../Web/Controller/AuthBackendController.php | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index 1f93de1c7..5b2a4f18e 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -138,24 +138,30 @@ class AuthBackendController extends Controller protected function createListTabs() { $tabs = $this->getTabs(); - $tabs->add( - 'user/list', - array( - 'title' => $this->translate('List users of authentication backends'), - 'label' => $this->translate('Users'), - 'icon' => 'user', - 'url' => 'user/list' - ) - ); - $tabs->add( - 'group/list', - array( - 'title' => $this->translate('List groups of user group backends'), - 'label' => $this->translate('Groups'), - 'icon' => 'users', - 'url' => 'group/list' - ) - ); + + if ($this->hasPermission('config/application/users/show')) { + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'icon' => 'user', + 'url' => 'user/list' + ) + ); + } + + if ($this->hasPermission('config/application/groups/show')) { + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('Groups'), + 'icon' => 'users', + 'url' => 'group/list' + ) + ); + } return $tabs; } From e55d43418d228b1afe7ad2ba97cb239dccd209b5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 10:39:29 +0200 Subject: [PATCH 184/239] RoleForm: Add new permission sets for user and group management refs #8826 --- application/forms/Security/RoleForm.php | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 07b09ce17..536a7efe0 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -21,14 +21,26 @@ class RoleForm extends ConfigForm * @var array */ protected $providedPermissions = array( - '*' => '*', - 'config/*' => 'config/*', - 'config/application/*' => 'config/application/*', - 'config/application/general' => 'config/application/general', - 'config/application/authentication' => 'config/application/authentication', - 'config/application/resources' => 'config/application/resources', - 'config/application/roles' => 'config/application/roles', - 'config/modules' => 'config/modules' + '*' => '*', + 'config/*' => 'config/*', + 'config/application/*' => 'config/application/*', + 'config/application/general' => 'config/application/general', + 'config/application/authentication' => 'config/application/authentication', + 'config/application/resources' => 'config/application/resources', + 'config/application/roles' => 'config/application/roles', + 'config/application/users/*' => 'config/application/users/*', + 'config/application/users/show' => 'config/application/users/show', + 'config/application/users/add' => 'config/application/users/add', + 'config/application/users/edit' => 'config/application/users/edit', + 'config/application/users/remove' => 'config/application/users/remove', + 'config/application/groups/*' => 'config/application/groups/*', + 'config/application/groups/show' => 'config/application/groups/show', + 'config/application/groups/add' => 'config/application/groups/add', + 'config/application/groups/edit' => 'config/application/groups/edit', + 'config/application/groups/remove' => 'config/application/groups/remove', + 'config/application/groups/member/add' => 'config/application/groups/member/add', + 'config/application/groups/member/remove' => 'config/application/groups/member/remove', + 'config/modules' => 'config/modules' ); /** From 23b7ab07647a51b6d5c53deeab211c076bda5691 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 27 May 2015 11:47:18 +0200 Subject: [PATCH 185/239] DbRepository: Remove COLLATE from a query column in case of a pgsql connection refs #8826 --- library/Icinga/Repository/DbRepository.php | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index a1fb800ee..f4c7c8332 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -64,14 +64,72 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ protected $statementColumnMap; + /** + * List of columns where the COLLATE SQL-instruction has been removed + * + * This list is being populated in case of a PostgreSQL backend only, + * to ensure case-insensitive string comparison in WHERE clauses. + * + * @var array + */ + protected $columnsWithoutCollation; + /** * Create a new DB repository object * + * In case $this->queryColumns has already been initialized, this initializes + * $this->columnsWithoutCollation in case of a PostgreSQL connection. + * * @param DbConnection $ds The datasource to use */ public function __construct(DbConnection $ds) { parent::__construct($ds); + + $this->columnsWithoutCollation = array(); + if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) { + $this->queryColumns = $this->removeCollateInstruction($this->queryColumns); + } + } + + /** + * Return the query columns being provided + * + * Initializes $this->columnsWithoutCollation in case of a PostgreSQL connection. + * + * @return array + */ + public function getQueryColumns() + { + if ($this->queryColumns === null) { + $this->queryColumns = parent::getQueryColumns(); + if ($this->ds->getDbType() === 'pgsql') { + $this->queryColumns = $this->removeCollateInstruction($this->queryColumns); + } + } + + return $this->queryColumns; + } + + /** + * Remove each COLLATE SQL-instruction from all given query columns + * + * @param array $queryColumns + * + * @return array $queryColumns, the updated version + */ + protected function removeCollateInstruction($queryColumns) + { + foreach ($queryColumns as & $columns) { + foreach ($columns as & $column) { + $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count); + if ($count > 0) { + $this->columnsWithoutCollation[] = $column; + } + } + } + + return $queryColumns; } /** @@ -281,6 +339,33 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $this->prependTablePrefix($table); } + /** + * Recurse the given filter, require each column for the given table and convert all values + * + * In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower() + * on the value if a COLLATE SQL-instruction is part of the resolved column. + * + * @param string $table + * @param Filter $filter + */ + public function requireFilter($table, Filter $filter) + { + parent::requireFilter($table, $filter); + + if ($filter->isExpression()) { + $column = $filter->getColumn(); + if (in_array($column, $this->columnsWithoutCollation) && strpos($column, 'LOWER') !== 0) { + $filter->setColumn('LOWER(' . $column . ')'); + $expression = $filter->getExpression(); + if (is_array($expression)) { + $filter->setExpression(array_map('strtolower', $expression)); + } else { + $filter->setExpression(strtolower($expression)); + } + } + } + } + /** * Return this repository's query columns of the given table mapped to their respective aliases * From 647dd9d4251efe04e9998d1740d3217bce6974c3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 13:25:26 +0200 Subject: [PATCH 186/239] RepositoryQuery: Pass through the query when requiring a table or column This allows now to adjust the query in custom repository implementations. refs #8826 --- library/Icinga/Repository/DbRepository.php | 18 +++++--- library/Icinga/Repository/Repository.php | 44 +++++++++++-------- library/Icinga/Repository/RepositoryQuery.php | 12 ++--- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index f4c7c8332..433842a57 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -323,13 +323,15 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Validate that the requested table exists * - * @param string $table + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) * * @return string The table's name, with the table prefix being prepended * - * @throws ProgrammingError In case the given table does not exist + * @throws ProgrammingError In case the given table does not exist */ - public function requireTable($table) + public function requireTable($table, RepositoryQuery $query = null) { $statementColumns = $this->getStatementColumns(); if (! isset($statementColumns[$table])) { @@ -345,12 +347,14 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, * In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower() * on the value if a COLLATE SQL-instruction is part of the resolved column. * - * @param string $table - * @param Filter $filter + * @param string $table The table being filtered + * @param Filter $filter The filter to recurse + * @param RepositoryQuery $query An optional query to pass as context + * (Directly passed through to $this->requireFilterColumn) */ - public function requireFilter($table, Filter $filter) + public function requireFilter($table, Filter $filter, RepositoryQuery $query = null) { - parent::requireFilter($table, $filter); + parent::requireFilter($table, $filter, $query); if ($filter->isExpression()) { $column = $filter->getColumn(); diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 8bc6517af..56f383f0c 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -592,13 +592,15 @@ abstract class Repository implements Selectable /** * Validate that the requested table exists * - * @param string $table + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) * - * @return string The table's name, may differ from the given one + * @return string The table's name, may differ from the given one * - * @throws ProgrammingError In case the given table does not exist + * @throws ProgrammingError In case the given table does not exist */ - public function requireTable($table) + public function requireTable($table, RepositoryQuery $query = null) { $queryColumns = $this->getQueryColumns(); if (! isset($queryColumns[$table])) { @@ -611,18 +613,20 @@ abstract class Repository implements Selectable /** * Recurse the given filter, require each column for the given table and convert all values * - * @param string $table - * @param Filter $filter + * @param string $table The table being filtered + * @param Filter $filter The filter to recurse + * @param RepositoryQuery $query An optional query to pass as context + * (Directly passed through to $this->requireFilterColumn) */ - public function requireFilter($table, Filter $filter) + public function requireFilter($table, Filter $filter, RepositoryQuery $query = null) { if ($filter->isExpression()) { $column = $filter->getColumn(); - $filter->setColumn($this->requireFilterColumn($table, $column)); + $filter->setColumn($this->requireFilterColumn($table, $column, $query)); $filter->setExpression($this->persistColumn($column, $filter->getExpression())); } elseif ($filter->isChain()) { foreach ($filter->filters() as $chainOrExpression) { - $this->requireFilter($table, $chainOrExpression); + $this->requireFilter($table, $chainOrExpression, $query); } } } @@ -707,14 +711,15 @@ abstract class Repository implements Selectable /** * Validate that the given column is a valid query target and return it or the actual name if it's an alias * - * @param string $table The table where to look for the column or alias - * @param string $name The name or alias of the column to validate + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation) * - * @return string The given column's name + * @return string The given column's name * - * @throws QueryException In case the given column is not a valid query column + * @throws QueryException In case the given column is not a valid query column */ - public function requireQueryColumn($table, $name) + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) { if (in_array($name, $this->getFilterColumns())) { throw new QueryException(t('Filter column "%s" cannot be queried'), $name); @@ -748,14 +753,15 @@ abstract class Repository implements Selectable /** * Validate that the given column is a valid filter target and return it or the actual name if it's an alias * - * @param string $table The table where to look for the column or alias - * @param string $name The name or alias of the column to validate + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation) * - * @return string The given column's name + * @return string The given column's name * - * @throws QueryException In case the given column is not a valid filter column + * @throws QueryException In case the given column is not a valid filter column */ - public function requireFilterColumn($table, $name) + public function requireFilterColumn($table, $name, RepositoryQuery $query = null) { if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) { throw new QueryException(t('Filter column "%s" not found'), $name); diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 4d87e5026..f92d7eb9f 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -76,7 +76,7 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function from($target, array $columns = null) { - $target = $this->repository->requireTable($target); + $target = $this->repository->requireTable($target, $this); $this->query = $this->repository ->getDataSource() ->select() @@ -127,7 +127,7 @@ class RepositoryQuery implements QueryInterface, Iterator } else { $columns = array(); foreach ($desiredColumns as $customAlias => $columnAlias) { - $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias); + $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this); if ($resolvedColumn !== $columnAlias) { $columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn; } elseif (is_string($customAlias)) { @@ -154,7 +154,7 @@ class RepositoryQuery implements QueryInterface, Iterator public function where($column, $value = null) { $this->query->where( - $this->repository->requireFilterColumn($this->target, $column), + $this->repository->requireFilterColumn($this->target, $column, $this), $this->repository->persistColumn($column, $value) ); return $this; @@ -186,7 +186,7 @@ class RepositoryQuery implements QueryInterface, Iterator public function setFilter(Filter $filter) { $filter = clone $filter; - $this->repository->requireFilter($this->target, $filter); + $this->repository->requireFilter($this->target, $filter, $this); $this->query->setFilter($filter); return $this; } @@ -203,7 +203,7 @@ class RepositoryQuery implements QueryInterface, Iterator public function addFilter(Filter $filter) { $filter = clone $filter; - $this->repository->requireFilter($this->target, $filter); + $this->repository->requireFilter($this->target, $filter, $this); $this->query->addFilter($filter); return $this; } @@ -268,7 +268,7 @@ class RepositoryQuery implements QueryInterface, Iterator try { $this->query->order( - $this->repository->requireFilterColumn($this->target, $column), + $this->repository->requireFilterColumn($this->target, $column, $this), $specificDirection ?: $baseDirection // I would have liked the following solution, but hey, a coder should be allowed to produce crap... // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection From 5326ce6bca17efe66a144d8afba56af3a66053fd Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 13:44:51 +0200 Subject: [PATCH 187/239] DbRepository: Add support for table specific aliases This was previously only possible for the base table and in case one wanted to use table aliases in the query column definition for non-base tables as well, it did not work well due to not being explicitly supported. Now, to use such table aliases one must initialize DbRepository::tableAliases. refs #8826 --- library/Icinga/Repository/DbRepository.php | 136 +++++++++++++++------ library/Icinga/Repository/Repository.php | 4 +- 2 files changed, 102 insertions(+), 38 deletions(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 433842a57..f3b0c679c 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -17,6 +17,7 @@ use Icinga\Exception\StatementException; * * Additionally provided features: *
      + *
    • Support for table aliases
    • *
    • Automatic table prefix handling
    • *
    • Insert, update and delete capabilities
    • *
    • Differentiation between statement and query columns
    • @@ -31,6 +32,17 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ protected $ds; + /** + * The table aliases being applied + * + * This must be initialized by repositories which are going to make use of table aliases. Every table for which + * aliased columns are provided must be defined in this array using its name as key and the alias being used as + * value. Failure to do so will result in invalid queries. + * + * @var array + */ + protected $tableAliases; + /** * The statement columns being provided * @@ -111,6 +123,32 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $this->queryColumns; } + /** + * Return the table aliases to be applied + * + * Calls $this->initializeTableAliases() in case $this->tableAliases is null. + * + * @return array + */ + public function getTableAliases() + { + if ($this->tableAliases === null) { + $this->tableAliases = $this->initializeTableAliases(); + } + + return $this->tableAliases; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the table aliases lazily + * + * @return array + */ + protected function initializeTableAliases() + { + return array(); + } + /** * Remove each COLLATE SQL-instruction from all given query columns * @@ -166,9 +204,11 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Remove the datasource's prefix from the given table name and return the remaining part * - * @param mixed $table + * @param array|string $table * - * @return mixed + * @return array|string + * + * @throws IcingaException In case $table is not of a supported type */ protected function removeTablePrefix($table) { @@ -194,6 +234,45 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $table; } + /** + * Return the given table with its alias being applied + * + * @param array|string $table + * + * @return array|string + */ + protected function applyTableAlias($table) + { + $tableAliases = $this->getTableAliases(); + if (is_array($table) || !isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) { + return $table; + } + + return array($tableAliases[$nonPrefixedTable] => $table); + } + + /** + * Return the given table with its alias being cleared + * + * @param array|string $table + * + * @return string + * + * @throws IcingaException In case $table is not of a supported type + */ + protected function clearTableAlias($table) + { + if (is_string($table)) { + return $table; + } + + if (is_array($table)) { + return reset($table); + } + + throw new IcingaException('Table alias handling for type "%s" is not supported', type($table)); + } + /** * Insert a table row with the given data * @@ -323,11 +402,13 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Validate that the requested table exists * + * This will prepend the datasource's table prefix and will apply the table's alias, if any. + * * @param string $table The table to validate * @param RepositoryQuery $query An optional query to pass as context * (unused by the base implementation) * - * @return string The table's name, with the table prefix being prepended + * @return array|string * * @throws ProgrammingError In case the given table does not exist */ @@ -338,7 +419,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, $table = parent::requireTable($table); } - return $this->prependTablePrefix($table); + return $this->prependTablePrefix($this->applyTableAlias($table)); } /** @@ -373,7 +454,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Return this repository's query columns of the given table mapped to their respective aliases * - * @param mixed $table + * @param array|string $table * * @return array * @@ -381,61 +462,48 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ public function requireAllQueryColumns($table) { - if (is_array($table)) { - $table = array_shift($table); - } - - return parent::requireAllQueryColumns($this->removeTablePrefix($table)); + return parent::requireAllQueryColumns($this->removeTablePrefix($this->clearTableAlias($table))); } /** * Return the query column name for the given alias or null in case the alias does not exist * - * @param mixed $table - * @param string $alias + * @param array|string $table + * @param string $alias * * @return string|null */ public function resolveQueryColumnAlias($table, $alias) { - if (is_array($table)) { - $table = array_shift($table); - } - - return parent::resolveQueryColumnAlias($this->removeTablePrefix($table), $alias); + return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias); } /** * Return whether the given query column name or alias is available in the given table * - * @param mixed $table - * @param string $column + * @param array|string $table + * @param string $column * * @return bool */ public function validateQueryColumnAssociation($table, $column) { - if (is_array($table)) { - $table = array_shift($table); - } - - return parent::validateQueryColumnAssociation($this->removeTablePrefix($table), $column); + return parent::validateQueryColumnAssociation( + $this->removeTablePrefix($this->clearTableAlias($table)), + $column + ); } /** * Return the statement column name for the given alias or null in case the alias does not exist * - * @param mixed $table + * @param string $table * @param string $alias * * @return string|null */ public function resolveStatementColumnAlias($table, $alias) { - if (is_array($table)) { - $table = array_shift($table); - } - $statementColumnMap = $this->getStatementColumnMap(); if (isset($statementColumnMap[$alias])) { return $statementColumnMap[$alias]; @@ -450,17 +518,13 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Return whether the given alias or statement column name is available in the given table * - * @param mixed $table + * @param string $table * @param string $alias * * @return bool */ public function validateStatementColumnAssociation($table, $alias) { - if (is_array($table)) { - $table = array_shift($table); - } - $statementTableMap = $this->getStatementTableMap(); if (isset($statementTableMap[$alias])) { return $statementTableMap[$alias] === $this->removeTablePrefix($table); @@ -473,7 +537,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Return whether the given column name or alias of the given table is a valid statement column * - * @param mixed $table The table where to look for the column or alias + * @param string $table The table where to look for the column or alias * @param string $name The column name or alias to check * * @return bool @@ -493,7 +557,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, /** * Validate that the given column is a valid statement column and return it or the actual name if it's an alias * - * @param mixed $table The table for which to require the column + * @param string $table The table for which to require the column * @param string $name The name or alias of the column to validate * * @return string The given column's name diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 56f383f0c..7fba11727 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -48,7 +48,7 @@ abstract class Repository implements Selectable * * This will be automatically set to the first key of $queryColumns if not explicitly set. * - * @var mixed + * @var string */ protected $baseTable; @@ -196,7 +196,7 @@ abstract class Repository implements Selectable /** * Return the base table name this repository is responsible for * - * @return mixed + * @return string * * @throws ProgrammingError In case no base table name has been set and * $this->queryColumns does not provide one either From 58d78f59f3fa2b875ad67a620cfe8799d9a478cd Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 13:49:36 +0200 Subject: [PATCH 188/239] DbQuery: Initialize self::$select as early as possible I'd like to use Zend's implementation instead of re-inventing the wheel just because someone decided to only work with a copy of it in the frameworks query but do exactly the opposite in the monitoring module's IDO query... --- library/Icinga/Data/Db/DbQuery.php | 21 +++++++++++-------- .../Monitoring/Backend/Ido/Query/IdoQuery.php | 12 +---------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php index c284f0e86..df90ef13c 100644 --- a/library/Icinga/Data/Db/DbQuery.php +++ b/library/Icinga/Data/Db/DbQuery.php @@ -8,7 +8,7 @@ use Icinga\Data\Filter\FilterChain; use Icinga\Data\Filter\FilterOr; use Icinga\Data\Filter\FilterAnd; use Icinga\Data\Filter\FilterNot; -use Icinga\Exception\IcingaException; +use Icinga\Exception\QueryException; use Zend_Db_Select; /** @@ -66,6 +66,7 @@ class DbQuery extends SimpleQuery protected function init() { $this->db = $this->ds->getDbAdapter(); + $this->select = $this->db->select(); parent::init(); } @@ -75,6 +76,13 @@ class DbQuery extends SimpleQuery return $this; } + public function from($target, array $fields = null) + { + parent::from($target, $fields); + $this->select->from($this->target, array()); + return $this; + } + public function where($condition, $value = null) { // $this->count = $this->select = null; @@ -83,9 +91,6 @@ class DbQuery extends SimpleQuery protected function dbSelect() { - if ($this->select === null) { - $this->select = $this->db->select()->from($this->target, array()); - } return clone $this->select; } @@ -151,7 +156,7 @@ class DbQuery extends SimpleQuery $op = ' AND '; $str .= ' NOT '; } else { - throw new IcingaException( + throw new QueryException( 'Cannot render filter: %s', $filter ); @@ -212,7 +217,7 @@ class DbQuery extends SimpleQuery if (! $value) { /* NOTE: It's too late to throw exceptions, we might finish in __toString - throw new IcingaException(sprintf( + throw new QueryException(sprintf( '"%s" is not a valid time expression', $value )); @@ -318,9 +323,7 @@ class DbQuery extends SimpleQuery public function __clone() { - if ($this->select) { - $this->select = clone $this->select; - } + $this->select = clone $this->select; } /** diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php index 9371399bc..0b436d7bf 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php @@ -415,21 +415,11 @@ abstract class IdoQuery extends DbQuery } elseif ($dbType === 'pgsql') { $this->initializeForPostgres(); } - $this->dbSelect(); + $this->joinBaseTables(); $this->select->columns($this->columns); - //$this->joinBaseTables(); $this->prepareAliasIndexes(); } - protected function dbSelect() - { - if ($this->select === null) { - $this->select = $this->db->select(); - $this->joinBaseTables(); - } - return clone $this->select; - } - /** * Join the base tables for this query */ From 119b2fdddb0d8dd15406581f28d51b52ae906b8d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 13:52:00 +0200 Subject: [PATCH 189/239] DbQuery: Allow to join additional tables This should just be the beginning of such additions, there is still a group(), distinct(), etc missing.. --- library/Icinga/Data/Db/DbQuery.php | 133 +++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php index df90ef13c..19849c943 100644 --- a/library/Icinga/Data/Db/DbQuery.php +++ b/library/Icinga/Data/Db/DbQuery.php @@ -346,4 +346,137 @@ class DbQuery extends SimpleQuery $this->group = $group; return $this; } + + /** + * Return whether the given table has been joined + * + * @param string $table + * + * @return bool + */ + public function hasJoinedTable($table) + { + $fromPart = $this->select->getPart(Zend_Db_Select::FROM); + if (isset($fromPart[$table])) { + return true; + } + + foreach ($fromPart as $options) { + if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) { + return true; + } + } + + return false; + } + + /** + * Add an INNER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function join($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinInner($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add an INNER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinInner($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinInner($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a LEFT OUTER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinLeft($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinLeft($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a RIGHT OUTER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinRight($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinRight($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a FULL OUTER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinFull($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinFull($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a CROSS JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinCross($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinCross($name, $cols, $schema); + return $this; + } + + /** + * Add a NATURAL JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinNatural($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinNatural($name, $cols, $schema); + return $this; + } } From 08f8fe6f49be64bb3e52f6d4a3b29d9d4e10ace3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 13:53:49 +0200 Subject: [PATCH 190/239] DbRepository: Add support for joining tables based on query/filter columns refs #8826 --- library/Icinga/Repository/DbRepository.php | 121 +++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index f3b0c679c..38099b0e7 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -11,6 +11,7 @@ use Icinga\Data\Updatable; use Icinga\Exception\IcingaException; use Icinga\Exception\ProgrammingError; use Icinga\Exception\StatementException; +use Icinga\Util\String; /** * Abstract base class for concrete database repository implementations @@ -21,6 +22,7 @@ use Icinga\Exception\StatementException; *
    • Automatic table prefix handling
    • *
    • Insert, update and delete capabilities
    • *
    • Differentiation between statement and query columns
    • + *
    • Capability to join additional tables depending on the columns being selected or used in a filter
    • *
    */ abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible @@ -494,6 +496,54 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, ); } + /** + * Validate that the given column is a valid query target and return it or the actual name if it's an alias + * + * Attempts to join the given column from a different table if its association to the given table cannot be + * verified. + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context, + * if not given no join will be attempted + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid query column + */ + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) + { + if ($query === null || $this->validateQueryColumnAssociation($table, $name)) { + return parent::requireQueryColumn($table, $name, $query); + } + + return $this->joinColumn($name, $table, $query); + } + + /** + * Validate that the given column is a valid filter target and return it or the actual name if it's an alias + * + * Attempts to join the given column from a different table if its association to the given table cannot be + * verified. + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context, + * if not given no join will be attempted + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid filter column + */ + public function requireFilterColumn($table, $name, RepositoryQuery $query = null) + { + if ($query === null || $this->validateQueryColumnAssociation($table, $name)) { + return parent::requireFilterColumn($table, $name, $query); + } + + return $this->joinColumn($name, $table, $query); + } + /** * Return the statement column name for the given alias or null in case the alias does not exist * @@ -576,4 +626,75 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $column; } + + /** + * Join alias or column $name into $table using $query + * + * Attempts to find a valid table for the given alias or column name and a method labelled join + * to process the actual join logic. If neither of those is found, ProgrammingError will be thrown. + * The method is called with the same parameters but in reversed order. + * + * @param string $name The alias or column name to join into $target + * @param array|string $target The table to join $name into + * @param RepositoryQUery $query The query to apply the JOIN-clause on + * + * @return string The resolved alias or $name + * + * @throws ProgrammingError In case no valid table or join-method is found + */ + public function joinColumn($name, $target, RepositoryQuery $query) + { + $tableName = $this->findTableName($name); + if (! $tableName) { + throw new ProgrammingError( + 'Unable to find a valid table for column "%s" to join into "%s"', + $name, + $this->removeTablePrefix($this->clearTableAlias($target)) + ); + } + + $column = $this->resolveQueryColumnAlias($tableName, $name); + + $prefixedTableName = $this->prependTablePrefix($tableName); + if ($query->getQuery()->hasJoinedTable($prefixedTableName)) { + return $column; + } + + $joinMethod = 'join' . String::cname($tableName); + if (! method_exists($this, $joinMethod)) { + throw new ProgrammingError( + 'Unable to join table "%s" into "%s". Method "%s" not found', + $tableName, + $this->removeTablePrefix($this->clearTableAlias($target)), + $joinMethod + ); + } + + $this->$joinMethod($query, $target, $name); + return $column; + } + + /** + * Return the table name for the given alias or column name + * + * @param string $column + * + * @return string|null null in case no table is found + */ + protected function findTableName($column) + { + $aliasTableMap = $this->getAliasTableMap(); + if (isset($aliasTableMap[$column])) { + return $aliasTableMap[$column]; + } + + foreach ($aliasTableMap as $alias => $table) { + if (strpos($alias, '.') !== false) { + list($_, $alias) = split('.', $column, 2); + if ($alias === $column) { + return $table; + } + } + } + } } From fd931e42320e55f082d169a6704f69d51f4afba5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 14:26:48 +0200 Subject: [PATCH 191/239] RepositoryQuery: getQuery() might be called during prepareQueryColumns() --- library/Icinga/Repository/RepositoryQuery.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index f92d7eb9f..8048ce434 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -77,10 +77,8 @@ class RepositoryQuery implements QueryInterface, Iterator public function from($target, array $columns = null) { $target = $this->repository->requireTable($target, $this); - $this->query = $this->repository - ->getDataSource() - ->select() - ->from($target, $this->prepareQueryColumns($target, $columns)); + $this->query = $this->repository->getDataSource()->select()->from($target); + $this->query->columns($this->prepareQueryColumns($target, $columns)); $this->target = $target; return $this; } From 1950ddcbd6aee92c74741d4de36821466d54e988 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 15:00:46 +0200 Subject: [PATCH 192/239] mysql schema: Introduce `id' and `group_id' Required to ensure referential integrity when renaming groups. refs #8826 --- etc/schema/mysql.schema.sql | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/etc/schema/mysql.schema.sql b/etc/schema/mysql.schema.sql index 16ba9f4ff..bbc801d8b 100644 --- a/etc/schema/mysql.schema.sql +++ b/etc/schema/mysql.schema.sql @@ -1,21 +1,23 @@ # Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ CREATE TABLE `icingaweb_group`( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL, `parent` varchar(64) COLLATE utf8_unicode_ci NULL DEFAULT NULL, `ctime` timestamp NULL DEFAULT NULL, `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`name`) + PRIMARY KEY (`id`), + UNIQUE KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `icingaweb_group_membership`( - `group_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL, + `group_id` int(10) unsigned NOT NULL, `username` varchar(64) COLLATE utf8_unicode_ci NOT NULL, `ctime` timestamp NULL DEFAULT NULL, `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`group_name`,`username`), - CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_name`) - REFERENCES `icingaweb_group` (`name`) + PRIMARY KEY (`group_id`,`username`), + CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_id`) + REFERENCES `icingaweb_group` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `icingaweb_user`( From 1c6ded9324c11e71e9e05d81459fec638028cf99 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 15:01:21 +0200 Subject: [PATCH 193/239] pgsql schema: Introduce `id' and `group_id' Required to ensure referential integrity when renaming groups. refs #8826 --- etc/schema/pgsql.schema.sql | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/etc/schema/pgsql.schema.sql b/etc/schema/pgsql.schema.sql index 034d8288a..09ecb0b71 100644 --- a/etc/schema/pgsql.schema.sql +++ b/etc/schema/pgsql.schema.sql @@ -1,6 +1,7 @@ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ CREATE TABLE "icingaweb_group" ( + "id" serial, "name" character varying(64) NOT NULL, "parent" character varying(64) NULL DEFAULT NULL, "ctime" timestamp NULL DEFAULT NULL, @@ -10,7 +11,7 @@ CREATE TABLE "icingaweb_group" ( ALTER TABLE ONLY "icingaweb_group" ADD CONSTRAINT pk_icingaweb_group PRIMARY KEY ( - "name" + "id" ); CREATE UNIQUE INDEX idx_icingaweb_group @@ -20,7 +21,7 @@ CREATE UNIQUE INDEX idx_icingaweb_group ); CREATE TABLE "icingaweb_group_membership" ( - "group_name" character varying(64) NOT NULL, + "group_id" int NOT NULL, "username" character varying(64) NOT NULL, "ctime" timestamp NULL DEFAULT NULL, "mtime" timestamp NULL DEFAULT NULL @@ -28,15 +29,17 @@ CREATE TABLE "icingaweb_group_membership" ( ALTER TABLE ONLY "icingaweb_group_membership" ADD CONSTRAINT pk_icingaweb_group_membership - PRIMARY KEY ( - "group_name", - "username" + FOREIGN KEY ( + "group_id" + ) + REFERENCES "icingaweb_group" ( + "id" ); CREATE UNIQUE INDEX idx_icingaweb_group_membership ON "icingaweb_group_membership" USING btree ( - lower((group_name)::text), + group_id, lower((username)::text) ); From 32b99be8abff5eabfb6c75b34f7faed1a82bc918 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 15:22:15 +0200 Subject: [PATCH 194/239] DbUserGroupBackend: Adjust to fit the new database schema refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 88 ++++++++++++++----- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index 36aa61a1d..d3096dadd 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -5,6 +5,7 @@ namespace Icinga\Authentication\UserGroup; use Icinga\Data\Filter\Filter; use Icinga\Repository\DbRepository; +use Icinga\Repository\RepositoryQuery; use Icinga\User; class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterface @@ -16,23 +17,33 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ protected $queryColumns = array( 'group' => array( - 'group' => 'name COLLATE utf8_general_ci', - 'group_name' => 'name', - 'parent' => 'parent COLLATE utf8_general_ci', - 'parent_name' => 'parent', - 'created_at' => 'UNIX_TIMESTAMP(ctime)', - 'last_modified' => 'UNIX_TIMESTAMP(mtime)' + 'group_id' => 'g.id', + 'group' => 'g.name COLLATE utf8_general_ci', + 'group_name' => 'g.name', + 'parent' => 'g.parent COLLATE utf8_general_ci', + 'parent_name' => 'g.parent', + 'created_at' => 'UNIX_TIMESTAMP(g.ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(g.mtime)' ), 'group_membership' => array( - 'group' => 'group_name COLLATE utf8_general_ci', - 'group_name', - 'user' => 'username COLLATE utf8_general_ci', - 'user_name' => 'username', - 'created_at' => 'UNIX_TIMESTAMP(ctime)', - 'last_modified' => 'UNIX_TIMESTAMP(mtime)' + 'group_id' => 'gm.group_id', + 'user' => 'gm.username COLLATE utf8_general_ci', + 'user_name' => 'gm.username', + 'created_at' => 'UNIX_TIMESTAMP(gm.ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(gm.mtime)' ) ); + /** + * The table aliases being applied + * + * @var array + */ + protected $tableAliases = array( + 'group' => 'g', + 'group_membership' => 'gm' + ); + /** * The statement columns being provided * @@ -40,10 +51,15 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ protected $statementColumns = array( 'group' => array( + 'group_id' => 'id', + 'group_name' => 'name', + 'parent_name' => 'parent', 'created_at' => 'ctime', 'last_modified' => 'mtime' ), 'group_membership' => array( + 'group_id' => 'group_id', + 'user_name' => 'username', 'created_at' => 'ctime', 'last_modified' => 'mtime' ) @@ -100,23 +116,19 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ public function getMemberships(User $user) { - $groupStmt = $this->ds->select() - ->from($this->prependTablePrefix('group'), array('name', 'parent')) - ->getSelectQuery() - ->query(); $groups = array(); - foreach ($groupStmt as $group) { + foreach ($this->ds->select()->from($this->prependTablePrefix('group'), array('name', 'parent')) as $group) { + // Using the raw query here due to the non-existent necessity to join, convert, or... $groups[$group->name] = $group->parent; } - $membershipStmt = $this->ds->select() // TODO: Join this table - ->from($this->prependTablePrefix('group_membership'), array('group_name')) - ->where('username', $user->getUsername()) - ->getSelectQuery() - ->query(); - $memberships = array(); + $membershipQuery = $this + ->select() + ->from('group_membership', array('group_name')) + ->where('user_name', $user->getUsername()); - foreach ($membershipStmt as $membership) { + $memberships = array(); + foreach ($membershipQuery as $membership) { $memberships[] = $membership->group_name; $parent = $groups[$membership->group_name]; while ($parent !== null) { @@ -128,4 +140,32 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa return $memberships; } + + /** + * Join group into group_membership + * + * @param RepositoryQuery $query + */ + protected function joinGroup(RepositoryQuery $query) + { + $query->getQuery()->join( + $this->requireTable('group'), + 'gm.group_id = g.id', + array() + ); + } + + /** + * Join group_membership into group + * + * @param RepositoryQuery $query + */ + protected function joinGroupMembership(RepositoryQuery $query) + { + $query->getQuery()->join( + $this->requireTable('group_membership'), + 'g.id = gm.group_id', + array() + ); + } } From 385042ea92d2a79efa823cadf9311937b40c3be5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 16:27:48 +0200 Subject: [PATCH 195/239] RepositoryForm: Verify that the entry to work with is valid refs #8826 --- application/forms/RepositoryForm.php | 55 +++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php index 89b8940a4..136855d1e 100644 --- a/application/forms/RepositoryForm.php +++ b/application/forms/RepositoryForm.php @@ -5,6 +5,7 @@ namespace Icinga\Forms; use Exception; use Icinga\Data\Filter\Filter; +use Icinga\Exception\NotFoundError; use Icinga\Repository\Repository; use Icinga\Web\Form; use Icinga\Web\Notification; @@ -181,9 +182,25 @@ abstract class RepositoryForm extends Form } /** - * Populate the data of the entry being handled + * Prepare the form for the requested mode */ public function onRequest() + { + if ($this->shouldInsert()) { + $this->onInsertRequest(); + } elseif ($this->shouldUpdate()) { + $this->onUpdateRequest(); + } elseif ($this->shouldDelete()) { + $this->onDeleteRequest(); + } + } + + /** + * Prepare the form for mode insert + * + * Populates the form with the data passed to add(). + */ + protected function onInsertRequest() { $data = $this->getData(); if (! empty($data)) { @@ -191,6 +208,42 @@ abstract class RepositoryForm extends Form } } + /** + * Prepare the form for mode update + * + * Populates the form with either the data passed to edit() or tries to fetch it from the repository. + * + * @throws NotFoundError In case the entry to update cannot be found + */ + protected function onUpdateRequest() + { + $data = $this->getData(); + if (empty($data)) { + $row = $this->repository->select()->applyFilter($this->createFilter())->fetchRow(); + if ($row === false) { + throw new NotFoundError('Entry "%s" not found', $this->getIdentifier()); + } + + $data = get_object_vars($row); + } + + $this->populate($data); + } + + /** + * Prepare the form for mode delete + * + * Verifies that the repository contains the entry to delete. + * + * @throws NotFoundError In case the entry to delete cannot be found + */ + protected function onDeleteRequest() + { + if ($this->repository->select()->addFilter($this->createFilter())->count() === 0) { + throw new NotFoundError('Entry "%s" not found', $this->getIdentifier()); + } + } + /** * Apply the requested mode on the repository * From ad6b4016f0a0454cac25fc24881acb294b3fc7f5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 16:28:43 +0200 Subject: [PATCH 196/239] UserController: Let the form validate the user's name refs #8826 --- application/controllers/UserController.php | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index db60d4e96..05adf03d7 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -4,6 +4,7 @@ use \Exception; use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotFoundError; use Icinga\Forms\Config\User\CreateMembershipForm; use Icinga\Forms\Config\User\UserForm; use Icinga\Data\DataArray\ArrayDatasource; @@ -185,15 +186,15 @@ class UserController extends AuthBackendController $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); - $row = $backend->select(array('user_name', 'is_active'))->where('user_name', $userName)->fetchRow(); - if ($row === false) { - $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); - } - $form = new UserForm(); $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName))); $form->setRepository($backend); - $form->edit($userName, get_object_vars($row))->handleRequest(); + + try { + $form->edit($userName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } $this->view->form = $form; $this->render('form'); @@ -208,14 +209,15 @@ class UserController extends AuthBackendController $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); - if ($backend->select()->where('user_name', $userName)->count() === 0) { - $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); - } - $form = new UserForm(); $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); $form->setRepository($backend); - $form->remove($userName)->handleRequest(); + + try { + $form->remove($userName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } $this->view->form = $form; $this->render('form'); From 2c9af2fb81826d06af999f29608ccbcdb288c446 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 28 May 2015 16:28:59 +0200 Subject: [PATCH 197/239] GroupController: Let the form validate a group's name refs #8826 --- application/controllers/GroupController.php | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 2ae15c967..360ff17dd 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -6,6 +6,7 @@ use Icinga\Application\Logger; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Data\Reducible; use Icinga\Data\Filter\Filter; +use Icinga\Exception\NotFoundError; use Icinga\Forms\Config\UserGroup\AddMemberForm; use Icinga\Forms\Config\UserGroup\UserGroupForm; use Icinga\Web\Controller\AuthBackendController; @@ -185,17 +186,17 @@ class GroupController extends AuthBackendController $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); - $row = $backend->select(array('group_name'))->where('group_name', $groupName)->fetchRow(); - if ($row === false) { - $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); - } - $form = new UserGroupForm(); $form->setRedirectUrl( Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) ); $form->setRepository($backend); - $form->edit($groupName, get_object_vars($row))->handleRequest(); + + try { + $form->edit($groupName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } $this->view->form = $form; $this->render('form'); @@ -210,14 +211,15 @@ class GroupController extends AuthBackendController $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); - if ($backend->select()->where('group_name', $groupName)->count() === 0) { - $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); - } - $form = new UserGroupForm(); $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); $form->setRepository($backend); - $form->remove($groupName)->handleRequest(); + + try { + $form->remove($groupName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } $this->view->form = $form; $this->render('form'); From 5e0ae6410b19096f0a3ffa142bdead841d0d31ff Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 08:02:12 +0200 Subject: [PATCH 198/239] DbRepository: Consider a filter column without query context as a statement column Feels wrong..somehow, but it works. refs #8826 --- library/Icinga/Repository/DbRepository.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 38099b0e7..c43e8df54 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -529,7 +529,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, * @param string $table The table where to look for the column or alias * @param string $name The name or alias of the column to validate * @param RepositoryQuery $query An optional query to pass as context, - * if not given no join will be attempted + * if not given the column is considered being used for a statement filter * * @return string The given column's name * @@ -537,7 +537,11 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ public function requireFilterColumn($table, $name, RepositoryQuery $query = null) { - if ($query === null || $this->validateQueryColumnAssociation($table, $name)) { + if ($query === null) { + return $this->requireStatementColumn($table, $name); + } + + if ($this->validateQueryColumnAssociation($table, $name)) { return parent::requireFilterColumn($table, $name, $query); } From b123afe5943b6b6798ae492c9cb0554046c1f9b5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 08:54:32 +0200 Subject: [PATCH 199/239] mysql schema: Make parent column a foreign key as well refs #8826 --- etc/schema/mysql.schema.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/etc/schema/mysql.schema.sql b/etc/schema/mysql.schema.sql index bbc801d8b..5f22aead3 100644 --- a/etc/schema/mysql.schema.sql +++ b/etc/schema/mysql.schema.sql @@ -3,11 +3,13 @@ CREATE TABLE `icingaweb_group`( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL, - `parent` varchar(64) COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `parent` int(10) unsigned NULL DEFAULT NULL, `ctime` timestamp NULL DEFAULT NULL, `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `idx_name` (`name`) + UNIQUE KEY `idx_name` (`name`), + CONSTRAINT `fk_icingaweb_group_parent_id` FOREIGN KEY (`parent`) + REFERENCES `icingaweb_group` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `icingaweb_group_membership`( From 9fcebb001498307d4fe9b4e8af9155df13d5ef9d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 08:54:45 +0200 Subject: [PATCH 200/239] pgsql schema: Make parent column a foreign key as well refs #8826 --- etc/schema/pgsql.schema.sql | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/etc/schema/pgsql.schema.sql b/etc/schema/pgsql.schema.sql index 09ecb0b71..7f28c5d81 100644 --- a/etc/schema/pgsql.schema.sql +++ b/etc/schema/pgsql.schema.sql @@ -3,7 +3,7 @@ CREATE TABLE "icingaweb_group" ( "id" serial, "name" character varying(64) NOT NULL, - "parent" character varying(64) NULL DEFAULT NULL, + "parent" int NULL DEFAULT NULL, "ctime" timestamp NULL DEFAULT NULL, "mtime" timestamp NULL DEFAULT NULL ); @@ -20,6 +20,15 @@ CREATE UNIQUE INDEX idx_icingaweb_group lower((name)::text) ); +ALTER TABLE ONLY "icingaweb_group" + ADD CONSTRAINT fk_icingaweb_group_parent_id + FOREIGN KEY ( + "parent" + ) + REFERENCES "icingaweb_group" ( + "id" +); + CREATE TABLE "icingaweb_group_membership" ( "group_id" int NOT NULL, "username" character varying(64) NOT NULL, From c94e6a329275d7502e09a0eeebd73a8f2dc2e4c1 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 08:56:58 +0200 Subject: [PATCH 201/239] Db/IniUserGroupBackend: Drop column parent_name, it's not a name anymore refs #8826 --- application/controllers/GroupController.php | 2 -- application/views/scripts/group/show.phtml | 11 ----------- .../Authentication/UserGroup/DbUserGroupBackend.php | 7 +++---- .../Authentication/UserGroup/IniUserGroupBackend.php | 5 ++--- 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 360ff17dd..8a13bfd6f 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -75,7 +75,6 @@ class GroupController extends AuthBackendController $this->setupSortControl( array( 'group_name' => $this->translate('Group'), - 'parent_name' => $this->translate('Parent'), 'created_at' => $this->translate('Created at'), 'last_modified' => $this->translate('Last modified') ), @@ -94,7 +93,6 @@ class GroupController extends AuthBackendController $group = $backend->select(array( 'group_name', - 'parent_name', 'created_at', 'last_modified' ))->where('group_name', $groupName)->fetchRow(); diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 75895b908..1fd3e48c6 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -29,17 +29,6 @@ if ($this->hasPermission('config/application/groups/edit') && $backend instanceo

    escape($group->group_name); ?>

    -

    translate('Parent'); ?>: parent_name === null ? '-' : $this->qlink( - $group->parent_name, - 'group/show', - array( - 'backend' => $backend->getName(), - 'group' => $group->parent_name - ), - array( - 'title' => sprintf($this->translate('Show detailed information for group %s'), $group->parent_name) - ) - ); ?>

    translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($group->created_at); ?>

    translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?>

    diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index d3096dadd..2cf177e01 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -20,8 +20,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa 'group_id' => 'g.id', 'group' => 'g.name COLLATE utf8_general_ci', 'group_name' => 'g.name', - 'parent' => 'g.parent COLLATE utf8_general_ci', - 'parent_name' => 'g.parent', + 'parent' => 'g.parent', 'created_at' => 'UNIX_TIMESTAMP(g.ctime)', 'last_modified' => 'UNIX_TIMESTAMP(g.mtime)' ), @@ -53,7 +52,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa 'group' => array( 'group_id' => 'id', 'group_name' => 'name', - 'parent_name' => 'parent', + 'parent' => 'parent', 'created_at' => 'ctime', 'last_modified' => 'mtime' ), @@ -70,7 +69,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa * * @var array */ - protected $filterColumns = array('group', 'parent', 'user'); + protected $filterColumns = array('group', 'user'); /** * Initialize this database user group backend diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index a9d73038c..f17cbb6a8 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -21,7 +21,6 @@ class IniUserGroupBackend extends IniRepository implements UserGroupBackendInter 'group' => 'name', 'group_name' => 'name', 'parent' => 'parent', - 'parent_name' => 'parent', 'created_at' => 'ctime', 'last_modified' => 'mtime', 'users' @@ -33,7 +32,7 @@ class IniUserGroupBackend extends IniRepository implements UserGroupBackendInter * * @var array */ - protected $filterColumns = array('group', 'parent'); + protected $filterColumns = array('group'); /** * The value conversion rules to apply on a query @@ -96,7 +95,7 @@ class IniUserGroupBackend extends IniRepository implements UserGroupBackendInter $groups = array(); foreach ($result as $group) { - $groups[$group->group_name] = $group->parent_name; + $groups[$group->group_name] = $group->parent; } $username = strtolower($user->getUsername()); From 60ce78c958e517f6711d7308e54313c2cb48f1bb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 08:57:49 +0200 Subject: [PATCH 202/239] DbUserGroupBackend: Adjust how to load the name of a group's parent refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index 2cf177e01..10e2b8036 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -115,10 +115,23 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ public function getMemberships(User $user) { + $groupQuery = $this->ds + ->select() + ->from( + array('g' => $this->prependTablePrefix('group')), + array( + 'group_name' => 'g.name', + 'parent_name' => 'gg.name' + ) + )->joinLeft( + array('gg' => $this->prependTablePrefix('group')), + 'g.parent = gg.id', + array() + ); + $groups = array(); - foreach ($this->ds->select()->from($this->prependTablePrefix('group'), array('name', 'parent')) as $group) { - // Using the raw query here due to the non-existent necessity to join, convert, or... - $groups[$group->name] = $group->parent; + foreach ($groupQuery as $group) { + $groups[$group->group_name] = $group->parent_name; } $membershipQuery = $this From bb285db05beabe0ce2a4b0dee258295e36716bc9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 11:32:15 +0200 Subject: [PATCH 203/239] Differentiate the source or destination of a column when converting values refs #8826 --- .../Authentication/User/DbUserBackend.php | 8 +++- .../UserGroup/IniUserGroupBackend.php | 10 +++-- library/Icinga/Repository/DbRepository.php | 42 +++++++++++++++++++ library/Icinga/Repository/Repository.php | 38 ++++++++++------- library/Icinga/Repository/RepositoryQuery.php | 28 ++++++------- 5 files changed, 90 insertions(+), 36 deletions(-) diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 7a3de8966..8a7991a0f 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -76,11 +76,15 @@ class DbUserBackend extends DbRepository implements UserBackendInterface ); /** - * The value conversion rules to apply on a query/statement + * The value conversion rules to apply on a query or statement * * @var array */ - protected $conversionRules = array('password'); + protected $conversionRules = array( + 'user' => array( + 'password' + ) + ); /** * Initialize this database user backend diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php index f17cbb6a8..53eadc147 100644 --- a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php @@ -35,14 +35,16 @@ class IniUserGroupBackend extends IniRepository implements UserGroupBackendInter protected $filterColumns = array('group'); /** - * The value conversion rules to apply on a query + * The value conversion rules to apply on a query or statement * * @var array */ protected $conversionRules = array( - 'created_at' => 'date_time', - 'last_modified' => 'date_time', - 'users' => 'comma_separated_string' + 'groups' => array( + 'created_at' => 'date_time', + 'last_modified' => 'date_time', + 'users' => 'comma_separated_string' + ) ); /** diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index c43e8df54..9131b5052 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -401,6 +401,46 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, } } + /** + * Return whether this repository is capable of converting values for the given table + * + * @param array|string $table + * + * @return bool + */ + public function providesValueConversion($table) + { + return parent::providesValueConversion($this->removeTablePrefix($this->clearTableAlias($table))); + } + + /** + * Return the name of the conversion method for the given alias or column name and context + * + * @param array|string $table The datasource's table + * @param string $name The alias or column name for which to return a conversion method + * @param string $context The context of the conversion: persist or retrieve + * + * @return string + * + * @throws ProgrammingError In case a conversion rule is found but not any conversion method + */ + protected function getConverter($table, $name, $context) + { + if ( + $this->validateQueryColumnAssociation($table, $name) + || $this->validateStatementColumnAssociation($table, $name) + ) { + $table = $this->removeTablePrefix($this->clearTableAlias($table)); + } else { + $table = $this->findTableName($name); + if (! $table) { + throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?'); + } + } + + return parent::getConverter($table, $name, $context); + } + /** * Validate that the requested table exists * @@ -692,6 +732,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $aliasTableMap[$column]; } + // TODO(jom): Elaborate whether it makes sense to throw ProgrammingError + // instead (duplicate aliases in different tables?) foreach ($aliasTableMap as $alias => $table) { if (strpos($alias, '.') !== false) { list($_, $alias) = split('.', $column, 2); diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 7fba11727..7568a17f6 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -106,7 +106,7 @@ abstract class Repository implements Selectable protected $sortRules; /** - * The value conversion rules to apply on a query + * The value conversion rules to apply on a query or statement * * This may be initialized by concrete repository implementations and describes for which aliases or column * names what type of conversion is available. For entries, where the key is the alias/column and the value @@ -403,27 +403,30 @@ abstract class Repository implements Selectable } /** - * Return whether this repository is capable of converting values + * Return whether this repository is capable of converting values for the given table + * + * @param string $table * * @return bool */ - public function providesValueConversion() + public function providesValueConversion($table) { $conversionRules = $this->getConversionRules(); - return !empty($conversionRules); + return !empty($conversionRules) && isset($conversionRules[$table]); } /** * Convert a value supposed to be transmitted to the data source * + * @param string $table The table where to persist the value * @param string $name The alias or column name * @param mixed $value The value to convert * * @return mixed If conversion was possible, the converted value, otherwise the unchanged value */ - public function persistColumn($name, $value) + public function persistColumn($table, $name, $value) { - $converter = $this->getConverter($name, 'persist'); + $converter = $this->getConverter($table, $name, 'persist'); if ($converter !== null) { $value = $this->$converter($value); } @@ -434,14 +437,15 @@ abstract class Repository implements Selectable /** * Convert a value which was fetched from the data source * + * @param string $table The table the value has been fetched from * @param string $name The alias or column name * @param mixed $value The value to convert * * @return mixed If conversion was possible, the converted value, otherwise the unchanged value */ - public function retrieveColumn($name, $value) + public function retrieveColumn($table, $name, $value) { - $converter = $this->getConverter($name, 'retrieve'); + $converter = $this->getConverter($table, $name, 'retrieve'); if ($converter !== null) { $value = $this->$converter($value); } @@ -452,6 +456,7 @@ abstract class Repository implements Selectable /** * Return the name of the conversion method for the given alias or column name and context * + * @param string $table The datasource's table * @param string $name The alias or column name for which to return a conversion method * @param string $context The context of the conversion: persist or retrieve * @@ -459,12 +464,13 @@ abstract class Repository implements Selectable * * @throws ProgrammingError In case a conversion rule is found but not any conversion method */ - protected function getConverter($name, $context) + protected function getConverter($table, $name, $context) { $conversionRules = $this->getConversionRules(); + $tableRules = $conversionRules[$table]; // Check for a conversion method for the alias/column first - if (array_key_exists($name, $conversionRules) || in_array($name, $conversionRules)) { + if (array_key_exists($name, $tableRules) || in_array($name, $tableRules)) { $methodName = $context . join('', array_map('ucfirst', explode('_', $name))); if (method_exists($this, $methodName)) { return $methodName; @@ -472,22 +478,22 @@ abstract class Repository implements Selectable } // The conversion method for the type is just a fallback, but it is required to exist if defined - if (isset($conversionRules[$name])) { - $identifier = join('', array_map('ucfirst', explode('_', $conversionRules[$name]))); + if (isset($tableRules[$name])) { + $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$name]))); if (! method_exists($this, $context . $identifier)) { // Do not throw an error in case at least one conversion method exists if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) { throw new ProgrammingError( 'Cannot find any conversion method for type "%s"' . '. Add a proper conversion method or remove the type definition', - $conversionRules[$name] + $tableRules[$name] ); } Logger::debug( 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".', $context . $identifier, - $conversionRules[$name], + $tableRules[$name], $this->getName() ); } else { @@ -623,7 +629,7 @@ abstract class Repository implements Selectable if ($filter->isExpression()) { $column = $filter->getColumn(); $filter->setColumn($this->requireFilterColumn($table, $column, $query)); - $filter->setExpression($this->persistColumn($column, $filter->getExpression())); + $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression())); } elseif ($filter->isChain()) { foreach ($filter->filters() as $chainOrExpression) { $this->requireFilter($table, $chainOrExpression, $query); @@ -826,7 +832,7 @@ abstract class Repository implements Selectable { $resolved = array(); foreach ($data as $alias => $value) { - $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($alias, $value); + $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value); } return $resolved; diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 8048ce434..9a8eb7e83 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -153,7 +153,7 @@ class RepositoryQuery implements QueryInterface, Iterator { $this->query->where( $this->repository->requireFilterColumn($this->target, $column, $this), - $this->repository->persistColumn($column, $value) + $this->repository->persistColumn($this->target, $column, $value) ); return $this; } @@ -388,10 +388,10 @@ class RepositoryQuery implements QueryInterface, Iterator } $result = $this->query->fetchOne(); - if ($result !== false && $this->repository->providesValueConversion()) { + if ($result !== false && $this->repository->providesValueConversion($this->target)) { $columns = $this->getColumns(); $column = isset($columns[0]) ? $columns[0] : key($columns); - return $this->repository->retrieveColumn($column, $result); + return $this->repository->retrieveColumn($this->target, $column, $result); } return $result; @@ -409,13 +409,13 @@ class RepositoryQuery implements QueryInterface, Iterator } $result = $this->query->fetchRow(); - if ($result !== false && $this->repository->providesValueConversion()) { + if ($result !== false && $this->repository->providesValueConversion($this->target)) { foreach ($this->getColumns() as $alias => $column) { if (! is_string($alias)) { $alias = $column; } - $result->$alias = $this->repository->retrieveColumn($alias, $result->$alias); + $result->$alias = $this->repository->retrieveColumn($this->target, $alias, $result->$alias); } } @@ -434,12 +434,12 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchColumn(); - if (! empty($results) && $this->repository->providesValueConversion()) { + if (! empty($results) && $this->repository->providesValueConversion($this->target)) { $columns = $this->getColumns(); $aliases = array_keys($columns); $column = is_int($aliases[0]) ? $columns[0] : $aliases[0]; foreach ($results as & $value) { - $value = $this->repository->retrieveColumn($column, $value); + $value = $this->repository->retrieveColumn($this->target, $column, $value); } } @@ -460,15 +460,15 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchPairs(); - if (! empty($results) && $this->repository->providesValueConversion()) { + if (! empty($results) && $this->repository->providesValueConversion($this->target)) { $columns = $this->getColumns(); $aliases = array_keys($columns); $newResults = array(); foreach ($results as $colOneValue => $colTwoValue) { $colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0]; $colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]); - $colOneValue = $this->repository->retrieveColumn($colOne, $colOneValue); - $newResults[$colOneValue] = $this->repository->retrieveColumn($colTwo, $colTwoValue); + $colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue); + $newResults[$colOneValue] = $this->repository->retrieveColumn($this->target, $colTwo, $colTwoValue); } $results = $newResults; @@ -489,7 +489,7 @@ class RepositoryQuery implements QueryInterface, Iterator } $results = $this->query->fetchAll(); - if (! empty($results) && $this->repository->providesValueConversion()) { + if (! empty($results) && $this->repository->providesValueConversion($this->target)) { $columns = $this->getColumns(); foreach ($results as $row) { foreach ($columns as $alias => $column) { @@ -497,7 +497,7 @@ class RepositoryQuery implements QueryInterface, Iterator $alias = $column; } - $row->$alias = $this->repository->retrieveColumn($alias, $row->$alias); + $row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias); } } } @@ -545,13 +545,13 @@ class RepositoryQuery implements QueryInterface, Iterator public function current() { $row = $this->iterator->current(); - if ($this->repository->providesValueConversion()) { + if ($this->repository->providesValueConversion($this->target)) { foreach ($this->getColumns() as $alias => $column) { if (! is_string($alias)) { $alias = $column; } - $row->$alias = $this->repository->retrieveColumn($alias, $row->$alias); + $row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias); } } From a88037f45d42311394fce7ce9c352332399c18e7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 11:33:35 +0200 Subject: [PATCH 204/239] DbUserGroupBackend: Fetch and persist a group's id when it's name is given refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index 10e2b8036..e77d50f78 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -4,6 +4,7 @@ namespace Icinga\Authentication\UserGroup; use Icinga\Data\Filter\Filter; +use Icinga\Exception\NotFoundError; use Icinga\Repository\DbRepository; use Icinga\Repository\RepositoryQuery; use Icinga\User; @@ -58,6 +59,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa ), 'group_membership' => array( 'group_id' => 'group_id', + 'group_name' => 'group_id', 'user_name' => 'username', 'created_at' => 'ctime', 'last_modified' => 'mtime' @@ -71,6 +73,20 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ protected $filterColumns = array('group', 'user'); + /** + * The value conversion rules to apply on a query or statement + * + * @var array + */ + protected $conversionRules = array( + 'group' => array( + 'parent' => 'group_id' + ), + 'group_membership' => array( + 'group_name' => 'group_id' + ) + ); + /** * Initialize this database user group backend */ @@ -180,4 +196,31 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa array() ); } + + /** + * Fetch and return the corresponding id for the given group's name + * + * @param string $groupName + * + * @return int + * + * @throws NotFoundError In case no group with the given name is found + */ + protected function persistGroupId($groupName) + { + if (is_int($groupName)) { + return $groupName; // It's obviously already an id + } + + $groupId = $this->ds + ->select() + ->from($this->prependTablePrefix('group'), array('id')) + ->where('name', $groupName) + ->fetchOne(); + if ($groupId === false) { + throw new NotFoundError('Group "%s" does not exist', $groupName); + } + + return $groupId; + } } From 3959dc27d7128140150e8212c8d6785da9e7872c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 11:36:08 +0200 Subject: [PATCH 205/239] Repository: Do not return filter columns when requiring all query columns I really wonder why I did not notice this until now... refs #8826 --- library/Icinga/Repository/Repository.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 7568a17f6..d615fd149 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -653,7 +653,15 @@ abstract class Repository implements Selectable throw new ProgrammingError('Table name "%s" not found', $table); } - return $queryColumns[$table]; + $filterColumns = $this->getFilterColumns(); + $columns = array(); + foreach ($queryColumns[$table] as $alias => $column) { + if (! in_array(is_string($alias) ? $alias : $column, $filterColumns)) { + $columns[$alias] = $column; + } + } + + return $columns; } /** From b82d7d7cc0da17ec41162fb1f40182dcabe7fd07 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 11:37:42 +0200 Subject: [PATCH 206/239] DbRepository: split() is deprecated, use explode() instead Why php, why? :'( refs #8826 --- library/Icinga/Repository/DbRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 9131b5052..6c7a7b22b 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -736,7 +736,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, // instead (duplicate aliases in different tables?) foreach ($aliasTableMap as $alias => $table) { if (strpos($alias, '.') !== false) { - list($_, $alias) = split('.', $column, 2); + list($_, $alias) = explode('.', $column, 2); if ($alias === $column) { return $table; } From cc779024fe78070b8c2ff420716f4da8626f361b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 11:57:38 +0200 Subject: [PATCH 207/239] Repository: providesValueConversion() should not be required being called refs #8826 --- library/Icinga/Repository/Repository.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index d615fd149..ccb3da96a 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -467,6 +467,10 @@ abstract class Repository implements Selectable protected function getConverter($table, $name, $context) { $conversionRules = $this->getConversionRules(); + if (! isset($conversionRules[$table])) { + return; + } + $tableRules = $conversionRules[$table]; // Check for a conversion method for the alias/column first From c8e8a39f5a08de37cb1e51fa88336c90d804adfc Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 11:58:21 +0200 Subject: [PATCH 208/239] DbRepository: Relax check in providesValueConversion() Would otherwise conflict when tables are joined in. refs #8826 --- library/Icinga/Repository/DbRepository.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 6c7a7b22b..e2ba7d994 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -402,15 +402,19 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, } /** - * Return whether this repository is capable of converting values for the given table + * Return whether this repository is capable of converting values + * + * This does not check whether any conversion for the given table is available, as it may be possible + * that columns from another table where joined in which would otherwise not being converted. * * @param array|string $table * * @return bool */ - public function providesValueConversion($table) + public function providesValueConversion($_) { - return parent::providesValueConversion($this->removeTablePrefix($this->clearTableAlias($table))); + $conversionRules = $this->getConversionRules(); + return !empty($conversionRules); } /** From cb4d6f013a10be29b695d0c308ececc42f690a9c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 12:57:39 +0200 Subject: [PATCH 209/239] GroupController: Properly handle 404's when handling group members refs #8826 --- application/controllers/GroupController.php | 26 +++++++++++-------- .../forms/Config/UserGroup/AddMemberForm.php | 3 +++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 8a13bfd6f..0c2a4a45d 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -232,10 +232,6 @@ class GroupController extends AuthBackendController $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); - if ($backend->select()->where('group_name', $groupName)->count() === 0) { - $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); - } - $form = new AddMemberForm(); $form->setDataSource($this->fetchUsers()) ->setBackend($backend) @@ -243,8 +239,13 @@ class GroupController extends AuthBackendController ->setRedirectUrl( Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) ) - ->setUidDisabled() - ->handleRequest(); + ->setUidDisabled(); + + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } $this->view->form = $form; $this->render('form'); @@ -260,10 +261,6 @@ class GroupController extends AuthBackendController $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); - if ($backend->select()->where('group_name', $groupName)->count() === 0) { - $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); - } - $form = new Form(array( 'onSuccess' => function ($form) use ($groupName, $backend) { foreach ($form->getValue('user_name') as $userName) { @@ -280,6 +277,8 @@ class GroupController extends AuthBackendController $userName, $groupName )); + } catch (NotFoundError $e) { + throw $e; } catch (Exception $e) { Notification::error($e->getMessage()); } @@ -297,7 +296,12 @@ class GroupController extends AuthBackendController $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true)); $form->addElement('hidden', 'redirect'); - $form->handleRequest(); + + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } } /** diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php index 6a57df3b4..88064dcdd 100644 --- a/application/forms/Config/UserGroup/AddMemberForm.php +++ b/application/forms/Config/UserGroup/AddMemberForm.php @@ -7,6 +7,7 @@ use Exception; use Icinga\Data\Extensible; use Icinga\Data\Filter\Filter; use Icinga\Data\Selectable; +use Icinga\Exception\NotFoundError; use Icinga\Web\Form; use Icinga\Web\Notification; @@ -155,6 +156,8 @@ class AddMemberForm extends Form 'user_name' => $userName ) ); + } catch (NotFoundError $e) { + throw $e; // Trigger 404, the group name is initially accessed as GET parameter } catch (Exception $e) { Notification::error(sprintf( $this->translate('Failed to add "%s" as group member for "%s"'), From 04835db13eae1f6efc2019f30d80018e19a8fcc4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 16:34:34 +0200 Subject: [PATCH 210/239] Introduce form UserGroupBackendForm refs #8826 --- .../UserGroup/DbUserGroupBackendForm.php | 58 +++++ .../Config/UserGroup/UserGroupBackendForm.php | 198 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 application/forms/Config/UserGroup/DbUserGroupBackendForm.php create mode 100644 application/forms/Config/UserGroup/UserGroupBackendForm.php diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php new file mode 100644 index 000000000..9f1915968 --- /dev/null +++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php @@ -0,0 +1,58 @@ +setName('form_config_dbusergroupbackend'); + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $resourceNames = $this->getDatabaseResourceNames(); + $this->addElement( + 'select', + 'resource', + array( + 'required' => true, + 'label' => $this->translate('Database Connection'), + 'description' => $this->translate('The database connection to use for this backend'), + 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames) + ) + ); + } + + /** + * Return the names of all configured database resources + * + * @return array + */ + protected function getDatabaseResourceNames() + { + $names = array(); + foreach (ResourceFactory::getResourceConfigs() as $name => $config) { + if (strtolower($config->type) === 'db') { + $names[] = $name; + } + } + + return $names; + } +} diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php new file mode 100644 index 000000000..dd9875c9a --- /dev/null +++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php @@ -0,0 +1,198 @@ +setName('form_config_usergroupbackend'); + $this->setSubmitLabel($this->translate('Save Changes')); + } + + /** + * Return a form object for the given backend type + * + * @param string $type The backend type for which to return a form + * + * @return Form + */ + public function getBackendForm($type) + { + if ($type === 'db') { + return new DbUserGroupBackendForm(); + } else { + throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type)); + } + } + + /** + * Populate the form with the given backend's config + * + * @param string $name + * + * @return $this + * + * @throws NotFoundError In case no backend with the given name is found + */ + public function load($name) + { + if (! $this->config->hasSection($name)) { + throw new NotFoundError('No user group backend called "%s" found', $name); + } + + $data = $this->config->getSection($name)->toArray(); + $data['type'] = $data['backend']; + $data['name'] = $name; + $this->populate($data); + return $this; + } + + /** + * Add a new user group backend + * + * @param array $data + * + * @return $this + * + * @throws InvalidArgumentException In case $data does not contain a backend name + * @throws IcingaException In case a backend with the same name already exists + */ + public function add(array $data) + { + if (! isset($data['name'])) { + throw new InvalidArgumentException('Key \'name\' missing'); + } + + $backendName = $data['name']; + if ($this->config->hasSection($backendName)) { + throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName); + } + + unset($data['name']); + $this->config->setSection($backendName, $data); + return $this; + } + + /** + * Edit a user group backend + * + * @param string $name + * @param array $data + * + * @return $this + * + * @throws NotFoundError In case no backend with the given name is found + */ + public function edit($name, array $data) + { + if (! $this->config->hasSection($name)) { + throw new NotFoundError('No user group backend called "%s" found', $name); + } + + $backendConfig = $this->config->getSection($name); + if (isset($data['name']) && $data['name'] !== $name) { + $this->config->removeSection($name); + $name = $data['name']; + unset($data['name']); + } + + $this->config->setSection($name, $backendConfig->merge($data)); + return $this; + } + + /** + * Remove a user group backend + * + * @param string $name + * + * @return $this + */ + public function delete($name) + { + $this->config->removeSection($name); + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => $this->translate('Backend Name'), + 'description' => $this->translate( + 'The name of this user group backend that is used to differentiate it from others' + ), + 'validators' => array( + array( + 'Regex', + false, + array( + 'pattern' => '/^[^\\[\\]:]+$/', + 'messages' => array( + 'regexNotMatch' => $this->translate( + 'The backend name cannot contain \'[\', \']\' or \':\'.' + ) + ) + ) + ) + ) + ) + ); + + // TODO(jom): We did not think about how to configure custom group backends yet! + $backendTypes = array( + 'db' => $this->translate('Database') + ); + + $backendType = isset($formData['type']) ? $formData['type'] : null; + if ($backendType === null) { + $backendType = key($backendTypes); + } + + $this->addElement( + 'hidden', + 'backend', + array( + 'disabled' => true, // Prevents the element from being submitted, see #7717 + 'value' => $backendType + ) + ); + + $this->addElement( + 'select', + 'type', + array( + 'ignore' => true, + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Backend Type'), + 'description' => $this->translate('The type of this user group backend'), + 'multiOptions' => $backendTypes + ) + ); + + $backendForm = $this->getBackendForm($backendType); + $backendForm->createElements($formData); + $this->addElements($backendForm->getElements()); + } +} From ea959c2dfde844620b81a53fd6664a5af30802ca Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 16:35:30 +0200 Subject: [PATCH 211/239] Introduce controller UsergroupbackendController refs #8826 --- .../UsergroupbackendController.php | 149 ++++++++++++++++++ application/forms/Security/RoleForm.php | 44 +++--- .../views/scripts/usergroupbackend/form.phtml | 6 + .../views/scripts/usergroupbackend/list.phtml | 63 ++++++++ public/css/icinga/main-content.less | 11 ++ 5 files changed, 253 insertions(+), 20 deletions(-) create mode 100644 application/controllers/UsergroupbackendController.php create mode 100644 application/views/scripts/usergroupbackend/form.phtml create mode 100644 application/views/scripts/usergroupbackend/list.phtml diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php new file mode 100644 index 000000000..91560bf55 --- /dev/null +++ b/application/controllers/UsergroupbackendController.php @@ -0,0 +1,149 @@ +redirectNow('usergroupbackend/list'); + } + + /** + * Show a list of all user group backends + */ + public function listAction() + { + $this->assertPermission('config/application/usergroupbackend/*'); + $this->view->backendNames = Config::app('groups')->keys(); + $this->getTabs()->add( + 'usergroupbackend/list', + array( + 'title' => $this->translate('List all user group backends'), + 'label' => $this->translate('User group backends'), + 'url' => 'usergroupbackend/list' + ) + )->activate('usergroupbackend/list'); + } + + /** + * Create a new user group backend + */ + public function createAction() + { + $this->assertPermission('config/application/usergroupbackend/create'); + + $form = new UserGroupBackendForm(); + $form->setRedirectUrl('usergroupbackend/list'); + $form->setTitle($this->translate('Create New User Group Backend')); + $form->addDescription($this->translate('Create a new backend to associate users and groups with.')); + $form->setIniConfig(Config::app('groups')); + $form->setOnSuccess(function (UserGroupBackendForm $form) { + try { + $form->add($form->getValues()); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(t('User group backend successfully created')); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Edit an user group backend + */ + public function editAction() + { + $this->assertPermission('config/application/usergroupbackend/edit'); + $backendName = $this->params->getRequired('backend'); + + $form = new UserGroupBackendForm(); + $form->setAction(Url::fromRequest()); + $form->setRedirectUrl('usergroupbackend/list'); + $form->setTitle(sprintf($this->translate('Edit User Group Backend %s'), $backendName)); + $form->setIniConfig(Config::app('groups')); + $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) { + try { + $form->edit($backendName, $form->getValues()); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName)); + return true; + } + + return false; + }); + + try { + $form->load($backendName); + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName)); + } + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Remove a user group backend + */ + public function removeAction() + { + $this->assertPermission('config/application/usergroupbackend/remove'); + $backendName = $this->params->getRequired('backend'); + + $backendForm = new UserGroupBackendForm(); + $backendForm->setIniConfig(Config::app('groups')); + $form = new ConfirmRemovalForm(); + $form->setRedirectUrl('usergroupbackend/list'); + $form->setTitle(sprintf($this->translate('Remove User Group Backend %s'), $backendName)); + $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) { + try { + $backendForm->delete($backendName); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($backendForm->save()) { + Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName)); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } +} diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 536a7efe0..43ae6c1a0 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -21,26 +21,30 @@ class RoleForm extends ConfigForm * @var array */ protected $providedPermissions = array( - '*' => '*', - 'config/*' => 'config/*', - 'config/application/*' => 'config/application/*', - 'config/application/general' => 'config/application/general', - 'config/application/authentication' => 'config/application/authentication', - 'config/application/resources' => 'config/application/resources', - 'config/application/roles' => 'config/application/roles', - 'config/application/users/*' => 'config/application/users/*', - 'config/application/users/show' => 'config/application/users/show', - 'config/application/users/add' => 'config/application/users/add', - 'config/application/users/edit' => 'config/application/users/edit', - 'config/application/users/remove' => 'config/application/users/remove', - 'config/application/groups/*' => 'config/application/groups/*', - 'config/application/groups/show' => 'config/application/groups/show', - 'config/application/groups/add' => 'config/application/groups/add', - 'config/application/groups/edit' => 'config/application/groups/edit', - 'config/application/groups/remove' => 'config/application/groups/remove', - 'config/application/groups/member/add' => 'config/application/groups/member/add', - 'config/application/groups/member/remove' => 'config/application/groups/member/remove', - 'config/modules' => 'config/modules' + '*' => '*', + 'config/*' => 'config/*', + 'config/application/*' => 'config/application/*', + 'config/application/general' => 'config/application/general', + 'config/application/authentication' => 'config/application/authentication', + 'config/application/resources' => 'config/application/resources', + 'config/application/roles' => 'config/application/roles', + 'config/application/users/*' => 'config/application/users/*', + 'config/application/users/show' => 'config/application/users/show', + 'config/application/users/add' => 'config/application/users/add', + 'config/application/users/edit' => 'config/application/users/edit', + 'config/application/users/remove' => 'config/application/users/remove', + 'config/application/groups/*' => 'config/application/groups/*', + 'config/application/groups/show' => 'config/application/groups/show', + 'config/application/groups/add' => 'config/application/groups/add', + 'config/application/groups/edit' => 'config/application/groups/edit', + 'config/application/groups/remove' => 'config/application/groups/remove', + 'config/application/groups/member/add' => 'config/application/groups/member/add', + 'config/application/groups/member/remove' => 'config/application/groups/member/remove', + 'config/application/usergroupbackend/*' => 'config/application/usergroupbackend/*', + 'config/application/usergroupbackend/create' => 'config/application/usergroupbackend/create', + 'config/application/usergroupbackend/edit' => 'config/application/usergroupbackend/edit', + 'config/application/usergroupbackend/remove' => 'config/application/usergroupbackend/remove', + 'config/modules' => 'config/modules' ); /** diff --git a/application/views/scripts/usergroupbackend/form.phtml b/application/views/scripts/usergroupbackend/form.phtml new file mode 100644 index 000000000..cbf06590d --- /dev/null +++ b/application/views/scripts/usergroupbackend/form.phtml @@ -0,0 +1,6 @@ +
    + showOnlyCloseButton(); ?> +
    +
    + +
    \ No newline at end of file diff --git a/application/views/scripts/usergroupbackend/list.phtml b/application/views/scripts/usergroupbackend/list.phtml new file mode 100644 index 000000000..d98d207f1 --- /dev/null +++ b/application/views/scripts/usergroupbackend/list.phtml @@ -0,0 +1,63 @@ +hasPermission('config/application/usergroupbackend/create'); +$editPermitted = $this->hasPermission('config/application/usergroupbackend/edit'); +$removePermitted = $this->hasPermission('config/application/usergroupbackend/remove'); + +?> +
    + +
    +
    + +qlink( + $this->translate('Create A New User Group Backend'), + 'usergroupbackend/create', + null, + array( + 'icon' => 'plus' + ) +); ?> + + 0): ?> + + + + + + + + + + + + + + + + + + + +
    translate('Backend'); ?>translate('Remove'); ?>
    + + qlink( + $backendName, + 'usergroupbackend/edit', + array('backend' => $backendName), + array('title' => sprintf($this->translate('Edit user group backend %s'), $backendName)) + ); ?> + + escape($backendName); ?> + + qlink( + null, + 'usergroupbackend/remove', + array('backend' => $backendName), + array( + 'title' => sprintf($this->translate('Remove user group backend %s'), $backendName), + 'icon' => 'trash' + ) + ); ?>
    + +
    \ No newline at end of file diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less index e1a305509..2b0e2e50d 100644 --- a/public/css/icinga/main-content.less +++ b/public/css/icinga/main-content.less @@ -355,4 +355,15 @@ form.backend-selection { margin-left: 0; } } +} + +table.usergroupbackend-list { + th.backend-remove { + width: 8em; + text-align: right; + } + + td.backend-remove { + text-align: right; + } } \ No newline at end of file From d097f7fe8f94340346f1b4b9cd040193b479d23c Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 29 May 2015 16:36:05 +0200 Subject: [PATCH 212/239] Add menu entry for the user group backend configuration That's definitely just a placeholder... refs #8826 --- library/Icinga/Web/Menu.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 9fe7aa0a1..014909ff0 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -242,6 +242,11 @@ class Menu implements RecursiveIterator 'permission' => 'config/application/*', 'priority' => 300 )); + $section->add(t('UserGroupBackends'), array( + 'url' => 'usergroupbackend/list', + 'permission' => 'config/application/usergroupbackend/*', + 'priority' => 301 + )); $section->add(t('Configuration'), array( 'url' => 'config', 'permission' => 'config/application/*', From 7127d5eb398b6549dd97578ae21318fd891c9be2 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 12:20:36 +0200 Subject: [PATCH 213/239] Ldap\Connection: Connect automatically in case capabilities are not set yet --- library/Icinga/Protocol/Ldap/Connection.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 2f7cccd41..ba6203aab 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -617,6 +617,10 @@ class Connection implements Selectable */ public function getCapabilities() { + if ($this->capabilities === null) { + $this->connect(); // Populates $this->capabilities + } + return $this->capabilities; } From d1a5321d02df6531e6fa1a3bd3670e9fc2e29e12 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 12:23:16 +0200 Subject: [PATCH 214/239] LdapUserBackend: Fetch and interpret the correct attributes (ActiveDirectory) refs #8826 --- .../Authentication/User/LdapUserBackend.php | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 7fc61dbc1..dc6fe04b5 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -3,6 +3,8 @@ namespace Icinga\Authentication\User; +use DateTime; +use Icinga\Application\Logger; use Icinga\Data\ConfigObject; use Icinga\Exception\AuthenticationException; use Icinga\Exception\ProgrammingError; @@ -255,17 +257,102 @@ class LdapUserBackend extends Repository implements UserBackendInterface throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first'); } + if ($this->ds->getCapabilities()->hasAdOid()) { + $isActiveAttribute = 'userAccountControl'; + $createdAtAttribute = 'whenCreated'; + $lastModifiedAttribute = 'whenChanged'; + } else { + $isActiveAttribute = 'unknown'; + $createdAtAttribute = 'unknown'; + $lastModifiedAttribute = 'unknown'; + } + return array( $this->userClass => array( 'user' => $this->userNameAttribute, 'user_name' => $this->userNameAttribute, - 'is_active' => 'unknown', // msExchUserAccountControl == 2/512/514? <- AD LDAP - 'created_at' => 'whenCreated', // That's AD LDAP, - 'last_modified' => 'whenChanged' // what's OpenLDAP? + 'is_active' => $isActiveAttribute, + 'created_at' => $createdAtAttribute, + 'last_modified' => $lastModifiedAttribute ) ); } + /** + * Initialize this repository's conversion rules + * + * @return array + * + * @throws ProgrammingError In case $this->userClass has not been set yet + */ + protected function initializeConversionRules() + { + if ($this->userClass === null) { + throw new ProgrammingError('It is required to set the objectClass where to look for users first'); + } + + if ($this->ds->getCapabilities()->hasAdOid()) { + $stateConverter = 'user_account_control'; + $timeConverter = 'generalized_time'; + } else { + $timeConverter = null; + $stateConverter = null; + } + + return array( + $this->userClass => array( + 'is_active' => $stateConverter, + 'created_at' => $timeConverter, + 'last_modified' => $timeConverter + ) + ); + } + + /** + * Return whether the given userAccountControl value defines that a user is permitted to login + * + * @param string|null $value + * + * @return bool + */ + protected function retrieveUserAccountControl($value) + { + if ($value === null) { + return $value; + } + + $ADS_UF_ACCOUNTDISABLE = 2; + return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0; + } + + /** + * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation + * + * @param string|null $value + * + * @return int + */ + protected function retrieveGeneralizedTime($value) + { + if ($value === null) { + return $value; + } + + if ( + ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false + ) { + return $dateTime->getTimeStamp(); + } else { + Logger::debug(sprintf( + 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) for user backend "%s".', + $value, + $this->getName() + )); + } + } + /** * Probe the backend to test if authentication is possible * From 601b720a03543fd24cedb763585e64c3457e9f35 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 14:05:44 +0200 Subject: [PATCH 215/239] LdapUserBackend: Fetch and interpret the correct attributes (OpenLDAP) refs #8826 --- .../Authentication/User/LdapUserBackend.php | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index dc6fe04b5..154a33a44 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -262,9 +262,11 @@ class LdapUserBackend extends Repository implements UserBackendInterface $createdAtAttribute = 'whenCreated'; $lastModifiedAttribute = 'whenChanged'; } else { - $isActiveAttribute = 'unknown'; - $createdAtAttribute = 'unknown'; - $lastModifiedAttribute = 'unknown'; + // TODO(jom): Elaborate whether it is possible to add dynamic support for the ppolicy + $isActiveAttribute = 'shadowExpire'; + + $createdAtAttribute = 'createTimestamp'; + $lastModifiedAttribute = 'modifyTimestamp'; } return array( @@ -293,17 +295,15 @@ class LdapUserBackend extends Repository implements UserBackendInterface if ($this->ds->getCapabilities()->hasAdOid()) { $stateConverter = 'user_account_control'; - $timeConverter = 'generalized_time'; } else { - $timeConverter = null; - $stateConverter = null; + $stateConverter = 'shadow_expire'; } return array( $this->userClass => array( 'is_active' => $stateConverter, - 'created_at' => $timeConverter, - 'last_modified' => $timeConverter + 'created_at' => 'generalized_time', + 'last_modified' => 'generalized_time' ) ); } @@ -342,6 +342,9 @@ class LdapUserBackend extends Repository implements UserBackendInterface ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false ) { return $dateTime->getTimeStamp(); } else { @@ -353,6 +356,25 @@ class LdapUserBackend extends Repository implements UserBackendInterface } } + /** + * Return whether the given shadowExpire value defines that a user is permitted to login + * + * @param string|null $value + * + * @return bool + */ + protected function retrieveShadowExpire($value) + { + if ($value === null) { + return $value; + } + + $now = new DateTime(); + $bigBang = clone $now; + $bigBang->setTimestamp(0); + return ((int) $value) >= $bigBang->diff($now)->days; + } + /** * Probe the backend to test if authentication is possible * From 89311f96df3cc11424a3827c23ea3cabb1e1d917 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 14:06:37 +0200 Subject: [PATCH 216/239] Show a minus for a user's state if the state cannot be determined refs #8826 --- application/views/scripts/user/show.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 96af9224c..aadc00614 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -28,7 +28,7 @@ if ($this->hasPermission('config/application/users/edit') && $backend instanceof

    escape($user->user_name); ?>

    -

    translate('State'); ?>: is_active ? $this->translate('Active') : $this->translate('Inactive'); ?>

    +

    translate('State'); ?>: is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?>

    translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($user->created_at); ?>

    translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?>

    From 4bd36bc5004ea5f53265ecfe979f56136b3967a3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 14:25:29 +0200 Subject: [PATCH 217/239] UserGroupForm: Notify the user that memberships will be cleared... ...when removing a group. refs #8826 --- application/forms/Config/UserGroup/UserGroupForm.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php index eb3c2846d..598029c1d 100644 --- a/application/forms/Config/UserGroup/UserGroupForm.php +++ b/application/forms/Config/UserGroup/UserGroupForm.php @@ -59,6 +59,10 @@ class UserGroupForm extends RepositoryForm protected function createDeleteElements(array $formData) { $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier())); + $this->addDescription($this->translate( + 'Note that all users that are currently a member of this group will' + . ' have their membership cleared automatically.' + )); $this->setSubmitLabel($this->translate('Yes')); } From beb5bd7370549c40590a27d5ace56f11943318c0 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 15:03:08 +0200 Subject: [PATCH 218/239] Repository: Clone a filter implicitly in self::requireFilter($clone = true) refs #8826 --- .../Icinga/Authentication/User/DbUserBackend.php | 2 +- library/Icinga/Repository/DbRepository.php | 13 +++++++++---- library/Icinga/Repository/IniRepository.php | 4 ++-- library/Icinga/Repository/Repository.php | 13 +++++++++++-- library/Icinga/Repository/RepositoryQuery.php | 8 ++------ 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php index 8a7991a0f..0e5dad5fa 100644 --- a/library/Icinga/Authentication/User/DbUserBackend.php +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -126,7 +126,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface { $bind['last_modified'] = date('Y-m-d H:i:s'); if ($filter) { - $this->requireFilter($table, $filter); + $filter = $this->requireFilter($table, $filter); } $this->ds->update( diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index e2ba7d994..93f9ceaeb 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -296,7 +296,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, public function update($table, array $bind, Filter $filter = null) { if ($filter) { - $this->requireFilter($table, $filter); + $filter = $this->requireFilter($table, $filter); } $this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter); @@ -311,7 +311,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, public function delete($table, Filter $filter = null) { if ($filter) { - $this->requireFilter($table, $filter); + $filter = $this->requireFilter($table, $filter); } $this->ds->delete($this->prependTablePrefix($table), $filter); @@ -478,10 +478,13 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, * @param Filter $filter The filter to recurse * @param RepositoryQuery $query An optional query to pass as context * (Directly passed through to $this->requireFilterColumn) + * @param bool $clone Whether to clone $filter first + * + * @return Filter The udpated filter */ - public function requireFilter($table, Filter $filter, RepositoryQuery $query = null) + public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true) { - parent::requireFilter($table, $filter, $query); + $filter = parent::requireFilter($table, $filter, $query, $clone); if ($filter->isExpression()) { $column = $filter->getColumn(); @@ -495,6 +498,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, } } } + + return $filter; } /** diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php index 43f42e749..3c73464e5 100644 --- a/library/Icinga/Repository/IniRepository.php +++ b/library/Icinga/Repository/IniRepository.php @@ -94,7 +94,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable } if ($filter !== null) { - $this->requireFilter($target, $filter); + $filter = $this->requireFilter($target, $filter); } $newSection = null; @@ -147,7 +147,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable public function delete($target, Filter $filter = null) { if ($filter !== null) { - $this->requireFilter($target, $filter); + $filter = $this->requireFilter($target, $filter); } foreach (iterator_to_array($this->ds) as $section => $config) { diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index ccb3da96a..803f4958f 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -627,18 +627,27 @@ abstract class Repository implements Selectable * @param Filter $filter The filter to recurse * @param RepositoryQuery $query An optional query to pass as context * (Directly passed through to $this->requireFilterColumn) + * @param bool $clone Whether to clone $filter first + * + * @return Filter The udpated filter */ - public function requireFilter($table, Filter $filter, RepositoryQuery $query = null) + public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true) { + if ($clone) { + $filter = clone $filter; + } + if ($filter->isExpression()) { $column = $filter->getColumn(); $filter->setColumn($this->requireFilterColumn($table, $column, $query)); $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression())); } elseif ($filter->isChain()) { foreach ($filter->filters() as $chainOrExpression) { - $this->requireFilter($table, $chainOrExpression, $query); + $this->requireFilter($table, $chainOrExpression, $query, false); } } + + return $filter; } /** diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 9a8eb7e83..c5e9ac9ed 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -183,9 +183,7 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function setFilter(Filter $filter) { - $filter = clone $filter; - $this->repository->requireFilter($this->target, $filter, $this); - $this->query->setFilter($filter); + $this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this)); return $this; } @@ -200,9 +198,7 @@ class RepositoryQuery implements QueryInterface, Iterator */ public function addFilter(Filter $filter) { - $filter = clone $filter; - $this->repository->requireFilter($this->target, $filter, $this); - $this->query->addFilter($filter); + $this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this)); return $this; } From 62fff9480826f8e5dc9d79a98c9f13e4bd780ea4 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 15:16:03 +0200 Subject: [PATCH 219/239] DbUserGroupBackend: Do not try to fetch a group id for null refs #8826 --- .../Icinga/Authentication/UserGroup/DbUserGroupBackend.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index e77d50f78..fd1228065 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -208,8 +208,8 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa */ protected function persistGroupId($groupName) { - if (is_int($groupName)) { - return $groupName; // It's obviously already an id + if (! $groupName || is_int($groupName)) { + return $groupName; // It's obviously already an id or NULL } $groupId = $this->ds From 1385295e4eeb75e07cf3772d82157a702857ed12 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 15:33:35 +0200 Subject: [PATCH 220/239] DbUserGroupBackend: Properly handle sequences of group names refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index fd1228065..fbc58bd5f 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -200,16 +200,33 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa /** * Fetch and return the corresponding id for the given group's name * - * @param string $groupName + * @param string|array $groupName * * @return int * - * @throws NotFoundError In case no group with the given name is found + * @throws NotFoundError */ protected function persistGroupId($groupName) { - if (! $groupName || is_int($groupName)) { - return $groupName; // It's obviously already an id or NULL + if (! $groupName || empty($groupName) || is_int($groupName)) { + return $groupName; + } + + if (is_array($groupName)) { + if (is_int($groupName[0])) { + return $groupName; // In case the array contains mixed types... + } + + $groupIds = $this->ds + ->select() + ->from($this->prependTablePrefix('group'), array('id')) + ->where('name', $groupName) + ->fetchColumn(); + if (empty($groupIds)) { + throw new NotFoundError('No groups found matching one of: %s', implode(', ', $groupName)); + } + + return $groupIds; } $groupId = $this->ds From e936c76ca937c9b0fe6029db83306568f3e05c69 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 15:34:38 +0200 Subject: [PATCH 221/239] DbUserGroupBackend: Really clear memberships and parent relations... ...when removing a group. refs #8826 --- .../UserGroup/DbUserGroupBackend.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php index fbc58bd5f..e59b03258 100644 --- a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php @@ -122,6 +122,27 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa parent::update($table, $bind, $filter); } + /** + * Delete table rows, optionally limited by using a filter + * + * @param string $table + * @param Filter $filter + */ + public function delete($table, Filter $filter = null) + { + if ($table === 'group') { + parent::delete('group_membership', $filter); + $idQuery = $this->select(array('group_id')); + if ($filter !== null) { + $idQuery->applyFilter($filter); + } + + $this->update('group', array('parent' => null), Filter::where('parent', $idQuery->fetchColumn())); + } + + parent::delete($table, $filter); + } + /** * Return the groups the given user is a member of * From 80e4e419e2ed1d816411403d0c3d1d3a3b952439 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 15:56:01 +0200 Subject: [PATCH 222/239] pgsql schema: Add function definition for unix_timestamp This does not seem to require any special privileges since we're using SQL as language and no unusual data types. If this proves false though, feel free to fix this. refs #8826 --- etc/schema/pgsql.schema.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/etc/schema/pgsql.schema.sql b/etc/schema/pgsql.schema.sql index 7f28c5d81..56117d4f8 100644 --- a/etc/schema/pgsql.schema.sql +++ b/etc/schema/pgsql.schema.sql @@ -1,5 +1,9 @@ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ +CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS ' + SELECT EXTRACT(EPOCH FROM $1)::bigint AS result +' LANGUAGE sql; + CREATE TABLE "icingaweb_group" ( "id" serial, "name" character varying(64) NOT NULL, From c7ce1498bf34efc435d2fa8d2252eb25c50c297d Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 16:10:23 +0200 Subject: [PATCH 223/239] Introduce menu entry "Configuration" --- library/Icinga/Web/Menu.php | 35 ++++++++++++++++------------ modules/doc/configuration.php | 4 ++-- modules/monitoring/configuration.php | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 014909ff0..7e092cc92 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -233,50 +233,55 @@ class Menu implements RecursiveIterator )); $section = $this->add(t('System'), array( - 'icon' => 'wrench', + 'icon' => 'services', 'priority' => 200, 'renderer' => 'ProblemMenuItemRenderer' )); + if (Logger::writesToFile()) { + $section->add(t('Application Log'), array( + 'url' => 'list/applicationlog', + 'priority' => 300 + )); + } + + $section = $this->add(t('Configuration'), array( + 'icon' => 'wrench', + 'permission' => 'config/*', + 'priority' => 400 + )); $section->add(t('User-Management'), array( 'url' => 'user/list', 'permission' => 'config/application/*', - 'priority' => 300 + 'priority' => 500 )); $section->add(t('UserGroupBackends'), array( 'url' => 'usergroupbackend/list', 'permission' => 'config/application/usergroupbackend/*', - 'priority' => 301 + 'priority' => 510 )); $section->add(t('Configuration'), array( 'url' => 'config', 'permission' => 'config/application/*', - 'priority' => 400 + 'priority' => 600 )); $section->add(t('Modules'), array( 'url' => 'config/modules', 'permission' => 'config/modules', - 'priority' => 500 + 'priority' => 700 )); - if (Logger::writesToFile()) { - $section->add(t('Application Log'), array( - 'url' => 'list/applicationlog', - 'priority' => 600 - )); - } - $section = $this->add($auth->getUser()->getUsername(), array( 'icon' => 'user', - 'priority' => 700 + 'priority' => 800 )); $section->add(t('Preferences'), array( 'url' => 'preference', - 'priority' => 701 + 'priority' => 810 )); $section->add(t('Logout'), array( 'url' => 'authentication/logout', - 'priority' => 800, + 'priority' => 890, 'renderer' => 'ForeignMenuItemRenderer' )); } diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php index 031a62e64..ee3d9ebdd 100644 --- a/modules/doc/configuration.php +++ b/modules/doc/configuration.php @@ -7,7 +7,7 @@ $section = $this->menuSection($this->translate('Documentation'), array( 'title' => 'Documentation', 'icon' => 'book', 'url' => 'doc', - 'priority' => 190 + 'priority' => 390 )); $section->add('Icinga Web 2', array( @@ -18,7 +18,7 @@ $section->add('Module documentations', array( )); $section->add($this->translate('Developer - Style'), array( 'url' => 'doc/style/guide', - 'priority' => 200, + 'priority' => 399 )); $this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10); diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php index fd3ee334d..aa3b4a2fd 100644 --- a/modules/monitoring/configuration.php +++ b/modules/monitoring/configuration.php @@ -208,7 +208,7 @@ $section->add($this->translate('Alert Summary'), array( $section = $this->menuSection($this->translate('System')); $section->add($this->translate('Monitoring Health'), array( 'url' => 'monitoring/process/info', - 'priority' => 120, + 'priority' => 220, 'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer' )); From 49bb09d9d363f5863996c228c19a56037a9283fe Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 16:31:48 +0200 Subject: [PATCH 224/239] Add dedicated menu entries to manage users, groups and roles refs #8826 --- library/Icinga/Web/Menu.php | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 7e092cc92..1757f0e88 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -249,20 +249,30 @@ class Menu implements RecursiveIterator 'permission' => 'config/*', 'priority' => 400 )); - $section->add(t('User-Management'), array( - 'url' => 'user/list', + $section->add(t('Application'), array( + 'url' => 'config/application', 'permission' => 'config/application/*', + 'priority' => 450 + )); + $section->add(t('Users'), array( + 'url' => 'user/list', + 'permission' => 'config/application/user/show', 'priority' => 500 )); + $section->add(t('Groups'), array( + 'url' => 'group/list', + 'permission' => 'config/application/group/show', + 'priority' => 550 + )); + $section->add(t('Roles'), array( + 'url' => 'roles', + 'permission' => 'config/application/roles', + 'priority' => 600 + )); $section->add(t('UserGroupBackends'), array( 'url' => 'usergroupbackend/list', 'permission' => 'config/application/usergroupbackend/*', - 'priority' => 510 - )); - $section->add(t('Configuration'), array( - 'url' => 'config', - 'permission' => 'config/application/*', - 'priority' => 600 + 'priority' => 650 )); $section->add(t('Modules'), array( 'url' => 'config/modules', From 66fd7dfd9313f76911ffefdf568a674809665e67 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 16:35:27 +0200 Subject: [PATCH 225/239] ConfigController: Rename applicationAction to generalAction --- application/controllers/ConfigController.php | 16 ++++++++-------- .../config/{application.phtml => general.phtml} | 0 library/Icinga/Web/Menu.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename application/views/scripts/config/{application.phtml => general.phtml} (100%) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 77b5b66d9..321fcf882 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -38,12 +38,12 @@ class ConfigController extends Controller $auth = $this->Auth(); $allowedActions = array(); if ($auth->hasPermission('config/application/general')) { - $tabs->add('application', array( + $tabs->add('general', array( 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'), - 'label' => $this->translate('Application'), - 'url' => 'config/application' + 'label' => $this->translate('General'), + 'url' => 'config/general' )); - $allowedActions[] = 'application'; + $allowedActions[] = 'general'; } if ($auth->hasPermission('config/application/authentication')) { $tabs->add('authentication', array( @@ -96,11 +96,11 @@ class ConfigController extends Controller } /** - * Application configuration + * General configuration * - * @throws SecurityException If the user lacks the permission for configuring the application + * @throws SecurityException If the user lacks the permission for configuring the general configuration */ - public function applicationAction() + public function generalAction() { $this->assertPermission('config/application/general'); $form = new GeneralConfigForm(); @@ -108,7 +108,7 @@ class ConfigController extends Controller $form->handleRequest(); $this->view->form = $form; - $this->view->tabs->activate('application'); + $this->view->tabs->activate('general'); } /** diff --git a/application/views/scripts/config/application.phtml b/application/views/scripts/config/general.phtml similarity index 100% rename from application/views/scripts/config/application.phtml rename to application/views/scripts/config/general.phtml diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 1757f0e88..e5fd610ad 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -250,7 +250,7 @@ class Menu implements RecursiveIterator 'priority' => 400 )); $section->add(t('Application'), array( - 'url' => 'config/application', + 'url' => 'config', 'permission' => 'config/application/*', 'priority' => 450 )); From 17e7f1e7543ced6889192becddf0cb1690691b96 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 16:43:11 +0200 Subject: [PATCH 226/239] Link the roles configuration with the user and group management refs #8826 --- application/controllers/ConfigController.php | 10 ---- application/controllers/RolesController.php | 51 +++---------------- .../Web/Controller/AuthBackendController.php | 13 +++++ 3 files changed, 20 insertions(+), 54 deletions(-) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 321fcf882..ab4b62049 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -61,16 +61,6 @@ class ConfigController extends Controller )); $allowedActions[] = 'resource'; } - if ($auth->hasPermission('config/application/roles')) { - $tabs->add('roles', array( - 'title' => $this->translate( - 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' - ), - 'label' => $this->translate('Roles'), - 'url' => 'roles' - )); - $allowedActions[] = 'roles'; - } $this->firstAllowedAction = array_shift($allowedActions); } diff --git a/application/controllers/RolesController.php b/application/controllers/RolesController.php index d4b7b7c63..b889083c6 100644 --- a/application/controllers/RolesController.php +++ b/application/controllers/RolesController.php @@ -4,61 +4,21 @@ use Icinga\Application\Config; use Icinga\Forms\ConfirmRemovalForm; use Icinga\Forms\Security\RoleForm; -use Icinga\Web\Controller\ActionController; +use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Notification; -use Icinga\Web\Widget; /** * Roles configuration */ -class RolesController extends ActionController +class RolesController extends AuthBackendController { - /** - * Initialize tabs and validate the user's permissions - * - * @throws \Icinga\Security\SecurityException If the user lacks permissions for configuring roles - */ - public function init() - { - $this->assertPermission('config/application/roles'); - $tabs = $this->getTabs(); - $auth = $this->Auth(); - if ($auth->hasPermission('config/application/general')) { - $tabs->add('application', array( - 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'), - 'label' => $this->translate('Application'), - 'url' => 'config' - )); - } - if ($auth->hasPermission('config/application/authentication')) { - $tabs->add('authentication', array( - 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'), - 'label' => $this->translate('Authentication'), - 'url' => 'config/authentication' - )); - } - if ($auth->hasPermission('config/application/resources')) { - $tabs->add('resource', array( - 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'), - 'label' => $this->translate('Resources'), - 'url' => 'config/resource' - )); - } - $tabs->add('roles', array( - 'title' => $this->translate( - 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' - ), - 'label' => $this->translate('Roles'), - 'url' => 'roles' - )); - } - /** * List roles */ public function indexAction() { - $this->view->tabs->activate('roles'); + $this->assertPermission('config/application/roles'); + $this->createListTabs()->activate('roles'); $this->view->roles = Config::app('roles', true); } @@ -67,6 +27,7 @@ class RolesController extends ActionController */ public function newAction() { + $this->assertPermission('config/application/roles'); $role = new RoleForm(array( 'onSuccess' => function (RoleForm $role) { $name = $role->getElement('name')->getValue(); @@ -100,6 +61,7 @@ class RolesController extends ActionController */ public function updateAction() { + $this->assertPermission('config/application/roles'); $name = $this->_request->getParam('role'); if (empty($name)) { throw new Zend_Controller_Action_Exception( @@ -149,6 +111,7 @@ class RolesController extends ActionController */ public function removeAction() { + $this->assertPermission('config/application/roles'); $name = $this->_request->getParam('role'); if (empty($name)) { throw new Zend_Controller_Action_Exception( diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index 5b2a4f18e..a3d93e0f8 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -163,6 +163,19 @@ class AuthBackendController extends Controller ); } + if ($this->hasPermission('config/application/roles')) { + $tabs->add( + 'roles', + array( + 'title' => $this->translate( + 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' + ), + 'label' => $this->translate('Roles'), + 'url' => 'roles' + ) + ); + } + return $tabs; } } From 7b9983de38969c142cf6c913a7c0675c6d747f48 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 1 Jun 2015 17:16:24 +0200 Subject: [PATCH 227/239] Merge the menu entries for users, groups and roles into "Authentication" --- library/Icinga/Web/Menu.php | 36 ++++++++++------------------ modules/doc/configuration.php | 4 ++-- modules/monitoring/configuration.php | 2 +- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index e5fd610ad..4dff8e735 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -234,64 +234,54 @@ class Menu implements RecursiveIterator $section = $this->add(t('System'), array( 'icon' => 'services', - 'priority' => 200, + 'priority' => 700, 'renderer' => 'ProblemMenuItemRenderer' )); if (Logger::writesToFile()) { $section->add(t('Application Log'), array( 'url' => 'list/applicationlog', - 'priority' => 300 + 'priority' => 710 )); } $section = $this->add(t('Configuration'), array( 'icon' => 'wrench', 'permission' => 'config/*', - 'priority' => 400 + 'priority' => 800 )); $section->add(t('Application'), array( 'url' => 'config', 'permission' => 'config/application/*', - 'priority' => 450 + 'priority' => 810 )); - $section->add(t('Users'), array( - 'url' => 'user/list', - 'permission' => 'config/application/user/show', - 'priority' => 500 - )); - $section->add(t('Groups'), array( - 'url' => 'group/list', - 'permission' => 'config/application/group/show', - 'priority' => 550 - )); - $section->add(t('Roles'), array( - 'url' => 'roles', - 'permission' => 'config/application/roles', - 'priority' => 600 + $section->add(t('Authentication'), array( + 'url' => 'user', + 'permission' => 'config/authentication/*', + 'priority' => 820 )); $section->add(t('UserGroupBackends'), array( 'url' => 'usergroupbackend/list', 'permission' => 'config/application/usergroupbackend/*', - 'priority' => 650 + 'priority' => 830 )); $section->add(t('Modules'), array( 'url' => 'config/modules', 'permission' => 'config/modules', - 'priority' => 700 + 'priority' => 890 )); $section = $this->add($auth->getUser()->getUsername(), array( 'icon' => 'user', - 'priority' => 800 + 'priority' => 900 )); $section->add(t('Preferences'), array( 'url' => 'preference', - 'priority' => 810 + 'priority' => 910 )); $section->add(t('Logout'), array( 'url' => 'authentication/logout', - 'priority' => 890, + 'priority' => 990, 'renderer' => 'ForeignMenuItemRenderer' )); } diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php index ee3d9ebdd..392009798 100644 --- a/modules/doc/configuration.php +++ b/modules/doc/configuration.php @@ -7,7 +7,7 @@ $section = $this->menuSection($this->translate('Documentation'), array( 'title' => 'Documentation', 'icon' => 'book', 'url' => 'doc', - 'priority' => 390 + 'priority' => 700 )); $section->add('Icinga Web 2', array( @@ -18,7 +18,7 @@ $section->add('Module documentations', array( )); $section->add($this->translate('Developer - Style'), array( 'url' => 'doc/style/guide', - 'priority' => 399 + 'priority' => 790 )); $this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10); diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php index aa3b4a2fd..66b42aa20 100644 --- a/modules/monitoring/configuration.php +++ b/modules/monitoring/configuration.php @@ -208,7 +208,7 @@ $section->add($this->translate('Alert Summary'), array( $section = $this->menuSection($this->translate('System')); $section->add($this->translate('Monitoring Health'), array( 'url' => 'monitoring/process/info', - 'priority' => 220, + 'priority' => 720, 'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer' )); From 46e2393074671e2ad2f8f7ec211b3893fbed35d3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 08:58:21 +0200 Subject: [PATCH 228/239] UsergroupbackendController: Do only assert that the user has one permission The configuration of a backend itself should not be that granular. refs #8826 --- .../controllers/UsergroupbackendController.php | 13 ++++++++----- application/forms/Security/RoleForm.php | 5 +---- .../views/scripts/usergroupbackend/list.phtml | 17 ----------------- library/Icinga/Web/Menu.php | 2 +- 4 files changed, 10 insertions(+), 27 deletions(-) diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php index 91560bf55..e03880637 100644 --- a/application/controllers/UsergroupbackendController.php +++ b/application/controllers/UsergroupbackendController.php @@ -15,6 +15,14 @@ use Icinga\Web\Url; */ class UsergroupbackendController extends Controller { + /** + * Initialize this controller + */ + public function init() + { + $this->assertPermission('config/application/usergroupbackend'); + } + /** * Redirect to this controller's list action */ @@ -28,7 +36,6 @@ class UsergroupbackendController extends Controller */ public function listAction() { - $this->assertPermission('config/application/usergroupbackend/*'); $this->view->backendNames = Config::app('groups')->keys(); $this->getTabs()->add( 'usergroupbackend/list', @@ -45,8 +52,6 @@ class UsergroupbackendController extends Controller */ public function createAction() { - $this->assertPermission('config/application/usergroupbackend/create'); - $form = new UserGroupBackendForm(); $form->setRedirectUrl('usergroupbackend/list'); $form->setTitle($this->translate('Create New User Group Backend')); @@ -78,7 +83,6 @@ class UsergroupbackendController extends Controller */ public function editAction() { - $this->assertPermission('config/application/usergroupbackend/edit'); $backendName = $this->params->getRequired('backend'); $form = new UserGroupBackendForm(); @@ -118,7 +122,6 @@ class UsergroupbackendController extends Controller */ public function removeAction() { - $this->assertPermission('config/application/usergroupbackend/remove'); $backendName = $this->params->getRequired('backend'); $backendForm = new UserGroupBackendForm(); diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 43ae6c1a0..93f5db014 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -27,6 +27,7 @@ class RoleForm extends ConfigForm 'config/application/general' => 'config/application/general', 'config/application/authentication' => 'config/application/authentication', 'config/application/resources' => 'config/application/resources', + 'config/application/usergroupbackend' => 'config/application/usergroupbackend', 'config/application/roles' => 'config/application/roles', 'config/application/users/*' => 'config/application/users/*', 'config/application/users/show' => 'config/application/users/show', @@ -40,10 +41,6 @@ class RoleForm extends ConfigForm 'config/application/groups/remove' => 'config/application/groups/remove', 'config/application/groups/member/add' => 'config/application/groups/member/add', 'config/application/groups/member/remove' => 'config/application/groups/member/remove', - 'config/application/usergroupbackend/*' => 'config/application/usergroupbackend/*', - 'config/application/usergroupbackend/create' => 'config/application/usergroupbackend/create', - 'config/application/usergroupbackend/edit' => 'config/application/usergroupbackend/edit', - 'config/application/usergroupbackend/remove' => 'config/application/usergroupbackend/remove', 'config/modules' => 'config/modules' ); diff --git a/application/views/scripts/usergroupbackend/list.phtml b/application/views/scripts/usergroupbackend/list.phtml index d98d207f1..58aa2deba 100644 --- a/application/views/scripts/usergroupbackend/list.phtml +++ b/application/views/scripts/usergroupbackend/list.phtml @@ -1,15 +1,7 @@ -hasPermission('config/application/usergroupbackend/create'); -$editPermitted = $this->hasPermission('config/application/usergroupbackend/edit'); -$removePermitted = $this->hasPermission('config/application/usergroupbackend/remove'); - -?>
    - qlink( $this->translate('Create A New User Group Backend'), 'usergroupbackend/create', @@ -18,33 +10,25 @@ $removePermitted = $this->hasPermission('config/application/usergroupbackend/rem 'icon' => 'plus' ) ); ?> - 0): ?> - - - - diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 4dff8e735..ef27204c8 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -261,7 +261,7 @@ class Menu implements RecursiveIterator )); $section->add(t('UserGroupBackends'), array( 'url' => 'usergroupbackend/list', - 'permission' => 'config/application/usergroupbackend/*', + 'permission' => 'config/application/usergroupbackend', 'priority' => 830 )); $section->add(t('Modules'), array( From a558f2873ab1304d593d38a73927fd0ab7c9ea6f Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 09:02:57 +0200 Subject: [PATCH 229/239] Rename permission config/application/users* to config/authentication/... refs #8826 --- application/controllers/UserController.php | 10 +++++----- application/forms/Security/RoleForm.php | 10 +++++----- application/views/scripts/user/list.phtml | 4 ++-- application/views/scripts/user/show.phtml | 2 +- .../Icinga/Web/Controller/AuthBackendController.php | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 05adf03d7..a8db8d541 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -30,7 +30,7 @@ class UserController extends AuthBackendController */ public function listAction() { - $this->assertPermission('config/application/users/show'); + $this->assertPermission('config/authentication/users/show'); $backendNames = array_map( function ($b) { return $b->getName(); }, $this->loadUserBackends('Icinga\Data\Selectable') @@ -88,7 +88,7 @@ class UserController extends AuthBackendController */ public function showAction() { - $this->assertPermission('config/application/users/show'); + $this->assertPermission('config/authentication/users/show'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend')); @@ -166,7 +166,7 @@ class UserController extends AuthBackendController */ public function addAction() { - $this->assertPermission('config/application/users/add'); + $this->assertPermission('config/authentication/users/add'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); $form = new UserForm(); $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); @@ -182,7 +182,7 @@ class UserController extends AuthBackendController */ public function editAction() { - $this->assertPermission('config/application/users/edit'); + $this->assertPermission('config/authentication/users/edit'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); @@ -205,7 +205,7 @@ class UserController extends AuthBackendController */ public function removeAction() { - $this->assertPermission('config/application/users/remove'); + $this->assertPermission('config/authentication/users/remove'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 93f5db014..d321ac8fd 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -29,11 +29,11 @@ class RoleForm extends ConfigForm 'config/application/resources' => 'config/application/resources', 'config/application/usergroupbackend' => 'config/application/usergroupbackend', 'config/application/roles' => 'config/application/roles', - 'config/application/users/*' => 'config/application/users/*', - 'config/application/users/show' => 'config/application/users/show', - 'config/application/users/add' => 'config/application/users/add', - 'config/application/users/edit' => 'config/application/users/edit', - 'config/application/users/remove' => 'config/application/users/remove', + 'config/authentication/users/*' => 'config/authentication/users/*', + 'config/authentication/users/show' => 'config/authentication/users/show', + 'config/authentication/users/add' => 'config/authentication/users/add', + 'config/authentication/users/edit' => 'config/authentication/users/edit', + 'config/authentication/users/remove' => 'config/authentication/users/remove', 'config/application/groups/*' => 'config/application/groups/*', 'config/application/groups/show' => 'config/application/groups/show', 'config/application/groups/add' => 'config/application/groups/add', diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 51c0eb7b7..76a6f2b8b 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -22,8 +22,8 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list users') . ''; return; } else { - $extensible = $this->hasPermission('config/application/users/add') && $backend instanceof Extensible; - $reducible = $this->hasPermission('config/application/users/remove') && $backend instanceof Reducible; + $extensible = $this->hasPermission('config/authentication/users/add') && $backend instanceof Extensible; + $reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible; } if (count($users) > 0): ?> diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index aadc00614..bccfaa21e 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -5,7 +5,7 @@ use Icinga\Data\Reducible; use Icinga\Data\Selectable; $editLink = null; -if ($this->hasPermission('config/application/users/edit') && $backend instanceof Updatable) { +if ($this->hasPermission('config/authentication/users/edit') && $backend instanceof Updatable) { $editLink = $this->qlink( null, 'user/edit', diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index a3d93e0f8..a423b8023 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -139,7 +139,7 @@ class AuthBackendController extends Controller { $tabs = $this->getTabs(); - if ($this->hasPermission('config/application/users/show')) { + if ($this->hasPermission('config/authentication/users/show')) { $tabs->add( 'user/list', array( From 3fffd90135b382f69f312eab8c2d176a75670cfc Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 09:04:34 +0200 Subject: [PATCH 230/239] Rename permission config/application/groups* to config/authentication/... refs #8826 --- application/controllers/GroupController.php | 10 +++++----- application/forms/Security/RoleForm.php | 10 +++++----- application/views/scripts/group/list.phtml | 4 ++-- application/views/scripts/group/show.phtml | 4 ++-- application/views/scripts/user/show.phtml | 2 +- .../Icinga/Web/Controller/AuthBackendController.php | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 0c2a4a45d..ef95dc3fc 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -30,7 +30,7 @@ class GroupController extends AuthBackendController */ public function listAction() { - $this->assertPermission('config/application/groups/show'); + $this->assertPermission('config/authentication/groups/show'); $backendNames = array_map( function ($b) { return $b->getName(); }, $this->loadUserGroupBackends('Icinga\Data\Selectable') @@ -87,7 +87,7 @@ class GroupController extends AuthBackendController */ public function showAction() { - $this->assertPermission('config/application/groups/show'); + $this->assertPermission('config/authentication/groups/show'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend')); @@ -164,7 +164,7 @@ class GroupController extends AuthBackendController */ public function addAction() { - $this->assertPermission('config/application/groups/add'); + $this->assertPermission('config/authentication/groups/add'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); $form = new UserGroupForm(); $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); @@ -180,7 +180,7 @@ class GroupController extends AuthBackendController */ public function editAction() { - $this->assertPermission('config/application/groups/edit'); + $this->assertPermission('config/authentication/groups/edit'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); @@ -205,7 +205,7 @@ class GroupController extends AuthBackendController */ public function removeAction() { - $this->assertPermission('config/application/groups/remove'); + $this->assertPermission('config/authentication/groups/remove'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index d321ac8fd..b3b7aa9cd 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -34,11 +34,11 @@ class RoleForm extends ConfigForm 'config/authentication/users/add' => 'config/authentication/users/add', 'config/authentication/users/edit' => 'config/authentication/users/edit', 'config/authentication/users/remove' => 'config/authentication/users/remove', - 'config/application/groups/*' => 'config/application/groups/*', - 'config/application/groups/show' => 'config/application/groups/show', - 'config/application/groups/add' => 'config/application/groups/add', - 'config/application/groups/edit' => 'config/application/groups/edit', - 'config/application/groups/remove' => 'config/application/groups/remove', + 'config/authentication/groups/*' => 'config/authentication/groups/*', + 'config/authentication/groups/show' => 'config/authentication/groups/show', + 'config/authentication/groups/add' => 'config/authentication/groups/add', + 'config/authentication/groups/edit' => 'config/authentication/groups/edit', + 'config/authentication/groups/remove' => 'config/authentication/groups/remove', 'config/application/groups/member/add' => 'config/application/groups/member/add', 'config/application/groups/member/remove' => 'config/application/groups/member/remove', 'config/modules' => 'config/modules' diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index 49d8dc4c9..bcd9dca93 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -22,8 +22,8 @@ if ($backend === null) { echo $this->translate('No backend found which is able to list groups') . ''; return; } else { - $extensible = $this->hasPermission('config/application/groups/add') && $backend instanceof Extensible; - $reducible = $this->hasPermission('config/application/groups/remove') && $backend instanceof Reducible; + $extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible; + $reducible = $this->hasPermission('config/authentication/groups/remove') && $backend instanceof Reducible; } if (count($groups) > 0): ?> diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 1fd3e48c6..636a449d4 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -3,10 +3,10 @@ use Icinga\Data\Extensible; use Icinga\Data\Updatable; -$extensible = $this->hasPermission('config/application/groups/add') && $backend instanceof Extensible; +$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible; $editLink = null; -if ($this->hasPermission('config/application/groups/edit') && $backend instanceof Updatable) { +if ($this->hasPermission('config/authentication/groups/edit') && $backend instanceof Updatable) { $editLink = $this->qlink( null, 'group/edit', diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index bccfaa21e..82f4c53f9 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -54,7 +54,7 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc @@ -40,7 +40,7 @@ ); ?>" title="translate( 'Move up in authentication order' ); ?>" aria-label="translate('Move authentication backend %s upwards'), + $this->translate('Move user backend %s upwards'), $backendNames[$i] ); ?>"> icon('up-big'); ?> @@ -54,7 +54,7 @@ ); ?>" title="translate( 'Move down in authentication order' ); ?>" aria-label="translate('Move authentication backend %s downwards'), + $this->translate('Move user backend %s downwards'), $backendNames[$i] ); ?>"> icon('down-big'); ?> diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php index f3bce41fa..bb68792a6 100644 --- a/modules/setup/application/forms/AuthBackendPage.php +++ b/modules/setup/application/forms/AuthBackendPage.php @@ -4,9 +4,9 @@ namespace Icinga\Module\Setup\Forms; use Icinga\Web\Form; -use Icinga\Forms\Config\Authentication\DbBackendForm; -use Icinga\Forms\Config\Authentication\LdapBackendForm; -use Icinga\Forms\Config\Authentication\ExternalBackendForm; +use Icinga\Forms\Config\UserBackend\DbBackendForm; +use Icinga\Forms\Config\UserBackend\LdapBackendForm; +use Icinga\Forms\Config\UserBackend\ExternalBackendForm; use Icinga\Data\ConfigObject; /** @@ -105,7 +105,7 @@ class AuthBackendPage extends Form } if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { - if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidAuthenticationBackend($this)) { + if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidUserBackend($this)) { $this->addSkipValidationCheckbox(); return false; } diff --git a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php similarity index 79% rename from test/php/application/forms/Config/Authentication/DbBackendFormTest.php rename to test/php/application/forms/Config/UserBackend/DbBackendFormTest.php index 695ef4885..d58ff8a33 100644 --- a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php +++ b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php @@ -1,7 +1,7 @@ andReturn(2); // Passing array(null) is required to make Mockery call the constructor... - $form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null)); + $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) ->andReturnUsing(function ($s) { return $s; }); @@ -41,8 +41,8 @@ class DbBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_db_backend')); $this->assertTrue( - DbBackendForm::isValidAuthenticationBackend($form), - 'DbBackendForm claims that a valid authentication backend with users is not valid' + DbBackendForm::isValidUserBackend($form), + 'DbBackendForm claims that a valid user backend with users is not valid' ); } @@ -58,7 +58,7 @@ class DbBackendFormTest extends BaseTestCase ->andReturn(0); // Passing array(null) is required to make Mockery call the constructor... - $form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null)); + $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) ->andReturnUsing(function ($s) { return $s; }); @@ -67,8 +67,8 @@ class DbBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_db_backend')); $this->assertFalse( - DbBackendForm::isValidAuthenticationBackend($form), - 'DbBackendForm claims that an invalid authentication backend without users is valid' + DbBackendForm::isValidUserBackend($form), + 'DbBackendForm claims that an invalid user backend without users is valid' ); } diff --git a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php similarity index 79% rename from test/php/application/forms/Config/Authentication/LdapBackendFormTest.php rename to test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php index 442770e83..f7373a7ae 100644 --- a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php +++ b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php @@ -1,7 +1,7 @@ shouldReceive('setConfig')->andReturnNull(); // Passing array(null) is required to make Mockery call the constructor... - $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null)); + $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) ->andReturnUsing(function ($s) { return $s; }); @@ -42,8 +42,8 @@ class LdapBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_ldap_backend')); $this->assertTrue( - LdapBackendForm::isValidAuthenticationBackend($form), - 'LdapBackendForm claims that a valid authentication backend with users is not valid' + LdapBackendForm::isValidUserBackend($form), + 'LdapBackendForm claims that a valid user backend with users is not valid' ); } @@ -58,7 +58,7 @@ class LdapBackendFormTest extends BaseTestCase ->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException); // Passing array(null) is required to make Mockery call the constructor... - $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null)); + $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null)); $form->shouldReceive('getView->escape') ->with(Mockery::type('string')) ->andReturnUsing(function ($s) { return $s; }); @@ -67,8 +67,8 @@ class LdapBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_ldap_backend')); $this->assertFalse( - LdapBackendForm::isValidAuthenticationBackend($form), - 'LdapBackendForm claims that an invalid authentication backend without users is valid' + LdapBackendForm::isValidUserBackend($form), + 'LdapBackendForm claims that an invalid user backend without users is valid' ); } diff --git a/test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php b/test/php/application/forms/Config/UserBackendReorderFormTest.php similarity index 64% rename from test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php rename to test/php/application/forms/Config/UserBackendReorderFormTest.php index 23563f31b..240d578be 100644 --- a/test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php +++ b/test/php/application/forms/Config/UserBackendReorderFormTest.php @@ -5,10 +5,10 @@ namespace Tests\Icinga\Forms\Config; use Icinga\Test\BaseTestCase; use Icinga\Application\Config; -use Icinga\Forms\Config\AuthenticationBackendConfigForm; -use Icinga\Forms\Config\AuthenticationBackendReorderForm; +use Icinga\Forms\Config\UserBackendConfigForm; +use Icinga\Forms\Config\UserBackendReorderForm; -class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendConfigForm +class UserBackendConfigFormWithoutSave extends UserBackendConfigForm { public static $newConfig; @@ -19,11 +19,11 @@ class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendCo } } -class AuthenticationBackendReorderFormProvidingConfigFormWithoutSave extends AuthenticationBackendReorderForm +class UserBackendReorderFormProvidingConfigFormWithoutSave extends UserBackendReorderForm { public function getConfigForm() { - $form = new AuthenticationBackendConfigFormWithoutSave(); + $form = new UserBackendConfigFormWithoutSave(); $form->setIniConfig($this->config); return $form; } @@ -45,7 +45,7 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase ->shouldReceive('isPost')->andReturn(true) ->shouldReceive('getPost')->andReturn(array('backend_newpos' => 'test3|1')); - $form = new AuthenticationBackendReorderFormProvidingConfigFormWithoutSave(); + $form = new UserBackendReorderFormProvidingConfigFormWithoutSave(); $form->setIniConfig($config); $form->setTokenDisabled(); $form->setUidDisabled(); @@ -53,8 +53,8 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase $this->assertEquals( array('test1', 'test3', 'test2'), - AuthenticationBackendConfigFormWithoutSave::$newConfig->keys(), - 'Moving elements with AuthenticationBackendReorderForm does not seem to properly work' + UserBackendConfigFormWithoutSave::$newConfig->keys(), + 'Moving elements with UserBackendReorderForm does not seem to properly work' ); } } From ae30a62055efffa007160d2845dc5cb3644cc322 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 10:23:40 +0200 Subject: [PATCH 235/239] ConfigController: Add tab for the user group backend configuration refs #8826 --- application/controllers/ConfigController.php | 20 +++++--- .../UsergroupbackendController.php | 47 +++++++++++++++---- library/Icinga/Web/Menu.php | 5 -- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 6b7dbc705..ce020733b 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -46,6 +46,14 @@ class ConfigController extends Controller )); $allowedActions[] = 'general'; } + if ($auth->hasPermission('config/application/resources')) { + $tabs->add('resource', array( + 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'), + 'label' => $this->translate('Resources'), + 'url' => 'config/resource' + )); + $allowedActions[] = 'resource'; + } if ($auth->hasPermission('config/application/userbackend')) { $tabs->add('userbackend', array( 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'), @@ -54,13 +62,13 @@ class ConfigController extends Controller )); $allowedActions[] = 'userbackend'; } - if ($auth->hasPermission('config/application/resources')) { - $tabs->add('resource', array( - 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'), - 'label' => $this->translate('Resources'), - 'url' => 'config/resource' + if ($auth->hasPermission('config/application/usergroupbackend')) { + $tabs->add('usergroupbackend', array( + 'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'), + 'label' => $this->translate('User Groups'), + 'url' => 'usergroupbackend/list' )); - $allowedActions[] = 'resource'; + $allowedActions[] = 'usergroupbackend'; } $this->firstAllowedAction = array_shift($allowedActions); } diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php index e03880637..cdb6826be 100644 --- a/application/controllers/UsergroupbackendController.php +++ b/application/controllers/UsergroupbackendController.php @@ -37,14 +37,7 @@ class UsergroupbackendController extends Controller public function listAction() { $this->view->backendNames = Config::app('groups')->keys(); - $this->getTabs()->add( - 'usergroupbackend/list', - array( - 'title' => $this->translate('List all user group backends'), - 'label' => $this->translate('User group backends'), - 'url' => 'usergroupbackend/list' - ) - )->activate('usergroupbackend/list'); + $this->createListTabs()->activate('usergroupbackend'); } /** @@ -149,4 +142,42 @@ class UsergroupbackendController extends Controller $this->view->form = $form; $this->render('form'); } + + /** + * Create the tabs for the application configuration + */ + protected function createListTabs() + { + $tabs = $this->getTabs(); + if ($this->hasPermission('config/application/general')) { + $tabs->add('general', array( + 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'), + 'label' => $this->translate('General'), + 'url' => 'config/general' + )); + } + if ($this->hasPermission('config/application/resources')) { + $tabs->add('resource', array( + 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'), + 'label' => $this->translate('Resources'), + 'url' => 'config/resource' + )); + } + if ($this->hasPermission('config/application/userbackend')) { + $tabs->add('userbackend', array( + 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'), + 'label' => $this->translate('Authentication'), + 'url' => 'config/userbackend' + )); + } + if ($this->hasPermission('config/application/usergroupbackend')) { + $tabs->add('usergroupbackend', array( + 'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'), + 'label' => $this->translate('User Groups'), + 'url' => 'usergroupbackend/list' + )); + } + + return $tabs; + } } diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index ef27204c8..63d92fc9f 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -259,11 +259,6 @@ class Menu implements RecursiveIterator 'permission' => 'config/authentication/*', 'priority' => 820 )); - $section->add(t('UserGroupBackends'), array( - 'url' => 'usergroupbackend/list', - 'permission' => 'config/application/usergroupbackend', - 'priority' => 830 - )); $section->add(t('Modules'), array( 'url' => 'config/modules', 'permission' => 'config/modules', From 5d50eabb4410ceea0ba9170bbfe3e5fb5004ffe5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 10:39:49 +0200 Subject: [PATCH 236/239] FileReader: Mimic cursor capability --- library/Icinga/Protocol/File/FileReader.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/Icinga/Protocol/File/FileReader.php b/library/Icinga/Protocol/File/FileReader.php index 2c43d390a..d9c984ca8 100644 --- a/library/Icinga/Protocol/File/FileReader.php +++ b/library/Icinga/Protocol/File/FileReader.php @@ -4,6 +4,7 @@ namespace Icinga\Protocol\File; use Countable; +use ArrayIterator; use Icinga\Data\Selectable; use Icinga\Data\ConfigObject; @@ -71,6 +72,18 @@ class FileReader implements Selectable, Countable return new FileQuery($this); } + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param FileQuery $query + * + * @return ArrayIterator + */ + public function query(FileQuery $query) + { + return new ArrayIterator($this->fetchAll($query)); + } + /** * Return the number of available valid lines. * From 00c31ffd28845babbad674e1cf8c5c2bc5f59920 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 11:57:13 +0200 Subject: [PATCH 237/239] RolesController: Rename to RoleController --- ...RolesController.php => RoleController.php} | 22 +++++++++---------- .../{roles/update.phtml => role/form.phtml} | 4 ++-- .../{roles/index.phtml => role/list.phtml} | 6 ++--- application/views/scripts/roles/new.phtml | 6 ----- application/views/scripts/roles/remove.phtml | 6 ----- .../Web/Controller/AuthBackendController.php | 4 ++-- 6 files changed, 18 insertions(+), 30 deletions(-) rename application/controllers/{RolesController.php => RoleController.php} (91%) rename application/views/scripts/{roles/update.phtml => role/form.phtml} (50%) rename application/views/scripts/{roles/index.phtml => role/list.phtml} (95%) delete mode 100644 application/views/scripts/roles/new.phtml delete mode 100644 application/views/scripts/roles/remove.phtml diff --git a/application/controllers/RolesController.php b/application/controllers/RoleController.php similarity index 91% rename from application/controllers/RolesController.php rename to application/controllers/RoleController.php index 57c972f32..0384009ec 100644 --- a/application/controllers/RolesController.php +++ b/application/controllers/RoleController.php @@ -7,25 +7,22 @@ use Icinga\Forms\Security\RoleForm; use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Notification; -/** - * Roles configuration - */ -class RolesController extends AuthBackendController +class RoleController extends AuthBackendController { /** * List roles */ - public function indexAction() + public function listAction() { $this->assertPermission('config/authentication/roles/show'); - $this->createListTabs()->activate('roles'); + $this->createListTabs()->activate('role/list'); $this->view->roles = Config::app('roles', true); } /** * Create a new role */ - public function newAction() + public function addAction() { $this->assertPermission('config/authentication/roles/add'); $role = new RoleForm(array( @@ -49,9 +46,10 @@ class RolesController extends AuthBackendController ->setTitle($this->translate('New Role')) ->setSubmitLabel($this->translate('Create Role')) ->setIniConfig(Config::app('roles', true)) - ->setRedirectUrl('roles') + ->setRedirectUrl('role/list') ->handleRequest(); $this->view->form = $role; + $this->render('form'); } /** @@ -59,7 +57,7 @@ class RolesController extends AuthBackendController * * @throws Zend_Controller_Action_Exception If the required parameter 'role' is missing or the role does not exist */ - public function updateAction() + public function editAction() { $this->assertPermission('config/authentication/roles/edit'); $name = $this->_request->getParam('role'); @@ -99,9 +97,10 @@ class RolesController extends AuthBackendController } return false; }) - ->setRedirectUrl('roles') + ->setRedirectUrl('role/list') ->handleRequest(); $this->view->form = $role; + $this->render('form'); } /** @@ -148,8 +147,9 @@ class RolesController extends AuthBackendController $confirmation ->setTitle(sprintf($this->translate('Remove Role %s'), $name)) ->setSubmitLabel($this->translate('Remove Role')) - ->setRedirectUrl('roles') + ->setRedirectUrl('role/list') ->handleRequest(); $this->view->form = $confirmation; + $this->render('form'); } } diff --git a/application/views/scripts/roles/update.phtml b/application/views/scripts/role/form.phtml similarity index 50% rename from application/views/scripts/roles/update.phtml rename to application/views/scripts/role/form.phtml index ca1e1559e..cbf06590d 100644 --- a/application/views/scripts/roles/update.phtml +++ b/application/views/scripts/role/form.phtml @@ -1,6 +1,6 @@
    - showOnlyCloseButton() ?> + showOnlyCloseButton(); ?>
    - +
    \ No newline at end of file diff --git a/application/views/scripts/roles/index.phtml b/application/views/scripts/role/list.phtml similarity index 95% rename from application/views/scripts/roles/index.phtml rename to application/views/scripts/role/list.phtml index 17e947249..766ba26f3 100644 --- a/application/views/scripts/roles/index.phtml +++ b/application/views/scripts/role/list.phtml @@ -22,7 +22,7 @@
    translate('Backend'); ?> translate('Remove'); ?>
    - qlink( $backendName, 'usergroupbackend/edit', array('backend' => $backendName), array('title' => sprintf($this->translate('Edit user group backend %s'), $backendName)) ); ?> - - escape($backendName); ?> - qlink( null, 'usergroupbackend/remove', @@ -54,7 +38,6 @@ $removePermitted = $this->hasPermission('config/application/usergroupbackend/rem 'icon' => 'trash' ) ); ?>
    - hasPermission('config/application/groups/show') && $membership->backend instanceof Selectable): ?> + hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?> qlink($membership->group_name, 'group/show', array( 'backend' => $membership->backend->getName(), 'group' => $membership->group_name diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index a423b8023..ad9921303 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -151,7 +151,7 @@ class AuthBackendController extends Controller ); } - if ($this->hasPermission('config/application/groups/show')) { + if ($this->hasPermission('config/authentication/groups/show')) { $tabs->add( 'group/list', array( From cf96e66ff2a1589b1504280d4a5e23cdfcb1d19e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 09:07:06 +0200 Subject: [PATCH 231/239] Rename permission config/application/roles* to config/authentication/... Does also split it into *, show, add, edit, remove as this should behave like any other authentication configuration. refs #8826 --- application/controllers/RolesController.php | 8 ++++---- application/forms/Security/RoleForm.php | 6 +++++- library/Icinga/Web/Controller/AuthBackendController.php | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/application/controllers/RolesController.php b/application/controllers/RolesController.php index b889083c6..57c972f32 100644 --- a/application/controllers/RolesController.php +++ b/application/controllers/RolesController.php @@ -17,7 +17,7 @@ class RolesController extends AuthBackendController */ public function indexAction() { - $this->assertPermission('config/application/roles'); + $this->assertPermission('config/authentication/roles/show'); $this->createListTabs()->activate('roles'); $this->view->roles = Config::app('roles', true); } @@ -27,7 +27,7 @@ class RolesController extends AuthBackendController */ public function newAction() { - $this->assertPermission('config/application/roles'); + $this->assertPermission('config/authentication/roles/add'); $role = new RoleForm(array( 'onSuccess' => function (RoleForm $role) { $name = $role->getElement('name')->getValue(); @@ -61,7 +61,7 @@ class RolesController extends AuthBackendController */ public function updateAction() { - $this->assertPermission('config/application/roles'); + $this->assertPermission('config/authentication/roles/edit'); $name = $this->_request->getParam('role'); if (empty($name)) { throw new Zend_Controller_Action_Exception( @@ -111,7 +111,7 @@ class RolesController extends AuthBackendController */ public function removeAction() { - $this->assertPermission('config/application/roles'); + $this->assertPermission('config/authentication/roles/remove'); $name = $this->_request->getParam('role'); if (empty($name)) { throw new Zend_Controller_Action_Exception( diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index b3b7aa9cd..7175b0931 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -28,7 +28,6 @@ class RoleForm extends ConfigForm 'config/application/authentication' => 'config/application/authentication', 'config/application/resources' => 'config/application/resources', 'config/application/usergroupbackend' => 'config/application/usergroupbackend', - 'config/application/roles' => 'config/application/roles', 'config/authentication/users/*' => 'config/authentication/users/*', 'config/authentication/users/show' => 'config/authentication/users/show', 'config/authentication/users/add' => 'config/authentication/users/add', @@ -41,6 +40,11 @@ class RoleForm extends ConfigForm 'config/authentication/groups/remove' => 'config/authentication/groups/remove', 'config/application/groups/member/add' => 'config/application/groups/member/add', 'config/application/groups/member/remove' => 'config/application/groups/member/remove', + 'config/authentication/roles/*' => 'config/authentication/roles/*', + 'config/authentication/roles/show' => 'config/authentication/roles/show', + 'config/authentication/roles/add' => 'config/authentication/roles/add', + 'config/authentication/roles/edit' => 'config/authentication/roles/edit', + 'config/authentication/roles/remove' => 'config/authentication/roles/remove', 'config/modules' => 'config/modules' ); diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index ad9921303..aafa2d703 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -163,7 +163,7 @@ class AuthBackendController extends Controller ); } - if ($this->hasPermission('config/application/roles')) { + if ($this->hasPermission('config/authentication/roles/show')) { $tabs->add( 'roles', array( From 9bd5d4148ee1293a7987f36627b65f26304ebc51 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 09:08:16 +0200 Subject: [PATCH 232/239] Drop permission config/application/groups/member refs #8826 --- application/controllers/GroupController.php | 6 +++--- application/controllers/UserController.php | 6 +++--- application/forms/Security/RoleForm.php | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index ef95dc3fc..fe8dd58f8 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -129,7 +129,7 @@ class GroupController extends AuthBackendController $this->view->members = $members; $this->createShowTabs($backend->getName(), $groupName)->activate('group/show'); - if ($this->hasPermission('config/application/groups/member/remove') && $backend instanceof Reducible) { + if ($this->hasPermission('config/authentication/groups/edit') && $backend instanceof Reducible) { $removeForm = new Form(); $removeForm->setUidDisabled(); $removeForm->setAction( @@ -228,7 +228,7 @@ class GroupController extends AuthBackendController */ public function addmemberAction() { - $this->assertPermission('config/application/groups/member/add'); + $this->assertPermission('config/authentication/groups/edit'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); @@ -256,7 +256,7 @@ class GroupController extends AuthBackendController */ public function removememberAction() { - $this->assertPermission('config/application/groups/member/remove'); + $this->assertPermission('config/authentication/groups/edit'); $this->assertHttpMethod('POST'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index a8db8d541..30e0bc100 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -121,7 +121,7 @@ class UserController extends AuthBackendController $memberships ); - if ($this->hasPermission('config/application/groups/member/add')) { + if ($this->hasPermission('config/authentication/groups/edit')) { $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); $this->view->showCreateMembershipLink = ! empty($extensibleBackends); } else { @@ -133,7 +133,7 @@ class UserController extends AuthBackendController $this->view->memberships = $memberships; $this->createShowTabs($backend->getName(), $userName)->activate('user/show'); - if ($this->hasPermission('config/application/groups/member/remove')) { + if ($this->hasPermission('config/authentication/groups/edit')) { $removeForm = new Form(); $removeForm->setUidDisabled(); $removeForm->addElement('hidden', 'user_name', array( @@ -228,7 +228,7 @@ class UserController extends AuthBackendController */ public function createmembershipAction() { - $this->assertPermission('config/application/groups/member/add'); + $this->assertPermission('config/authentication/groups/edit'); $userName = $this->params->getRequired('user'); $backend = $this->getUserBackend($this->params->getRequired('backend')); diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 7175b0931..afedf772f 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -38,8 +38,6 @@ class RoleForm extends ConfigForm 'config/authentication/groups/add' => 'config/authentication/groups/add', 'config/authentication/groups/edit' => 'config/authentication/groups/edit', 'config/authentication/groups/remove' => 'config/authentication/groups/remove', - 'config/application/groups/member/add' => 'config/application/groups/member/add', - 'config/application/groups/member/remove' => 'config/application/groups/member/remove', 'config/authentication/roles/*' => 'config/authentication/roles/*', 'config/authentication/roles/show' => 'config/authentication/roles/show', 'config/authentication/roles/add' => 'config/authentication/roles/add', From 8875ce7d950956e953a8d6247ec8cfd922fb13cd Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 09:09:11 +0200 Subject: [PATCH 233/239] Provide permission config/authentication/* --- application/forms/Security/RoleForm.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index afedf772f..03f5e06ca 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -28,6 +28,7 @@ class RoleForm extends ConfigForm 'config/application/authentication' => 'config/application/authentication', 'config/application/resources' => 'config/application/resources', 'config/application/usergroupbackend' => 'config/application/usergroupbackend', + 'config/authentication/*' => 'config/authentication/*', 'config/authentication/users/*' => 'config/authentication/users/*', 'config/authentication/users/show' => 'config/authentication/users/show', 'config/authentication/users/add' => 'config/authentication/users/add', From 2490d0ae67b53a93d737cead1a2bb59d5477c6bb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 09:58:57 +0200 Subject: [PATCH 234/239] ConfigController: We're configuring user backends from now on refs #8826 --- application/controllers/ConfigController.php | 81 ++++++++++--------- .../DbBackendForm.php | 10 +-- .../ExternalBackendForm.php | 6 +- .../LdapBackendForm.php | 10 +-- ...nfigForm.php => UserBackendConfigForm.php} | 52 ++++++------ ...derForm.php => UserBackendReorderForm.php} | 8 +- application/forms/Security/RoleForm.php | 2 +- .../create.phtml | 0 .../modify.phtml | 0 .../remove.phtml | 0 .../reorder.phtml | 4 +- .../scripts/form/reorder-authbackend.phtml | 16 ++-- .../application/forms/AuthBackendPage.php | 8 +- .../DbBackendFormTest.php | 16 ++-- .../LdapBackendFormTest.php | 16 ++-- ...est.php => UserBackendReorderFormTest.php} | 16 ++-- 16 files changed, 124 insertions(+), 121 deletions(-) rename application/forms/Config/{Authentication => UserBackend}/DbBackendForm.php (90%) rename application/forms/Config/{Authentication => UserBackend}/ExternalBackendForm.php (93%) rename application/forms/Config/{Authentication => UserBackend}/LdapBackendForm.php (94%) rename application/forms/Config/{AuthenticationBackendConfigForm.php => UserBackendConfigForm.php} (87%) rename application/forms/Config/{AuthenticationBackendReorderForm.php => UserBackendReorderForm.php} (87%) rename application/views/scripts/config/{authentication => userbackend}/create.phtml (100%) rename application/views/scripts/config/{authentication => userbackend}/modify.phtml (100%) rename application/views/scripts/config/{authentication => userbackend}/remove.phtml (100%) rename application/views/scripts/config/{authentication => userbackend}/reorder.phtml (71%) rename test/php/application/forms/Config/{Authentication => UserBackend}/DbBackendFormTest.php (79%) rename test/php/application/forms/Config/{Authentication => UserBackend}/LdapBackendFormTest.php (79%) rename test/php/application/forms/Config/{AuthenticationBackendReorderFormTest.php => UserBackendReorderFormTest.php} (64%) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index ab4b62049..6b7dbc705 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -5,14 +5,15 @@ use Icinga\Application\Config; use Icinga\Application\Icinga; use Icinga\Application\Modules\Module; use Icinga\Data\ResourceFactory; -use Icinga\Forms\Config\AuthenticationBackendConfigForm; -use Icinga\Forms\Config\AuthenticationBackendReorderForm; +use Icinga\Forms\Config\UserBackendConfigForm; +use Icinga\Forms\Config\UserBackendReorderForm; use Icinga\Forms\Config\GeneralConfigForm; use Icinga\Forms\Config\ResourceConfigForm; use Icinga\Forms\ConfirmRemovalForm; use Icinga\Security\SecurityException; use Icinga\Web\Controller; use Icinga\Web\Notification; +use Icinga\Web\Url; use Icinga\Web\Widget; /** @@ -45,13 +46,13 @@ class ConfigController extends Controller )); $allowedActions[] = 'general'; } - if ($auth->hasPermission('config/application/authentication')) { - $tabs->add('authentication', array( + if ($auth->hasPermission('config/application/userbackend')) { + $tabs->add('userbackend', array( 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'), 'label' => $this->translate('Authentication'), - 'url' => 'config/authentication' + 'url' => 'config/userbackend' )); - $allowedActions[] = 'authentication'; + $allowedActions[] = 'userbackend'; } if ($auth->hasPermission('config/application/resources')) { $tabs->add('resource', array( @@ -191,71 +192,72 @@ class ConfigController extends Controller } /** - * Action for listing and reordering authentication backends + * Action for listing and reordering user backends */ - public function authenticationAction() + public function userbackendAction() { - $this->assertPermission('config/application/authentication'); - $form = new AuthenticationBackendReorderForm(); + $this->assertPermission('config/application/userbackend'); + $form = new UserBackendReorderForm(); $form->setIniConfig(Config::app('authentication')); $form->handleRequest(); $this->view->form = $form; - $this->view->tabs->activate('authentication'); - $this->render('authentication/reorder'); + $this->view->tabs->activate('userbackend'); + $this->render('userbackend/reorder'); } /** - * Action for creating a new authentication backend + * Action for creating a new user backend */ - public function createauthenticationbackendAction() + public function createuserbackendAction() { - $this->assertPermission('config/application/authentication'); - $form = new AuthenticationBackendConfigForm(); - $form->setTitle($this->translate('Create New Authentication Backend')); + $this->assertPermission('config/application/userbackend'); + $form = new UserBackendConfigForm(); + $form->setTitle($this->translate('Create New User Backend')); $form->addDescription($this->translate( 'Create a new backend for authenticating your users. This backend' . ' will be added at the end of your authentication order.' )); $form->setIniConfig(Config::app('authentication')); $form->setResourceConfig(ResourceFactory::getResourceConfigs()); - $form->setRedirectUrl('config/authentication'); + $form->setRedirectUrl('config/userbackend'); $form->handleRequest(); $this->view->form = $form; - $this->view->tabs->activate('authentication'); - $this->render('authentication/create'); + $this->view->tabs->activate('userbackend'); + $this->render('userbackend/create'); } /** - * Action for editing authentication backends + * Action for editing user backends */ - public function editauthenticationbackendAction() + public function edituserbackendAction() { - $this->assertPermission('config/application/authentication'); - $form = new AuthenticationBackendConfigForm(); - $form->setTitle($this->translate('Edit Backend')); + $this->assertPermission('config/application/userbackend'); + $form = new UserBackendConfigForm(); + $form->setTitle($this->translate('Edit User Backend')); $form->setIniConfig(Config::app('authentication')); $form->setResourceConfig(ResourceFactory::getResourceConfigs()); - $form->setRedirectUrl('config/authentication'); + $form->setRedirectUrl('config/userbackend'); + $form->setAction(Url::fromRequest()); $form->handleRequest(); $this->view->form = $form; - $this->view->tabs->activate('authentication'); - $this->render('authentication/modify'); + $this->view->tabs->activate('userbackend'); + $this->render('userbackend/modify'); } /** - * Action for removing a backend from the authentication list + * Action for removing a user backend */ - public function removeauthenticationbackendAction() + public function removeuserbackendAction() { - $this->assertPermission('config/application/authentication'); + $this->assertPermission('config/application/userbackend'); $form = new ConfirmRemovalForm(array( 'onSuccess' => function ($form) { - $configForm = new AuthenticationBackendConfigForm(); + $configForm = new UserBackendConfigForm(); $configForm->setIniConfig(Config::app('authentication')); - $authBackend = $form->getRequest()->getQuery('auth_backend'); + $authBackend = $form->getRequest()->getQuery('backend'); try { $configForm->remove($authBackend); @@ -266,7 +268,7 @@ class ConfigController extends Controller if ($configForm->save()) { Notification::success(sprintf( - t('Authentication backend "%s" has been successfully removed'), + t('User backend "%s" has been successfully removed'), $authBackend )); } else { @@ -274,13 +276,14 @@ class ConfigController extends Controller } } )); - $form->setTitle($this->translate('Remove Backend')); - $form->setRedirectUrl('config/authentication'); + $form->setTitle($this->translate('Remove User Backend')); + $form->setRedirectUrl('config/userbackend'); + $form->setAction(Url::fromRequest()); $form->handleRequest(); $this->view->form = $form; - $this->view->tabs->activate('authentication'); - $this->render('authentication/remove'); + $this->view->tabs->activate('userbackend'); + $this->render('userbackend/remove'); } /** @@ -363,7 +366,7 @@ class ConfigController extends Controller if ($config->get('resource') === $resource) { $form->addDescription(sprintf( $this->translate( - 'The resource "%s" is currently in use by the authentication backend "%s". ' . + 'The resource "%s" is currently utilized for authentication by user backend "%s". ' . 'Removing the resource can result in noone being able to log in any longer.' ), $resource, diff --git a/application/forms/Config/Authentication/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php similarity index 90% rename from application/forms/Config/Authentication/DbBackendForm.php rename to application/forms/Config/UserBackend/DbBackendForm.php index babd4383b..f096f3e35 100644 --- a/application/forms/Config/Authentication/DbBackendForm.php +++ b/application/forms/Config/UserBackend/DbBackendForm.php @@ -1,7 +1,7 @@ getResourceConfig())); diff --git a/application/forms/Config/Authentication/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php similarity index 93% rename from application/forms/Config/Authentication/ExternalBackendForm.php rename to application/forms/Config/UserBackend/ExternalBackendForm.php index 86087eb66..4f3a4585f 100644 --- a/application/forms/Config/Authentication/ExternalBackendForm.php +++ b/application/forms/Config/UserBackend/ExternalBackendForm.php @@ -1,13 +1,13 @@ getResourceConfig())); diff --git a/application/forms/Config/AuthenticationBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php similarity index 87% rename from application/forms/Config/AuthenticationBackendConfigForm.php rename to application/forms/Config/UserBackendConfigForm.php index 86322902b..0a30dd590 100644 --- a/application/forms/Config/AuthenticationBackendConfigForm.php +++ b/application/forms/Config/UserBackendConfigForm.php @@ -11,11 +11,11 @@ use Icinga\Application\Platform; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; -use Icinga\Forms\Config\Authentication\DbBackendForm; -use Icinga\Forms\Config\Authentication\LdapBackendForm; -use Icinga\Forms\Config\Authentication\ExternalBackendForm; +use Icinga\Forms\Config\UserBackend\DbBackendForm; +use Icinga\Forms\Config\UserBackend\LdapBackendForm; +use Icinga\Forms\Config\UserBackend\ExternalBackendForm; -class AuthenticationBackendConfigForm extends ConfigForm +class UserBackendConfigForm extends ConfigForm { /** * The available resources split by type @@ -76,7 +76,7 @@ class AuthenticationBackendConfigForm extends ConfigForm } /** - * Add a particular authentication backend + * Add a particular user backend * * The backend to add is identified by the array-key `name'. * @@ -90,9 +90,9 @@ class AuthenticationBackendConfigForm extends ConfigForm { $name = isset($values['name']) ? $values['name'] : ''; if (! $name) { - throw new InvalidArgumentException($this->translate('Authentication backend name missing')); + throw new InvalidArgumentException($this->translate('User backend name missing')); } elseif ($this->config->hasSection($name)) { - throw new InvalidArgumentException($this->translate('Authentication backend already exists')); + throw new InvalidArgumentException($this->translate('User backend already exists')); } unset($values['name']); @@ -101,7 +101,7 @@ class AuthenticationBackendConfigForm extends ConfigForm } /** - * Edit a particular authentication backend + * Edit a particular user backend * * @param string $name The name of the backend to edit * @param array $values The values to edit the configuration with @@ -113,11 +113,11 @@ class AuthenticationBackendConfigForm extends ConfigForm public function edit($name, array $values) { if (! $name) { - throw new InvalidArgumentException($this->translate('Old authentication backend name missing')); + throw new InvalidArgumentException($this->translate('Old user backend name missing')); } elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) { - throw new InvalidArgumentException($this->translate('New authentication backend name missing')); + throw new InvalidArgumentException($this->translate('New user backend name missing')); } elseif (! $this->config->hasSection($name)) { - throw new InvalidArgumentException($this->translate('Unknown authentication backend provided')); + throw new InvalidArgumentException($this->translate('Unknown user backend provided')); } $backendConfig = $this->config->getSection($name); @@ -132,7 +132,7 @@ class AuthenticationBackendConfigForm extends ConfigForm } /** - * Remove the given authentication backend + * Remove the given user backend * * @param string $name The name of the backend to remove * @@ -143,9 +143,9 @@ class AuthenticationBackendConfigForm extends ConfigForm public function remove($name) { if (! $name) { - throw new InvalidArgumentException($this->translate('Authentication backend name missing')); + throw new InvalidArgumentException($this->translate('user backend name missing')); } elseif (! $this->config->hasSection($name)) { - throw new InvalidArgumentException($this->translate('Unknown authentication backend provided')); + throw new InvalidArgumentException($this->translate('Unknown user backend provided')); } $backendConfig = $this->config->getSection($name); @@ -154,7 +154,7 @@ class AuthenticationBackendConfigForm extends ConfigForm } /** - * Move the given authentication backend up or down in order + * Move the given user backend up or down in order * * @param string $name The name of the backend to be moved * @param int $position The new (absolute) position of the backend @@ -166,9 +166,9 @@ class AuthenticationBackendConfigForm extends ConfigForm public function move($name, $position) { if (! $name) { - throw new InvalidArgumentException($this->translate('Authentication backend name missing')); + throw new InvalidArgumentException($this->translate('User backend name missing')); } elseif (! $this->config->hasSection($name)) { - throw new InvalidArgumentException($this->translate('Unknown authentication backend provided')); + throw new InvalidArgumentException($this->translate('Unknown user backend provided')); } $backendOrder = $this->config->keys(); @@ -186,7 +186,7 @@ class AuthenticationBackendConfigForm extends ConfigForm } /** - * Add or edit an authentication backend and save the configuration + * Add or edit an user backend and save the configuration * * Performs a connectivity validation using the submitted values. A checkbox is * added to the form to skip the check if it fails and redirection is aborted. @@ -197,20 +197,20 @@ class AuthenticationBackendConfigForm extends ConfigForm { if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) { $backendForm = $this->getBackendForm($this->getElement('type')->getValue()); - if (false === $backendForm::isValidAuthenticationBackend($this)) { + if (false === $backendForm::isValidUserBackend($this)) { $this->addElement($this->getForceCreationCheckbox()); return false; } } - $authBackend = $this->request->getQuery('auth_backend'); + $authBackend = $this->request->getQuery('backend'); try { if ($authBackend === null) { // create new backend $this->add($this->getValues()); - $message = $this->translate('Authentication backend "%s" has been successfully created'); + $message = $this->translate('User backend "%s" has been successfully created'); } else { // edit existing backend $this->edit($authBackend, $this->getValues()); - $message = $this->translate('Authentication backend "%s" has been successfully changed'); + $message = $this->translate('User backend "%s" has been successfully changed'); } } catch (InvalidArgumentException $e) { Notification::error($e->getMessage()); @@ -225,7 +225,7 @@ class AuthenticationBackendConfigForm extends ConfigForm } /** - * Populate the form in case an authentication backend is being edited + * Populate the form in case an user backend is being edited * * @see Form::onRequest() * @@ -233,12 +233,12 @@ class AuthenticationBackendConfigForm extends ConfigForm */ public function onRequest() { - $authBackend = $this->request->getQuery('auth_backend'); + $authBackend = $this->request->getQuery('backend'); if ($authBackend !== null) { if ($authBackend === '') { - throw new ConfigurationError($this->translate('Authentication backend name missing')); + throw new ConfigurationError($this->translate('User backend name missing')); } elseif (! $this->config->hasSection($authBackend)) { - throw new ConfigurationError($this->translate('Unknown authentication backend provided')); + throw new ConfigurationError($this->translate('Unknown user backend provided')); } elseif ($this->config->getSection($authBackend)->backend === null) { throw new ConfigurationError( sprintf($this->translate('Backend "%s" has no `backend\' setting'), $authBackend) diff --git a/application/forms/Config/AuthenticationBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php similarity index 87% rename from application/forms/Config/AuthenticationBackendReorderForm.php rename to application/forms/Config/UserBackendReorderForm.php index 34f20d851..9069e73e3 100644 --- a/application/forms/Config/AuthenticationBackendReorderForm.php +++ b/application/forms/Config/UserBackendReorderForm.php @@ -7,7 +7,7 @@ use InvalidArgumentException; use Icinga\Web\Notification; use Icinga\Forms\ConfigForm; -class AuthenticationBackendReorderForm extends ConfigForm +class UserBackendReorderForm extends ConfigForm { /** * Initialize this form @@ -38,7 +38,7 @@ class AuthenticationBackendReorderForm extends ConfigForm } /** - * Update the authentication backend order and save the configuration + * Update the user backend order and save the configuration * * @see Form::onSuccess() */ @@ -62,13 +62,13 @@ class AuthenticationBackendReorderForm extends ConfigForm } /** - * Return the config form for authentication backends + * Return the config form for user backends * * @return ConfigForm */ protected function getConfigForm() { - $form = new AuthenticationBackendConfigForm(); + $form = new UserBackendConfigForm(); $form->setIniConfig($this->config); return $form; } diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 03f5e06ca..ee5d312f0 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -25,8 +25,8 @@ class RoleForm extends ConfigForm 'config/*' => 'config/*', 'config/application/*' => 'config/application/*', 'config/application/general' => 'config/application/general', - 'config/application/authentication' => 'config/application/authentication', 'config/application/resources' => 'config/application/resources', + 'config/application/userbackend' => 'config/application/userbackend', 'config/application/usergroupbackend' => 'config/application/usergroupbackend', 'config/authentication/*' => 'config/authentication/*', 'config/authentication/users/*' => 'config/authentication/users/*', diff --git a/application/views/scripts/config/authentication/create.phtml b/application/views/scripts/config/userbackend/create.phtml similarity index 100% rename from application/views/scripts/config/authentication/create.phtml rename to application/views/scripts/config/userbackend/create.phtml diff --git a/application/views/scripts/config/authentication/modify.phtml b/application/views/scripts/config/userbackend/modify.phtml similarity index 100% rename from application/views/scripts/config/authentication/modify.phtml rename to application/views/scripts/config/userbackend/modify.phtml diff --git a/application/views/scripts/config/authentication/remove.phtml b/application/views/scripts/config/userbackend/remove.phtml similarity index 100% rename from application/views/scripts/config/authentication/remove.phtml rename to application/views/scripts/config/userbackend/remove.phtml diff --git a/application/views/scripts/config/authentication/reorder.phtml b/application/views/scripts/config/userbackend/reorder.phtml similarity index 71% rename from application/views/scripts/config/authentication/reorder.phtml rename to application/views/scripts/config/userbackend/reorder.phtml index e4b72d7e1..64a6fc594 100644 --- a/application/views/scripts/config/authentication/reorder.phtml +++ b/application/views/scripts/config/userbackend/reorder.phtml @@ -2,8 +2,8 @@
    - - icon('plus'); ?>translate('Create A New Authentication Backend'); ?> + + icon('plus'); ?>translate('Create A New User Backend'); ?>
    diff --git a/application/views/scripts/form/reorder-authbackend.phtml b/application/views/scripts/form/reorder-authbackend.phtml index cd8001436..20d4e3696 100644 --- a/application/views/scripts/form/reorder-authbackend.phtml +++ b/application/views/scripts/form/reorder-authbackend.phtml @@ -12,22 +12,22 @@
    qlink( $backendNames[$i], - 'config/editAuthenticationBackend', - array('auth_backend' => $backendNames[$i]), + 'config/edituserbackend', + array('backend' => $backendNames[$i]), array( 'icon' => 'edit', - 'title' => sprintf($this->translate('Edit authentication backend %s'), $backendNames[$i]) + 'title' => sprintf($this->translate('Edit user backend %s'), $backendNames[$i]) ) ); ?> qlink( '', - 'config/removeAuthenticationBackend', - array('auth_backend' => $backendNames[$i]), + 'config/removeuserbackend', + array('backend' => $backendNames[$i]), array( 'icon' => 'trash', - 'title' => sprintf($this->translate('Remove authentication backend %s'), $backendNames[$i]) + 'title' => sprintf($this->translate('Remove user backend %s'), $backendNames[$i]) ) ); ?> qlink( $name, - 'roles/update', + 'role/edit', array('role' => $name), array('title' => sprintf($this->translate('Edit role %s'), $name)) ); ?> @@ -54,7 +54,7 @@ qlink( '', - 'roles/remove', + 'role/remove', array('role' => $name), array( 'icon' => 'trash', @@ -67,7 +67,7 @@
    - + translate('Create a New Role') ?>
    diff --git a/application/views/scripts/roles/new.phtml b/application/views/scripts/roles/new.phtml deleted file mode 100644 index ca1e1559e..000000000 --- a/application/views/scripts/roles/new.phtml +++ /dev/null @@ -1,6 +0,0 @@ -
    - showOnlyCloseButton() ?> -
    -
    - -
    \ No newline at end of file diff --git a/application/views/scripts/roles/remove.phtml b/application/views/scripts/roles/remove.phtml deleted file mode 100644 index ca1e1559e..000000000 --- a/application/views/scripts/roles/remove.phtml +++ /dev/null @@ -1,6 +0,0 @@ -
    - showOnlyCloseButton() ?> -
    -
    - -
    \ No newline at end of file diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index aafa2d703..45b512c8a 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -165,13 +165,13 @@ class AuthBackendController extends Controller if ($this->hasPermission('config/authentication/roles/show')) { $tabs->add( - 'roles', + 'role/list', array( 'title' => $this->translate( 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' ), 'label' => $this->translate('Roles'), - 'url' => 'roles' + 'url' => 'role/list' ) ); } From 7213379cacb0c10e6d31d8761c1bd023829806aa Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 11:59:04 +0200 Subject: [PATCH 238/239] AuthBackendController: Add final indexAction Required to automatically redirect to the first permitted list action. refs #8826 --- application/controllers/ConfigController.php | 2 +- application/controllers/GroupController.php | 8 -------- application/controllers/UserController.php | 8 -------- .../Web/Controller/AuthBackendController.php | 17 +++++++++++++++++ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 1837d3a89..9d55910e9 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -84,7 +84,7 @@ class ConfigController extends Controller public function indexAction() { if ($this->firstAllowedAction === null) { - throw new SecurityException($this->translate('No permission for configuration')); + throw new SecurityException($this->translate('No permission for application configuration')); } $action = $this->getTabs()->get($this->firstAllowedAction); if (substr($action->getUrl()->getPath(), 0, 7) === 'config/') { diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index fe8dd58f8..70954aaf1 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -17,14 +17,6 @@ use Icinga\Web\Widget; class GroupController extends AuthBackendController { - /** - * Redirect to this controller's list action - */ - public function indexAction() - { - $this->redirectNow('group/list'); - } - /** * List all user groups of a single backend */ diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index 30e0bc100..c71fe9696 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -17,14 +17,6 @@ use Icinga\Web\Widget; class UserController extends AuthBackendController { - /** - * Redirect to this controller's list action - */ - public function indexAction() - { - $this->redirectNow('user/list'); - } - /** * List all users of a single backend */ diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php index 45b512c8a..43ed78fd7 100644 --- a/library/Icinga/Web/Controller/AuthBackendController.php +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -9,6 +9,7 @@ use Icinga\Authentication\User\UserBackend; use Icinga\Authentication\User\UserBackendInterface; use Icinga\Authentication\UserGroup\UserGroupBackend; use Icinga\Authentication\UserGroup\UserGroupBackendInterface; +use Icinga\Security\SecurityException; use Icinga\Web\Controller; /** @@ -16,6 +17,22 @@ use Icinga\Web\Controller; */ class AuthBackendController extends Controller { + /** + * Redirect to the first permitted list action + */ + final public function indexAction() + { + if ($this->hasPermission('config/authentication/users/show')) { + $this->redirectNow('user/list'); + } elseif ($this->hasPermission('config/authentication/groups/show')) { + $this->redirectNow('group/list'); + } elseif ($this->hasPermission('config/authentication/roles/show')) { + $this->redirectNow('role/list'); + } else { + throw new SecurityException($this->translate('No permission for authentication configuration')); + } + } + /** * Return all user backends implementing the given interface * From 267e71f38beb896b2983563f89f4ca7772ba573a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 2 Jun 2015 12:01:02 +0200 Subject: [PATCH 239/239] User: Consider the required permission more important if it has a wildcard refs #9202 --- library/Icinga/User.php | 2 +- test/php/library/Icinga/UserTest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/library/Icinga/User.php b/library/Icinga/User.php index f9df0d664..c322a04d0 100644 --- a/library/Icinga/User.php +++ b/library/Icinga/User.php @@ -426,7 +426,7 @@ class User // matches $any = strpos($requiredPermission, '*'); foreach ($this->permissions as $grantedPermission) { - if ($any !== false && strpos($grantedPermission, '*') === false) { + if ($any !== false) { $wildcard = $any; } else { // If the permit contains a wildcard, grant the permission if it's related to the permit diff --git a/test/php/library/Icinga/UserTest.php b/test/php/library/Icinga/UserTest.php index dc55dc62d..97cf412c0 100644 --- a/test/php/library/Icinga/UserTest.php +++ b/test/php/library/Icinga/UserTest.php @@ -67,13 +67,15 @@ class UserTest extends BaseTestCase 'test', 'test/some/specific', 'test/more/*', - 'test/wildcard-with-wildcard/*' + 'test/wildcard-with-wildcard/*', + 'test/even-more/specific-with-wildcard/*' )); $this->assertTrue($user->can('test')); $this->assertTrue($user->can('test/some/specific')); $this->assertTrue($user->can('test/more/everything')); $this->assertTrue($user->can('test/wildcard-with-wildcard/*')); $this->assertTrue($user->can('test/wildcard-with-wildcard/sub/sub')); + $this->assertTrue($user->can('test/even-more/*')); $this->assertFalse($user->can('not/test')); $this->assertFalse($user->can('test/some/not/so/specific')); $this->assertFalse($user->can('test/wildcard2/*'));