diff --git a/.vagrant-puppet/files/etc/icingaweb/authentication.ini b/.vagrant-puppet/files/etc/icingaweb/authentication.ini index d5b0d3e33..551cee143 100644 --- a/.vagrant-puppet/files/etc/icingaweb/authentication.ini +++ b/.vagrant-puppet/files/etc/icingaweb/authentication.ini @@ -1,11 +1,9 @@ [internal_ldap_authentication] -backend = ldap -resource = internal_ldap -; Object class of the user -user_class = inetOrgPerson -; ; Attribute name for username +backend = ldap +resource = internal_ldap +user_class = inetOrgPerson user_name_attribute = uid [internal_db_authentication] -backend = db -resource = "internal_db" +backend = db +resource = internal_db diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index 7709a81c0..0a4888cf3 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -30,11 +30,14 @@ # namespace Icinga\Application\Controllers; -use \Exception; use Icinga\Web\Controller\ActionController; -use Icinga\Authentication\Credential; use Icinga\Authentication\Manager as AuthManager; use Icinga\Form\Authentication\LoginForm; +use Icinga\Authentication\AuthChain; +use Icinga\Application\Config; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\ConfigurationError; +use Icinga\User; /** * Application wide controller for authentication @@ -64,16 +67,52 @@ class AuthenticationController extends ActionController $this->redirectNow($redirectUrl); } if ($this->view->form->isSubmittedAndValid()) { - $credentials = new Credential( - $this->view->form->getValue('username'), - $this->view->form->getValue('password') + $user = new User( + $this->view->form->getValue('username') ); - if (!$auth->authenticate($credentials)) { - $this->view->form->getElement('password') - ->addError(t('Incorrect username or password')); - } else { - $this->redirectNow($redirectUrl); + try { + $config = Config::app('authentication'); + } catch (NotReadableError $e) { + Logger::exception( + new Exception('Cannot load authentication configuration. An exception was thrown:', 0, $e) + ); + throw new ConfigurationError( + 'No authentication methods available. It seems that none authentication method has been set' + . ' up. Please contact your Icinga Web administrator' + ); } + $backendsWithError = 0; + // TODO(el): Currently the user is only notified about authentication backend problems when all backends + // have errors. It may be the case that the authentication backend which provides the user has errors + // but other authentication backends work. In that scenario the user is presented an error message + // saying "Incorrect username or password". We must inform the user that not all authentication methods + // are available. + $backendsTried = 0; + $chain = new AuthChain($config); + foreach ($chain as $backend) { + ++$backendsTried; + try { + if ($backend->authenticate($user, $this->view->form->getValue('password'))) { + $auth->setAuthenticated($user); + $this->redirectNow($redirectUrl); + } + } catch (Exception $e) { + Logger::exception( + new Exception( + 'Cannot authenticate against backend "' . $backend->getName() . '".' + . ' An exception was thrown:', 0, $e + ) + ); + ++$backendsWithError; + } + } + if ($backendsWithError === $backendsTried) { + throw new ConfigurationError( + 'No authentication methods available. It seems that all set up authentication methods have' + . ' errors. Please contact your Icinga Web administrator' + ); + } + $this->view->form->getElement('password')->addError(t('Incorrect username or password')); } } catch (Exception $e) { $this->view->errorInfo = $e->getMessage(); diff --git a/config/authentication.ini.in b/config/authentication.ini.in index bec4f2cab..bbb4d666b 100644 --- a/config/authentication.ini.in +++ b/config/authentication.ini.in @@ -11,12 +11,10 @@ @ldap_auth_disabled@ backend = ldap resource = internal_ldap -; Object class of the user user_class = @ldap_user_objectclass@ -; ; Attribute name for username user_name_attribute = @ldap_attribute_username@ [internal_db_authentication] @internal_auth_disabled@ backend = db -resource = "internal_db" +resource = internal_db diff --git a/config/membership.ini b/config/memberships.ini similarity index 100% rename from config/membership.ini rename to config/memberships.ini diff --git a/library/Icinga/Authentication/AdmissionLoader.php b/library/Icinga/Authentication/AdmissionLoader.php index e8e5f6a16..6f7bb796c 100644 --- a/library/Icinga/Authentication/AdmissionLoader.php +++ b/library/Icinga/Authentication/AdmissionLoader.php @@ -30,6 +30,7 @@ namespace Icinga\Authentication; use Icinga\Application\Config; +use Icinga\Exception\NotReadableError; use Icinga\Util\String; /** @@ -70,7 +71,12 @@ class AdmissionLoader public function getPermissions($username, array $groups) { $permissions = array(); - foreach (Config::app('permissions') as $section) { + try { + $config = Config::app('permissions'); + } catch (NotReadableError $e) { + return $permissions; + } + foreach ($config as $section) { if ($this->match($section, $username, $groups)) { foreach ($section as $key => $value) { if (strpos($key, 'permission') === 0) { @@ -79,7 +85,6 @@ class AdmissionLoader } } } - return $permissions; } @@ -94,15 +99,19 @@ class AdmissionLoader public function getRestrictions($username, array $groups) { $restrictions = array(); - foreach (Config::app('restrictions') as $section) { + try { + $config = Config::app('restrictions'); + } catch (NotReadableError $e) { + return $restrictions; + } + foreach ($config as $section) { if ($this->match($section, $username, $groups)) { - if (array_key_exists($section->name, $restrictions) === false) { + if (!array_key_exists($section->name, $restrictions)) { $restrictions[$section->name] = array(); } $restrictions[$section->name][] = $section->restriction; } } - return $restrictions; } } diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php new file mode 100644 index 000000000..3426ea5d5 --- /dev/null +++ b/library/Icinga/Authentication/AuthChain.php @@ -0,0 +1,66 @@ +config = $config; + } + + public function rewind() + { + $this->config->rewind(); + } + + public function current() + { + return $this->currentBackend; + } + + public function key() + { + return $this->config->key(); + } + + public function next() + { + $this->config->next(); + } + + public function valid() + { + if (!$this->config->valid()) { + return false; + } + $backendConfig = $this->config->current(); + if ((bool) $backendConfig->get('disabled', false) === true) { + $this->next(); + return $this->valid(); + } + try { + $name = $this->key(); + $backend = UserBackend::create($name, $backendConfig); + } catch (ConfigurationError $e) { + Logger::exception( + new ConfigurationError( + 'Cannot create authentication backend "' . $name . '". An exception was thrown:', 0, $e + ) + ); + $this->next(); + return $this->valid(); + } + $this->currentBackend = $backend; + return true; + } +} diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/Backend/DbUserBackend.php index 7f1eba270..62260461c 100644 --- a/library/Icinga/Authentication/Backend/DbUserBackend.php +++ b/library/Icinga/Authentication/Backend/DbUserBackend.php @@ -29,336 +29,98 @@ namespace Icinga\Authentication\Backend; -use \Exception; -use \stdClass; -use \Zend_Config; -use \Zend_Db; -use \Zend_Db_Adapter_Abstract; -use \Icinga\Data\ResourceFactory; -use \Icinga\User; -use \Icinga\Authentication\UserBackend; -use \Icinga\Authentication\Credential; -use \Icinga\Authentication; -use \Icinga\Application\Logger; -use \Icinga\Exception\ProgrammingError; -use \Icinga\Exception\ConfigurationError; +use Exception; +use Zend_Db_Expr; +use Icinga\Authentication\UserBackend; +use Icinga\Data\Db\Connection; +use Icinga\User; -/** - * User authentication backend (@see Icinga\Authentication\UserBackend) for - * authentication of users via an SQL database. The credentials needed to access - * the database are configurable via the application.ini - * - * See the UserBackend class (@see Icinga\Authentication\UserBackend) for - * usage information - */ -class DbUserBackend implements UserBackend +class DbUserBackend extends UserBackend { /** - * The database connection that will be used for fetching users + * Connection to the database * - * @var Zend_Db + * @var Connection */ - private $db; + private $conn; - /** - * The name of the user table - * - * @var String - */ - private $userTable = 'account'; - - /** - * Column name to identify active users - * - * @var string - */ - private $activeColumnName = 'active'; - - /** - * Column name to fetch the password - * - * @var string - */ - private $passwordColumnName = 'password'; - - /** - * Column name for password salt - * - * @var string - */ - private $saltColumnName = 'salt'; - - /** - * Column name for user name - * - * @var string - */ - private $userColumnName = 'username'; - - /** - * Column name of email - * - * @var string - */ - private $emailColumnName = null; - - /** - * Name of the backend - * - * @var string - */ - private $name; - - /** - * Create a new DbUserBackend - * - * @param Zend_Config $config The configuration for this authentication backend. - * 'resource' => The name of the resource to use, or an actual - * instance of Zend_Db_Adapter_Abstract - * 'name' => The name of this authentication backend - * - * @throws ConfigurationError When the given resource does not exist. - */ - public function __construct(Zend_Config $config) + public function __construct(Connection $conn) { - if (!isset($config->resource)) { - throw new ConfigurationError('An authentication backend must provide a resource.'); + $this->conn = $conn; + } + + /** + * Test whether the given user exists + * + * @param User $user + * + * @return bool + */ + public function hasUser(User $user) + { + $row = $this->conn->select()->from('account', array(new Zend_Db_Expr(1))) + ->where('username = ?', $user->getUsername()) + ->fetch(); + return $row !== false ? true : false; + } + + /** + * Authenticate + * + * @param User $user + * @param string $password + * + * @return bool + */ + public function authenticate(User $user, $password) + { + $salt = $this->getSalt($user->getUsername()); + if ($salt === null) { + return false; } - $this->name = $config->name; - if ($config->resource instanceof Zend_Db_Adapter_Abstract) { - $this->db = $config->resource; - } else { - $resource = ResourceFactory::createResource(ResourceFactory::getResourceConfig($config->resource)); - $this->db = $resource->getConnection(); + if ($salt === '') { + throw new Exception(); } + $row = $this->conn->select()->from('account', array(new Zend_Db_Expr(1))) + ->where('username = ?', $user->getUsername()) + ->where('active = ?', true) + ->where('password = ?', $this->hashPassword($password, $salt)) + ->fetchRow(); + return $row !== false ? true : false; } /** - * Setter for password column + * Get salt by username * - * @param string $passwordColumnName + * @param string $username + * + * @return string|null */ - public function setPasswordColumnName($passwordColumnName) + private function getSalt($username) { - $this->passwordColumnName = $passwordColumnName; + $row = $this->conn->select()->from('account', array('salt'))->where('username = ?', $username)->fetchRow(); + return $row !== false ? $row->salt : null; } /** - * Setter for password salt column - * - * @param string $saltColumnName - */ - public function setSaltColumnName($saltColumnName) - { - $this->saltColumnName = $saltColumnName; - } - - /** - * Setter for usernamea column - * - * @param string $userColumnName - */ - public function setUserColumnName($userColumnName) - { - $this->userColumnName = $userColumnName; - } - - /** - * Setter for database table - * - * @param String $userTable - */ - public function setUserTable($userTable) - { - $this->userTable = $userTable; - } - - /** - * Setter for column identifying an active user - * - * Set this to null if no active column exists. - * - * @param string $activeColumnName - */ - public function setActiveColumnName($activeColumnName) - { - $this->activeColumnName = $activeColumnName; - } - - /** - * Setter for email column - * - * Set to null if not needed - * - * @param string $emailColumnName - */ - public function setEmailColumnName($emailColumnName) - { - $this->emailColumnName = $emailColumnName; - } - - /** - * Name of the backend - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Check if the user identified by the given credentials is available - * - * @param Credential $credential Credential to find a user in the database - * - * @return boolean True when the username is known and currently active. - */ - public function hasUsername(Credential $credential) - { - $user = $this->getUserByName($credential->getUsername()); - return isset($user); - } - - /** - * Authenticate a user with the given credentials - * - * @param Credential $credential Credential to authenticate - * - * @return User|null The authenticated user or Null. - */ - public function authenticate(Credential $credential) - { - try { - $salt = $this->getUserSalt($credential->getUsername()); - } catch (Exception $e) { - Logger::error( - 'Could not fetch salt from database for user %s. Exception was thrown: %s', - $credential->getUsername(), - $e->getMessage() - ); - return null; - } - $sth = $this->db - ->select()->from($this->userTable) - ->where($this->userColumnName . ' = ?', $credential->getUsername()) - ->where( - $this->passwordColumnName . ' = ?', - $this->createPasswordHash($credential->getPassword(), $salt) - ); - - if ($this->activeColumnName !== null) { - $sth->where($this->activeColumnName . ' = ?', true); - } - - $res = $sth->query()->fetch(); - - if ($res !== false) { - return $this->createUserFromResult($res); - } - - return null; - } - - /** - * Fetch the users salt from the database - * - * @param string$username The user whose salt should be fetched - * - * @return string|null Return the salt-string or null, when the user does not exist - * @throws ProgrammingError - */ - private function getUserSalt($username) - { - $res = $this->db->select() - ->from($this->userTable, $this->saltColumnName) - ->where($this->userColumnName . ' = ?', $username) - ->query()->fetch(); - if ($res !== false) { - return $res->{$this->saltColumnName}; - } else { - throw new ProgrammingError('No Salt found for user "' . $username . '"'); - } - } - - /** - * Create password hash at this place + * Hash a password * * @param string $password * @param string $salt * * @return string */ - protected function createPasswordHash($password, $salt) { + private function hashPassword($password, $salt) { return hash_hmac('sha256', $password, $salt); } /** - * Fetch the user information from the database + * Get the number of users available * - * @param string $username The name of the user - * - * @return User|null Returns the user object, or null when the user does not exist + * @return int */ - private function getUserByName($username) + public function count() { - $this->db->getConnection(); - $sth = $this->db->select() - ->from($this->userTable) - ->where($this->userColumnName .' = ?', $username); - - if ($this->activeColumnName !== null) { - $sth->where($this->activeColumnName .' = ?', true); - } - - $res = $sth->query()->fetch(); - - if ($res !== false) { - return $this->createUserFromResult($res); - } - return null; - - } - - /** - * Create a new instance of User from a query result - * - * @param stdClass $resultRow Result object from database - * - * @return User The created instance of User. - */ - protected function createUserFromResult(stdClass $resultRow) - { - $usr = new User( - $resultRow->{$this->userColumnName}, - null, - null, - (isset($resultRow->{$this->emailColumnName})) ? $resultRow->{$this->emailColumnName} : null - ); - return $usr; - } - - /** - * Return the number of users in this database connection - * - * This class is mainly used for determining whether the authentication backend is valid or not - * - * @return int The number of users set in this backend - * @see UserBackend::getUserCount - */ - public function getUserCount() - { - $query = $this->db->select()->from($this->userTable, 'COUNT(*) as count')->query(); - return $query->fetch()->count; - } - - /** - * Try to connect to the underlying database. - * - * @throws ConfigurationError When the backend is not reachable with the given configuration. - */ - public function connect() - { - $this->db->getConnection(); + return $this->conn->select()->from('account', array('count' => 'COUNT(*)'))->fetch()->count(); } } diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index c4ff7993f..2e80302f1 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -29,170 +29,98 @@ namespace Icinga\Authentication\Backend; -use Icinga\Data\ResourceFactory; -use \stdClass; -use \Zend_Config; -use \Icinga\User; -use \Icinga\Authentication\UserBackend; -use \Icinga\Authentication\Credential; -use \Icinga\Protocol\Ldap; -use \Icinga\Protocol\Ldap\Connection as LdapConnection; -use \Icinga\Application\Config as IcingaConfig; -use \Icinga\Exception\ConfigurationError; +use Icinga\User; +use Icinga\Authentication\UserBackend; +use Icinga\Protocol\Ldap\Connection; -/** - * User authentication backend - */ -class LdapUserBackend implements UserBackend +class LdapUserBackend extends UserBackend { /** - * Ldap resource + * Connection to the LDAP server * * @var Connection **/ - protected $connection; + protected $conn; - /** - * The ldap connection information - * - * @var Zend_Config - */ - private $config; + protected $userClass; - /** - * Name of the backend - * - * @var string - */ - private $name; + protected $userNameAttribute; - /** - * Create a new LdapUserBackend - * - * @param Zend_Config $config The configuration for this authentication backend. - * 'resource' => The name of the resource to use, or an actual - * instance of \Icinga\Protocol\Ldap\Connection. - * 'name' => The name of this authentication backend. - * - * @throws ConfigurationError When the given resource does not exist. - */ - public function __construct(Zend_Config $config) + public function __construct(Connection $conn, $userClass, $userNameAttribute) { - if (!isset($config->resource)) { - throw new ConfigurationError('An authentication backend must provide a resource.'); - } - $this->config = $config; - $this->name = $config->name; - if ($config->resource instanceof LdapConnection) { - $this->connection = $config->resource; - } else { - $this->connection = ResourceFactory::createResource( - ResourceFactory::getResourceConfig($config->resource) + $this->conn = $conn; + $this->userClass = $userClass; + $this->userNameAttribute = $userNameAttribute; + } + + /** + * Create query + * + * @param string $username + * + * @return \Icinga\Protocol\Ldap\Query + **/ + protected function createQuery($username) + { + return $this->conn->select() + ->from( + $this->userClass, + array($this->userNameAttribute) + ) + ->where( + $this->userNameAttribute, + str_replace('*', '', $username) ); - } } /** - * Name of the backend + * Test whether the given user exists * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Test if the username exists - * - * @param Credential $credential Credential to find user in database + * @param User $user * * @return bool */ - public function hasUsername(Credential $credential) + public function hasUser(User $user) { - return $this->connection->fetchOne( - $this->selectUsername($credential->getUsername()) - ) === $credential->getUsername(); - } - - /** - * Removes the '*' character from $string - * - * @param string $string Input string - * - * @return string - **/ - protected function stripAsterisks($string) - { - return str_replace('*', '', $string); - } - - /** - * Tries to fetch the username - * - * @param string $username The username to select - * - * @return stdClass $result - **/ - protected function selectUsername($username) - { - return $this->connection->select() - ->from( - $this->config->user_class, - array( - $this->config->user_name_attribute - ) - ) - ->where( - $this->config->user_name_attribute, - $this->stripAsterisks($username) - ); + $username = $user->getUsername(); + return $this->conn->fetchOne($this->createQuery($username)) === $username; } /** * Authenticate * - * @param Credential $credentials Credential to authenticate + * @param User $user + * @param string $password * - * @return User + * @return bool */ - public function authenticate(Credential $credentials) + public function authenticate(User $user, $password) { - if ($this->connection->testCredentials( - $this->connection->fetchDN($this->selectUsername($credentials->getUsername())), - $credentials->getPassword() - )) { - return new User($credentials->getUsername()); + if ($this->conn->testCredentials( + $this->conn->fetchDN($this->createQuery($user->getUsername())), + $password + ) + ) { + return true; } + return false; } /** - * Return number of users in this backend + * Get the number of users available * - * @return int The number of users set in this backend - * @see UserBackend::getUserCount + * @return int */ - public function getUserCount() + public function count() { - return $this->connection->count( - $this->connection->select()->from( - $this->config->user_class, + return $this->conn->count( + $this->conn->select()->from( + $this->userClass, array( - $this->config->user_name_attribute + $this->userNameAttribute ) ) ); } - - /** - * - * Establish the connection to this authentication backend - * - * @throws \Exception When the connection to the resource is not possible. - */ - public function connect() - { - $this->connection->connect(); - } } + diff --git a/library/Icinga/Authentication/Credential.php b/library/Icinga/Authentication/Credential.php deleted file mode 100644 index fa75b7981..000000000 --- a/library/Icinga/Authentication/Credential.php +++ /dev/null @@ -1,135 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 - * @author Icinga Development Team - * - */ -// {{{ICINGA_LICENSE_HEADER}}} - -namespace Icinga\Authentication; - -/** - * Data holder object for authentication information - * - * This object should be used instead of passing names and - * passwords as primitives in order to allow additional information - * to be provided (like the domain) when needed. - */ -class Credential -{ - /** - * Username - * - * @var string - */ - private $username; - - /** - * Password - * - * @var string - */ - private $password; - - /** - * Domain - * - * @var string - */ - private $domain; - - /** - * Create a new credential object - * - * @param string $username - * @param string $password - * @param string $domain - */ - public function __construct($username = '', $password = null, $domain = null) - { - $this->username = $username; - $this->password = $password; - $this->domain = $domain; - } - - /** - * Getter for username - * - * @return string - */ - public function getUsername() - { - return $this->username; - } - - /** - * Setter for username - * - * @param string $username - */ - public function setUsername($username) - { - $this->username = $username; - } - - /** - * Getter for password - * - * @return string - */ - public function getPassword() - { - return $this->password; - } - - /** - * Setter for password - * - * @param string $password - */ - public function setPassword($password) - { - $this->password = $password; - } - - /** - * Getter for domain - * - * @return string - */ - public function getDomain() - { - return $this->domain; - } - - /** - * Setter for domain - * - * @param string $domain - */ - public function setDomain($domain) - { - $this->domain = $domain; - } -} diff --git a/library/Icinga/Authentication/GroupBackend.php b/library/Icinga/Authentication/GroupBackend.php deleted file mode 100644 index 9fa7cfddb..000000000 --- a/library/Icinga/Authentication/GroupBackend.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 - * @author Icinga Development Team - * - */ -// {{{ICINGA_LICENSE_HEADER}}}} - -namespace Icinga\Authentication; - -/** - * Api behaviour for a group backend - * - * @TODO(mh): Groups not implemented at present, re-implement if needed (#4624) - */ -interface GroupBackend -{ - /** - * Name of the backend - * - * @return string - */ - public function getName(); -} diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index e4e7efc0b..b8f2eab3d 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -33,25 +33,12 @@ use Exception; use Zend_Config; use Icinga\User; use Icinga\Web\Session; -use Icinga\Data\ResourceFactory; use Icinga\Application\Logger; -use Icinga\Exception\ConfigurationError; use Icinga\Exception\NotReadableError; -use Icinga\Exception\ProgrammingError; use Icinga\Application\Config as IcingaConfig; -use Icinga\Authentication\Backend\DbUserBackend; -use Icinga\Authentication\Backend\LdapUserBackend; use Icinga\User\Preferences; use Icinga\User\Preferences\PreferencesStore; -/** - * The authentication manager allows to identify users and - * to persist authentication information in a session. - * - * Direct instantiation is not permitted, the AuthenticationManager - * must be created using the getInstance method. Subsequent getInstance - * calls return the same object and ignore any additional configuration. - **/ class Manager { /** @@ -62,252 +49,41 @@ class Manager private static $instance; /** - * Instance of authenticated user + * Authenticated user * * @var User **/ private $user; - /** - * Array of user backends - * - * @var array - **/ - private $userBackends = array(); - - /** - * The configuration - * - * @var Zend_Config - */ - private $config = null; - - /** - * Creates a new authentication manager using the provided config (or the - * configuration provided in the authentication.ini if no config is given). - * - * @param Zend_Config $config The configuration to use for authentication - * instead of the authentication.ini - **/ - private function __construct(Zend_Config $config = null) + private function __construct() { - if ($config !== null) { - $this->setupBackends($config); - $this->config = $config; - } } /** * Get the authentication manager * - * @param Zend_Config $config - * - * @return self - * @see Manager:__construct + * @return self */ - public static function getInstance(Zend_Config $config = null) + public static function getInstance() { if (self::$instance === null) { - self::$instance = new static($config); + self::$instance = new static(); } return self::$instance; } - /** - * Initialize multiple backends from Zend Config - */ - private function setupBackends(Zend_Config $config) + public function setAuthenticated(User $user, $persist = true) { - foreach ($config as $name => $backendConfig) { - if ((bool) $backendConfig->get('disabled', false) === true) { - continue; - } - if ($backendConfig->name === null) { - $backendConfig->name = $name; - } - $backend = $this->createBackend($backendConfig); - $this->userBackends[$backend->getName()] = $backend; - } - } - - /** - * Create a backend from the given Zend_Config - * - * @param Zend_Config $backendConfig - * - * @return UserBackend - * @throws ConfigurationError - */ - private function createBackend(Zend_Config $backendConfig) - { - 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 "' . $backendConfig->name . '" defines an invalid backend' - . ' class. Backend class "' . $backendConfig->class. '" not found' - ); - } - return new $backendConfig->class($backendConfig); - } - if ($backendConfig->resource === null) { - throw new ConfigurationError( - 'Authentication configuration for backend "' . $backendConfig->name - . '" is missing the resource directive' - ); - } + $username = $user->getUsername(); try { - $type = ResourceFactory::getResourceConfig($backendConfig->resource)->type; - } catch (ProgrammingError $e) { - throw new ConfigurationError( - 'No authentication methods available. It seems that none resources have been set up. ' - . ' Please contact your Icinga Web administrator' + $config = IcingaConfig::app(); + } catch (NotReadableError $e) { + Logger::exception( + new Exception('Cannot load preferences for user "' . $username . '". An exception was thrown', 0, $e) ); + $config = new Zend_Config(array()); } - if ($type === null) { - throw new ConfigurationError( - 'Authentication configuration for backend "%s" is missing the type directive', - $backendConfig->name, - $backendConfig->class - ); - } - switch (strtolower($type)) { - case 'db': - return new DbUserBackend($backendConfig); - case 'ldap': - return new LdapUserBackend($backendConfig); - default: - throw new ConfigurationError( - 'Authentication configuration for backend "' . $backendConfig->name. '" defines an invalid backend' - . ' type. Backend type "' . $type . '" is not supported' - ); - } - } - - /** - * Add a user backend to the stack - * - * @param UserBackend $userBackend - */ - public function addUserBackend(UserBackend $userBackend) - { - $this->userBackends[$userBackend->getName()] = $userBackend; - } - - /** - * Get a user backend by name - * - * @param string $name - * - * @return UserBackend|null - */ - public function getUserBackend($name) - { - return (isset($this->userBackends[$name])) ? $this->userBackends[$name] : null; - } - - /** - * Find the backend which provides the user with the given credentials - * - * @param Credential $credentials - * - * @return UserBackend|null - * @throws ConfigurationError - */ - private function revealBackend(Credential $credentials) - { - if (count($this->userBackends) === 0) { - throw new ConfigurationError( - 'No authentication methods available. It seems that none authentication method has been set up. ' - . ' Please contact your Icinga Web administrator' - ); - } - $backendsWithError = 0; - // TODO(el): Currently the user is only notified about authentication backend problems when all backends - // have errors. It may be the case that the authentication backend which provides the user has errors but other - // authentication backends work. In that scenario the user is presented an error message saying "Incorrect - // username or password". We must inform the user that not all authentication methods are available. - foreach ($this->userBackends as $backend) { - Logger::debug( - 'Asking authentication backend "%s" for user "%s"', - $backend->getName(), - $credentials->getUsername() - ); - try { - $hasUser = $backend->hasUsername($credentials); - } catch (Exception $e) { - Logger::error( - 'Cannot ask authentication backend "%s" for user "%s". An exception was thrown: %s', - $backend->getName(), - $credentials->getUsername(), - $e->getMessage() - ); - ++$backendsWithError; - continue; - } - if ($hasUser === true) { - Logger::debug( - 'Authentication backend "%s" provides user "%s"', - $backend->getName(), - $credentials->getUsername() - ); - return $backend; - } else { - Logger::debug( - 'Authentication backend "%s" does not provide user "%s"', - $backend->getName(), - $credentials->getUsername() - ); - } - } - if ($backendsWithError === count($this->userBackends)) { - throw new ConfigurationError( - 'No authentication methods available. It seems that all set up authentication methods have errors. ' - . ' Please contact your Icinga Web administrator' - ); - } - return null; - } - - /** - * Try to authenticate a user with the given credentials - * - * @param Credential $credentials The credentials to use for authentication - * @param Boolean $persist Whether to persist the authentication result in the current session - * - * @return Boolean Whether the authentication was successful or not - * @throws ConfigurationError - */ - public function authenticate(Credential $credentials, $persist = true) - { - $userBackend = $this->revealBackend($credentials); - if ($userBackend === null) { - Logger::info('Unknown user "%s" tried to log in', $credentials->getUsername()); - return false; - } - if (($user = $userBackend->authenticate($credentials)) === null) { - Logger::info('User "%s" tried to log in with an incorrect password', $credentials->getUsername()); - return false; - } - - $username = $credentials->getUsername(); - - $membership = new Membership(); - - $groups = $membership->getGroupsByUsername($username); - $user->setGroups($groups); - - $admissionLoader = new AdmissionLoader(); - - $user->setPermissions( - $admissionLoader->getPermissions($username, $groups) - ); - - $user->setRestrictions( - $admissionLoader->getRestrictions($username, $groups) - ); - - if (($preferencesConfig = IcingaConfig::app()->preferences) !== null) { + if (($preferencesConfig = $config->preferences) !== null) { try { $preferencesStore = PreferencesStore::create( $preferencesConfig, @@ -315,21 +91,31 @@ class Manager ); $preferences = new Preferences($preferencesStore->load()); } catch (NotReadableError $e) { - Logger::error($e); + Logger::exception( + new Exception( + 'Cannot load preferences for user "' . $username . '". An exception was thrown', 0, $e + ) + ); $preferences = new Preferences(); } } else { $preferences = new Preferences(); } $user->setPreferences($preferences); + $membership = new Membership(); + $groups = $membership->getGroupsByUsername($username); + $user->setGroups($groups); + $admissionLoader = new AdmissionLoader(); + $user->setPermissions( + $admissionLoader->getPermissions($username, $groups) + ); + $user->setRestrictions( + $admissionLoader->getRestrictions($username, $groups) + ); $this->user = $user; if ($persist == true) { $this->persistCurrentUser(); } - - Logger::info('User "%s" logged in', $credentials->getUsername()); - - return true; } /** diff --git a/library/Icinga/Authentication/Membership.php b/library/Icinga/Authentication/Membership.php index fd6141c5b..2451963d9 100644 --- a/library/Icinga/Authentication/Membership.php +++ b/library/Icinga/Authentication/Membership.php @@ -30,6 +30,7 @@ namespace Icinga\Authentication; use Icinga\Application\Config; +use Icinga\Exception\NotReadableError; use Icinga\Util\String; /** @@ -47,10 +48,14 @@ class Membership public function getGroupsByUsername($username) { $groups = array(); - foreach (Config::app('membership') as $section) { + try { + $config = Config::app('memberships'); + } catch (NotReadableError $e) { + return $groups; + } + foreach ($config as $section) { $users = String::trimSplit($section->users); - - if (in_array($username, $users) === true) { + if (in_array($username, $users)) { $groups = array_merge($groups, String::trimSplit($section->groups)); } } diff --git a/library/Icinga/Authentication/UserBackend.php b/library/Icinga/Authentication/UserBackend.php index b7c36d713..216072c28 100644 --- a/library/Icinga/Authentication/UserBackend.php +++ b/library/Icinga/Authentication/UserBackend.php @@ -29,51 +29,133 @@ namespace Icinga\Authentication; -use \Zend_Config; -use \Icinga\User; -use Icinga\Authentication\Credential; +use Countable; +use Zend_Config; +use Icinga\Authentication\Backend\DbUserBackend; +use Icinga\Authentication\Backend\LdapUserBackend; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\User; -/** - * Public api for an user backend object - */ -interface UserBackend +abstract class UserBackend implements Countable { /** - * Test if the username exists + * Name of the backend * - * @param Credential $credentials + * @var string + */ + protected $name; + + /** + * Setter for the backend's name + * + * @param string $name + * + * @return self + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Getter for the backend's name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + public static function create($name, Zend_Config $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)) { + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name . '" defines an invalid backend' + . ' class. Backend class "' . $backendConfig->class. '" not found' + ); + } + return new $backendConfig->class($backendConfig); + } + if ($backendConfig->resource === null) { + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name + . '" is missing the resource directive' + ); + } + if (($backendType = $backendConfig->backend) === null) { + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name + . '" is missing the backend directive' + ); + } + 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); + switch (strtolower($backendType)) { + case 'db': + $backend = new DbUserBackend($resource); + break; + case 'msldap': + $backend = new LdapUserBackend( + $resource, + $backendConfig->get('user_class', 'user'), + $backendConfig->get('user_name_attribute', 'sAMAccountName') + ); + break; + case 'ldap': + if (($userClass = $backendConfig->user_class) === null) { + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name + . '" is missing the user_class directive' + ); + } + if (($userNameAttribute = $backendConfig->user_name_attribute) === null) { + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name + . '" is missing the user_name_attribute directive' + ); + } + $backend = new LdapUserBackend($resource, $userClass, $userNameAttribute); + break; + default: + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name. '" defines an invalid backend' + . ' type. Backend type "' . $backendType . '" is not supported' + ); + } + $backend->setName($name); + return $backend; + } + + /** + * Test whether the given user exists + * + * @param User $user * * @return bool */ - public function hasUsername(Credential $credentials); + abstract public function hasUser(User $user); /** * Authenticate * - * @param Credential $credentials + * @param User $user + * @param string $password * - * @return User + * @return bool */ - public function authenticate(Credential $credentials); - - /** - * Name of the backend - * - * @return string - */ - public function getName(); - - /** - * Get the number of users available through this backend - * - * @return int - */ - public function getUserCount(); - - /** - * Establish the connection to this authentication backend - * - * @throws ConfigurationError When the backend is not reachable with the given configuration. - */ - public function connect(); + abstract public function authenticate(User $user, $password); }