diff --git a/config/authentication.ini b/config/authentication.ini index e07116ca7..bc5b6813a 100644 --- a/config/authentication.ini +++ b/config/authentication.ini @@ -1,26 +1,32 @@ -[users] -backend=ldap -hostname=localhost -root_dn="ou=people,dc=icinga,dc=org" -bind_dn="cn=admin,cn=config" -bind_pw=admin -user_class=inetOrgPerson -user_name_attribute=uid +; authentication.ini +; +; Each section listed in this configuration represents a single backend +; that can be used to authenticate users or groups. Each databse backend must refer +; to a resource defined in resources.ini, +; +; The order of entries in this configuration is used to determine the fallback +; priority in case of an error. If the resource referenced in the first +; entry is not reachable, the next lower entry will be used for authentication. +; Please be aware that this behaviour is not valid for the authentication itself. +; The authentication will only be done against the one available resource with the highest +; priority. -[users-mysql] -backend=Db -dbtype=mysql -table=account -host=localhost -password=icinga -user=icingaweb -db=icingaweb +[users-ldap] +backend = "ldap" +target = "user" +hostname = "localhost" +root_dn = "ou=people,dc=icinga,dc=org" +bind_dn = "cn=admin,cn=config" +bind_pw = "admin" +user_class = "inetOrgPerson" +user_name_attribute = "uid" [users-pgsql] -backend=Db -dbtype=pgsql -table=account -host=localhost -password=icinga -user=icingaweb -db=icingaweb \ No newline at end of file +backend = "db" +target = "user" +resource = "icingaweb-pgsql" + +[users-mysql] +backend = "db" +target = "user" +resource = "icingaweb-mysql" diff --git a/config/config.ini b/config/config.ini index d2a539fcc..1f57302d0 100755 --- a/config/config.ini +++ b/config/config.ini @@ -27,8 +27,4 @@ type=ini ; Use database to store preference into mysql or postgres ;[preferences] ;type=db -;dbtype=pgsql -;dbhost=127.0.0.1 -;dbpassword=icingaweb -;dbuser=icingaweb -;dbname=icingaweb +;resource=icingaweb-mysql diff --git a/config/preferences/KEEP.md b/config/preferences/KEEP.md old mode 100644 new mode 100755 diff --git a/config/resources.ini b/config/resources.ini new file mode 100644 index 000000000..b45ef51e6 --- /dev/null +++ b/config/resources.ini @@ -0,0 +1,38 @@ +; resources.ini +; +; The configuration file *resources.ini* contains data sources that +; can be referenced in other configurations. This allows you to manage +; all connections to SQL databases in one single place, avoiding the need +: to edit several different configuration files, when the connection +; information of a resource change. +; +; Each section represents a resource, with the section name being the +; identifier used to reference this certain section. Depending on the +; resource type, each section contains different properties. The property +; *type* defines the resource type and thus how the properties are going to +; be interpreted. Currently only the resource type *db* is available. + + +[icingaweb-pgsql] +type = db +db = pgsql ; PostgreSQL +host = localhost +password = icinga +username = icingaweb +dbname = icingaweb + +[icingaweb-mysql] +type = db +db = mysql ; MySQL +host = localhost +password = icinga +username = icingaweb +dbname = icingaweb + +[ido] +type = db +dbname = mysql +host = localhost +password = icinga +username = icingaweb +db = icingaweb \ No newline at end of file diff --git a/doc/authentication.md b/doc/authentication.md index 70a22f144..4590dc230 100644 --- a/doc/authentication.md +++ b/doc/authentication.md @@ -1,29 +1,62 @@ -# Authentication via internal DB +# Authentication -The class DbUserBackend allows to handle the user authentication internally in a database. +The authentication manager can use different backend types like LDAP or Databases as data sources. During +the application bootstrap the different available resources are checked for availability and +the resource with the highest priority will be used for authentication. This behaviour is useful for setting +up fallback accounts, that are available when the regular authentication backend is not available. ## Configuration -The internal authentication is configured in *config/authentication.ini*. The value -of the configuration key "backend" will determine which UserBackend class to +The internal authentication is configured in *config/authentication.ini*. + +Each section listed in this configuration represents a single backend +that can be used to authenticate users or groups. + +The order of entries in this configuration is used to determine the fallback +priority in case of an error. If the resource referenced in the first entry (the one at the top if the file) +is not reachable, the next lower entry will be used for authentication. +Please be aware that this behaviour is not valid for the authentication itself. +The authentication will only be done against the one available resource with the highest +priority. When an account is only present in a backend with lower priority, it will not +be able to authenticate when a backend with higher priority is active that does not contain +this account. + +### Backend + +The value of the configuration key *backend* will determine which UserBackend class to load. To use the internal backend you need to specifiy the value "Db" which will cause the class "DbUserBackend" to be loaded. -There are various configuration keys in "Authentication.ini" and some are only -used by specific backends. The internal DB uses the values -*dbtype*,*table*,*host*,*password*,*user* and *db*, which define the used -connection parameters, the database and the table. +Currently these types of backends are allowed: + * ldap + * db -## Database support +#### db -The module currently supports these databases: +The authentication source is a SQL database and points to a resource defined in *resources.ini*, which +contains all the connection information. Every entry should therefore contain a property *resource* +with the name of the assigned resource. For a more detailed description about how to set up resources, +please read the chapter *Resources*. - - mysql (dbtype=mysql) - - PostgreSQL (dbtype=pgsql) +The authentication currently supports the databases MySQL and PostgreSQL. + +#### ldap + +The authentication source is an ldap server. The connection information should be directly present +in the *authentication.ini*, like described in the example configuration. -## Authentication +### target -The backend will store the salted hash of the password in the column "password" and the salt in the column "salt". +The value of the configuration key *target* defines the type of authentication the described backend provides. +The allowed values are *user* for a backend that provides user authentication or *group* for group authentication. + + +## Technical description + +If an ldap-backend is used, the standard ldap bind will be executed and all user credentials will be managed +directly by the ldap server. + +In case of an SQL-backend, the backend will store the salted hash of the password in the column "password" and the salt in the column "salt". When a password is checked, the hash is calculated with the function hash_hmac("sha256",salt,password) and compared -to the stored value. \ No newline at end of file +to the stored value. diff --git a/doc/preferences.md b/doc/preferences.md index cff8734dc..2e1860692 100644 --- a/doc/preferences.md +++ b/doc/preferences.md @@ -29,27 +29,12 @@ example: [preferences] type=db - dbtype=pgsql - dbhost=127.0.0.1 - dbpassword=icingaweb - dbuser=icingaweb - dbname=icingaweb + resource=icingaweb-pgsql ### Settings -* **dbtype**: Database adapter, currently supporting ***mysql*** or ***pgsql*** - -* **dbhost**: Host of the database server, use localhost or 127.0.0.1 -for unix socket transport - -* **dbpassword**: Password for the configured database user - -* **dbuser**: User who can connect to database - -* **dbname**: Name of the database - -* **port**(optional): For network connections the specific port if not default -(3306 for mysql and 5432 for postgres) +* **resource**: A reference to a database declared in *resources.ini*. Please read the chapter about + resources for a detailed description about how to set up resources. ### Preparation diff --git a/doc/resources.md b/doc/resources.md new file mode 100644 index 000000000..805886449 --- /dev/null +++ b/doc/resources.md @@ -0,0 +1,78 @@ +# Resources + +The configuration file *config/resources.ini* contains data sources that can be referenced +in other configurations. This allows you to manage all connections to databases at one central +place, avoiding the need to edit several different files, when the connection information of a resource change. + +## Configuration + +Each section represents a resource, with the section name being the identifier used to +reference this certain section. Depending on the resource type, each section contains different properties. +The property *type* defines the resource type and thus how the properties are going to be interpreted. +Currently only the resource type *db* is available. + +### db + +This resource type describes a SQL database. The property *db* defines the used database vendor, which +could be a value like *mysql* or *pgsql*. The other properties like *host*, *password*, *username* and +*dbname* are the connection information for the resource. + + +## Factory Implementations + +This section contains documentation documentation for the Icinga2-Web developers that want to +use resources defined in the *resources.ini*. Each supported resource type should have an own +factory class, that can be used to comfortably create instances of classes that provide access +to the data of the resources. + + +### DbAdapterFactory + +The DbAdapterFactory can be used to retrieve instances of Zend_Db_Adapter_Abstract for accessing +the data of the SQL database. + +Lets assume for the following examples, that we have an *resources.ini* that looks like this: + + [resource1] + type = "db" + db = "mysql" + dbname = "resource1" + host = "host" + username = "username1" + password = "password1" + + [resource2] + type = "db" + db = "pgsql" + dbname = "resource2" + host = "host" + username = "username2" + password = "password2" + + [resource3] + type = "other" + foo = "foo" + bar = "bar" + + +In the most simple use-case you can create an adapter by calling the +*getDbAdapter* function. The created adapter will be an instance of +Zend_Db_Adapter_Pdo_Mysql + + $adapter = DbAdapterFactory::getDbAdapter('resource1'); + + +If you specify a resource that does not exist or has the wrong type, +the factory will throw an ConfigurationException. You can make sure +a resource exists and has the right type, by calling the function *resourceExists*: + + if (DbAdapterFactory::resourceExists('resource3')) { + $adapter = DbAdapterFactory::getDbAdapter('resource3'); + } else { + // This returned false, because resource3 has a different type than "db" + echo 'resource does not exist, adapter could not be created...' + } + + +You can retrieve a list of all available resources by calling *getResources*. You will +get an array of all resources that have the type 'db'. \ No newline at end of file diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index 651209627..016baf6fd 100755 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -34,6 +34,8 @@ use Zend_Loader_Autoloader; use Icinga\Application\Modules\Manager as ModuleManager; use Icinga\Application\Platform; use \Icinga\Application\Config; +use Icinga\Exception\ProgrammingError; +use \Icinga\Application\DbAdapterFactory; use Icinga\Exception\ConfigurationError; use Icinga\Util\DateTimeFactory; @@ -341,7 +343,19 @@ abstract class ApplicationBootstrap } /** - * Setup time zone + * Setup factories that provide access to the resources + * + * @return self + */ + protected function setupResourceFactories() + { + $config = Config::app('resources'); + DbAdapterFactory::setConfig($config); + return $this; + } + + /** + * Setup default timezone * * @return self * @throws ConfigurationError if the timezone in config.ini isn't valid diff --git a/library/Icinga/Application/DbAdapterFactory.php b/library/Icinga/Application/DbAdapterFactory.php new file mode 100644 index 000000000..0b98a3a0b --- /dev/null +++ b/library/Icinga/Application/DbAdapterFactory.php @@ -0,0 +1,202 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Application; + +use Zend_Config; +use Zend_Db; +use Icinga\Application\Logger; +use Icinga\Util\ConfigAwareFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\ProgrammingError; +use Tests\Icinga\Application\ZendDbMock; + +/** + * Create resources using short identifiers referring to configuration entries + */ +class DbAdapterFactory implements ConfigAwareFactory +{ + /** + * Resource definitions + * + * @var Zend_Config + */ + private static $resources; + + /** + * The factory class used to create instances of Zend_Db_Adapter + * + * @var String + */ + private static $factoryClass; + + /** + * Resource cache to allow multiple use + * + * @var array + */ + private static $resourceCache = array(); + + /** + * Set the configuration that stores the available resources + * + * @param mixed $config The configuration containing the resources + * + * @param array $options Additional options that affect the factories behaviour: + * * factory : Set the factory class that creates instances + * of Zend_Db_Adapter for the different database types + * (used for testing) + */ + public static function setConfig($config, array $options = null) + { + if (is_array($config)) { + $config = new Zend_Config($config); + } + self::$resources = $config; + if (isset($options['factory'])) { + self::$factoryClass = $options['factory']; + } else { + self::$factoryClass = 'Zend_Db'; + } + } + + /** + * Reset the factory configuration back to the default state + */ + public static function resetConfig() + { + unset(self::$resources); + unset(self::$factoryClass); + } + + /** + * Get a list of all resources available to this factory + * + * @return array An array containing all resources compatible to this factory + */ + public static function getResources() + { + $resources = self::$resources->toArray(); + foreach ($resources as $identifier => $resource) { + if ($resource['type'] !== 'db') { + unset($resources[$identifier]); + } + } + return $resources; + } + + /** + * Return if a resource with the given identifier exists + * + * @param $identifier The name of the resource + * + * @return boolean If the resource exists and is compatible + */ + public static function resourceExists($identifier) + { + return isset(self::$resources->{$identifier}) + && (self::$resources->{$identifier}->type === 'db'); + } + + /** + * Get the resource with the given $identifier + * + * @param $identifier The name of the resource + */ + public static function getDbAdapter($identifier) + { + if (!isset(self::$resources)) { + $msg = 'Creation of resource ' . $identifier . ' not possible, because there is no configuration present.' + . ' Make shure this factory class was initialised correctly during the application bootstrap.'; + Logger::error($msg); + throw new ProgrammingError($msg); + } + if (!isset(self::$resources->{$identifier})) { + $msg = 'Creation of resource "' + . $identifier + . '" not possible, because there is no matching resource present in the configuration '; + Logger::error($msg); + throw new ConfigurationError($msg); + } + if (array_key_exists($identifier, self::$resourceCache)) { + return self::$resourceCache[$identifier]; + } else { + $res = self::createDbAdapter(self::$resources->{$identifier}); + self::$resourceCache[$identifier] = $res; + return $res; + } + } + + /** + * Create the Db_Adapter for the given configuration section + * + * @param mixed $config The configuration section containing the + * db information + * + * @return \Zend_Db_Adapter_Abstract The created Zend_Db_Adapter + * + * @throws \ConfigurationError When the specified db type is invalid + */ + private static function createDbAdapter($config) + { + if ($config->type !== 'db') { + throw new ConfigurationError( + 'Resource type must be "db" but is "' . $config->type . '"' + ); + } + $options = array( + 'dbname' => $config->dbname, + 'host' => $config->host, + 'username' => $config->username, + 'password' => $config->password, + ); + switch ($config->db) { + case 'mysql': + return self::callFactory('Pdo_Mysql', $options); + case 'pgsql': + return self::callFactory('Pdo_Pgsql', $options); + default: + throw new ConfigurationError('Unsupported db type ' . $config->db . '.'); + } + } + + /** + * Call the currently set factory class + * + * @param $adapter The name of the used db adapter + * @param $options OPTIONAL: an array or Zend_Config object with adapter + * parameters + * + * @return Zend_Db_Adapter_Abstract The created adapter + */ + private static function callFactory($adapter, $options) + { + $factory = self::$factoryClass; + return $factory::factory($adapter, $options); + } +} diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php index de20dca6e..4bba5fb6b 100644 --- a/library/Icinga/Application/Web.php +++ b/library/Icinga/Application/Web.php @@ -93,6 +93,7 @@ class Web extends ApplicationBootstrap return $this->setupConfig() ->setupErrorHandling() ->setupTimezone() + ->setupResourceFactories() ->setupRequest() ->setupZendMvc() ->setupTranslation() diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/Backend/DbUserBackend.php index 4259b63e7..43b9285b4 100644 --- a/library/Icinga/Authentication/Backend/DbUserBackend.php +++ b/library/Icinga/Authentication/Backend/DbUserBackend.php @@ -29,11 +29,11 @@ namespace Icinga\Authentication\Backend; -use Icinga\User; -use Icinga\Authentication\UserBackend; -use Icinga\Authentication\Credentials; -use Icinga\Authentication; -use Icinga\Application\Logger; +use \Icinga\User; +use \Icinga\Authentication\UserBackend; +use \Icinga\Authentication\Credentials; +use \Icinga\Authentication; +use \Icinga\Application\Logger; /** * User authentication backend (@see Icinga\Authentication\UserBackend) for @@ -43,7 +43,29 @@ use Icinga\Application\Logger; * See the UserBackend class (@see Icinga\Authentication\UserBackend) for * usage information */ -class DbUserBackend implements UserBackend { +class DbUserBackend implements UserBackend +{ + /** + * Mapping of all table column names + */ + + const USER_NAME_COLUMN = 'user_name'; + + const FIRST_NAME_COLUMN = 'first_name'; + + const LAST_NAME_COLUMN = 'last_name'; + + const LAST_LOGIN_COLUMN = 'last_login'; + + const SALT_COLUMN = 'salt'; + + const PASSWORD_COLUMN = 'password'; + + const ACTIVE_COLUMN = 'active'; + + const DOMAIN_COLUMN = 'domain'; + + const EMAIL_COLUMN = 'email'; /** * The database connection that will be used for fetching users @@ -53,72 +75,31 @@ class DbUserBackend implements UserBackend { private $db = null; /** - * The name of the user table as provided by the configuration + * The name of the user table * * @var String */ - private $userTable; - - /** - * Mapping of columns - * - * @var string - */ - private $USER_NAME_COLUMN = 'user_name', - $FIRST_NAME_COLUMN = 'first_name', - $LAST_NAME_COLUMN = 'last_name', - $LAST_LOGIN_COLUMN = 'last_login', - $SALT_COLUMN = 'salt', - $PASSWORD_COLUMN = 'password', - $ACTIVE_COLUMN = 'active', - $DOMAIN_COLUMN = 'domain', - $EMAIL_COLUMN = 'email'; - - /** - * Map the configuration dbtypes to the corresponding Zend-PDOs - * - * @var Array - */ - private $dbTypeMap = Array( - 'mysql' => 'PDO_MYSQL', - 'pgsql' => 'PDO_PGSQL' - ); - + private $userTable = "account"; /** * Create a DbUserBackend * - * @param $config The configuration-object containing the members host,user,password,db + * @param Zend_Db The database that provides the authentication data */ - public function __construct($config) + public function __construct($database) { - $this->dbtype = $config->dbtype; - $this->userTable = $config->table; - try { - $this->db = \Zend_Db::factory( - $this->dbTypeMap[$config->dbtype], - array( - 'host' => $config->host, - 'username' => $config->user, - 'password' => $config->password, - 'dbname' => $config->db - )); + $this->db = $database; - /* - * Test the connection settings - */ - $this->db->getConnection(); - $this->db->select()->from($this->userTable,new \Zend_Db_Expr('TRUE')); - } catch (\Zend_Db_Adapter_Exception $exc) { - Logger::error('Could not authenticate via database : %s ', $exc->getMessage()); - $this->db = null; - - } + /* + * Test if the connection is available + */ + $this->db->getConnection(); } /** * Check if the user identified by the given credentials is available * * @param Credentials $credentials The login credentials + * * @return boolean True when the username is known and currently active. */ public function hasUsername(Credentials $credential) @@ -135,6 +116,7 @@ class DbUserBackend implements UserBackend { * Authenticate a user with the given credentials * * @param Credentials $credentials The login credentials + * * @return User|null The authenticated user or Null. */ public function authenticate(Credentials $credential) @@ -146,12 +128,16 @@ class DbUserBackend implements UserBackend { $this->db->getConnection(); $res = $this->db ->select()->from($this->userTable) - ->where($this->USER_NAME_COLUMN.' = ?',$credential->getUsername()) - ->where($this->ACTIVE_COLUMN. ' = ?',true) - ->where($this->PASSWORD_COLUMN. ' = ?',hash_hmac('sha256', + ->where(self::USER_NAME_COLUMN.' = ?', $credential->getUsername()) + ->where(self::ACTIVE_COLUMN. ' = ?', true) + ->where( + self::PASSWORD_COLUMN. ' = ?', + hash_hmac( + 'sha256', $this->getUserSalt($credential->getUsername()), - $credential->getPassword()) + $credential->getPassword() ) + ) ->query()->fetch(); if (!empty($res)) { $this->updateLastLogin($credential->getUsername()); @@ -171,31 +157,34 @@ class DbUserBackend implements UserBackend { $this->db->update( $this->userTable, array( - $this->LAST_LOGIN_COLUMN => new \Zend_Db_Expr('NOW()') + self::LAST_LOGIN_COLUMN => new \Zend_Db_Expr('NOW()') ), - $this->USER_NAME_COLUMN.' = '.$this->db->quoteInto('?',$username)); + self::USER_NAME_COLUMN.' = '.$this->db->quoteInto('?', $username) + ); } /** * Fetch the users salt from the database * * @param $username The user whose salt should be fetched. + * * @return String|null Returns the salt-string or Null, when the user does not exist. */ private function getUserSalt($username) { $this->db->getConnection(); $res = $this->db->select() - ->from($this->userTable,$this->SALT_COLUMN) - ->where($this->USER_NAME_COLUMN.' = ?',$username) + ->from($this->userTable, self::SALT_COLUMN) + ->where(self::USER_NAME_COLUMN.' = ?', $username) ->query()->fetch(); - return $res[$this->SALT_COLUMN]; + return $res[self::SALT_COLUMN]; } /** * Fetch the user information from the database * * @param $username The name of the user. + * * @return User|null Returns the user object, or null when the user does not exist. */ private function getUserByName($username) @@ -208,8 +197,8 @@ class DbUserBackend implements UserBackend { $this->db->getConnection(); $res = $this->db-> select()->from($this->userTable) - ->where($this->USER_NAME_COLUMN.' = ?',$username) - ->where($this->ACTIVE_COLUMN.' = ?',true) + ->where(self::USER_NAME_COLUMN.' = ?', $username) + ->where(self::ACTIVE_COLUMN.' = ?', true) ->query()->fetch(); if (empty($res)) { return null; @@ -225,16 +214,18 @@ class DbUserBackend implements UserBackend { * Create a new instance of User from a query result * * @param array $result The query result-array containing the column + * * @return User The created instance of User. */ private function createUserFromResult(Array $result) { $usr = new User( - $result[$this->USER_NAME_COLUMN], - $result[$this->FIRST_NAME_COLUMN], - $result[$this->LAST_NAME_COLUMN], - $result[$this->EMAIL_COLUMN]); - $usr->setDomain($result[$this->DOMAIN_COLUMN]); + $result[self::USER_NAME_COLUMN], + $result[self::FIRST_NAME_COLUMN], + $result[self::LAST_NAME_COLUMN], + $result[self::EMAIL_COLUMN] + ); + $usr->setDomain($result[self::DOMAIN_COLUMN]); return $usr; } } diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index 0161368f8..52fc51704 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -45,24 +45,32 @@ use \Icinga\Application\Config as IcingaConfig; class LdapUserBackend implements UserBackend { /** - * @var Ldap\Connection - **/ + * @var Ldap\Connection + **/ protected $connection; /** - * Creates a new Authentication backend using the - * connection information provided in $config - * - * @param object $config The ldap connection information - **/ + * The ldap connection information + * + * @var object + */ + private $config; + + /** + * Creates a new Authentication backend using the + * connection information provided in $config + * + * @param object $config The ldap connection information + **/ public function __construct($config) { $this->connection = new Ldap\Connection($config); + $this->config = $config; } /** - * @see Icinga\Authentication\UserBackend::hasUsername - **/ + * @see Icinga\Authentication\UserBackend::hasUsername + **/ public function hasUsername(Credentials $credential) { return $this->connection->fetchOne( @@ -71,44 +79,44 @@ class LdapUserBackend implements UserBackend } /** - * Removes the '*' characted from $string - * - * @param String $string - * - * @return String - **/ + * Removes the '*' characted from $string + * + * @param String $string + * + * @return String + **/ protected function stripAsterisks($string) { return str_replace('*', '', $string); } /** - * Tries to fetch the username given in $username from - * the ldap connection, using the configuration parameters - * given in the Authentication configuration - * - * @param String $username The username to select - * - * @return object $result - **/ + * Tries to fetch the username given in $username from + * the ldap connection, using the configuration parameters + * given in the Authentication configuration + * + * @param String $username The username to select + * + * @return object $result + **/ protected function selectUsername($username) { return $this->connection->select() ->from( - IcingaConfig::app('authentication')->users->user_class, + $this->config->user_class, array( - IcingaConfig::app('authentication')->users->user_name_attribute + $this->config->user_name_attribute ) ) ->where( - IcingaConfig::app('authentication')->users->user_name_attribute, + $this->config->user_name_attribute, $this->stripAsterisks($username) ); } /** - * @see Icinga\Authentication\UserBackend::authenticate - **/ + * @see Icinga\Authentication\UserBackend::authenticate + **/ public function authenticate(Credentials $credentials) { if (!$this->connection->testCredentials( diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index b1e45c3f4..f8081aa23 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -28,74 +28,75 @@ namespace Icinga\Authentication; -use Icinga\Application\Logger; +use \Icinga\Application\Logger; use \Icinga\Application\Config as IcingaConfig; -use Icinga\Exception\ConfigurationError as ConfigError; -use Icinga\User; +use \Icinga\Application\DbAdapterFactory; +use \Icinga\Exception\ConfigurationError as ConfigError; +use \Icinga\User; /** -* The authentication manager allows to identify users and -* to persist authentication information in a session. -* -* Direct instanciation is not permitted, the Authencation manager -* must be created using the getInstance method. Subsequent getInstance -* calls return the same object and ignore any additional configuration -* -* When creating the Authentication manager with standard PHP Sessions, -* you have to decide whether you want to modify the session on the first -* initialization and provide the 'writeSession' option if so, otherwise -* session changes won't be written to disk. This is done to prevent PHP -* from blockung concurrent requests -* -* @TODO: Group support is not implemented yet -**/ + * The authentication manager allows to identify users and + * to persist authentication information in a session. + * + * Direct instanciation is not permitted, the Authencation manager + * must be created using the getInstance method. Subsequent getInstance + * calls return the same object and ignore any additional configuration + * + * When creating the Authentication manager with standard PHP Sessions, + * you have to decide whether you want to modify the session on the first + * initialization and provide the 'writeSession' option if so, otherwise + * session changes won't be written to disk. This is done to prevent PHP + * from blockung concurrent requests + * + * @TODO: Group support is not implemented yet + **/ class Manager { const BACKEND_TYPE_USER = "User"; const BACKEND_TYPE_GROUP = "Group"; /** - * @var Manager - **/ + * @var Manager + **/ private static $instance = null; /** - * @var User - **/ + * @var User + **/ private $user = null; private $groups = array(); /** - * @var UserBackend - **/ + * @var UserBackend + **/ private $userBackend = null; /** - * @var GroupBackend - **/ + * @var GroupBackend + **/ private $groupBackend = null; /** - * @var Session - **/ + * @var Session + **/ private $session = null; /** - * Creates a new authentication manager using the provided config (or the - * configuration provided in the authentication.ini if no config is given) - * and with the given options. - * - * @param IcingaConfig $config The configuration to use for authentication - * instead of the authentication.ini - * @param Array $options Additional options that affect the managers behaviour. - * Supported values: - * * writeSession : Whether the session should be writable - * * userBackendClass : Allows to provide an own user backend class - * (used for testing) - * * groupBackendClass : Allows to provide an own group backend class - * (used for testing) - * * sessionClass : Allows to provide a different session implementation) - **/ + * Creates a new authentication manager using the provided config (or the + * configuration provided in the authentication.ini if no config is given) + * and with the given options. + * + * @param IcingaConfig $config The configuration to use for authentication + * instead of the authentication.ini + * @param Array $options Additional options that affect the managers behaviour. + * Supported values: + * * writeSession : Whether the session should be writable + * * userBackendClass : Allows to provide an own user backend class + * (used for testing) + * * groupBackendClass : Allows to provide an own group backend class + * (used for testing) + * * sessionClass : Allows to provide a different session implementation) + **/ private function __construct($config = null, array $options = array()) { if ($config === null) { @@ -103,14 +104,14 @@ class Manager } if (isset($options["userBackendClass"])) { $this->userBackend = $options["userBackendClass"]; - } elseif ($config->users !== null) { - $this->userBackend = $this->initBackend(self::BACKEND_TYPE_USER, $config->users); + } else { + $this->userBackend = $this->initBestBackend(self::BACKEND_TYPE_USER, $config); } if (isset($options["groupBackendClass"])) { $this->groupBackend = $options["groupBackendClass"]; - } elseif ($config->groups != null) { - $this->groupBackend = $this->initBackend(self::BACKEND_TYPE_GROUP, $config->groups); + } else { + $this->groupBackend = $this->initBestBackend(self::BACKEND_TYPE_GROUP, $config); } if (!isset($options["sessionClass"])) { @@ -126,8 +127,8 @@ class Manager } /** - * @see Manager:__construct() - **/ + * @see Manager:__construct() + **/ public static function getInstance($config = null, array $options = array()) { if (self::$instance === null) { @@ -137,50 +138,82 @@ class Manager } /** - * Clears the instance (this is mostly needed for testing and shouldn't be called otherwise) - **/ + * Clear the instance (this is mostly needed for testing and shouldn't be called otherwise) + **/ public static function clearInstance() { self::$instance = null; } /** - * Creates a backend for the the given authenticationTarget (User or Group) and the - * Authenticaiton source. - * - * initBackend("User", "Ldap") would create a UserLdapBackend, - * initBackend("Group", "MySource") would create a GroupMySourceBackend, - * - * Supported backends can be found in the Authentication\Backend folder - * - * @param String $authenticationTarget "User" or "Group", depending on what - * authentication information the backend should - * provide - * @param String $authenticationSource The Source, see the above examples - * - * @return (null|UserBackend|GroupBackend) - **/ - private function initBackend($authenticationTarget, $authenticationSource) + * Create a connection to the best available backend + * + * @param String $target "User" or "Group", depending on what + * authentication information the backend should provide + * @param Mixed $backends The configuration containing all backend configurations + * in falling priority + * + * @return (null|UserBackend|GroupBackend) + */ + private function initBestBackend($target, $backends) { - $backend = ucwords(strtolower($authenticationSource->backend)); - - if (!$backend) { + foreach ($backends as $backend) { + if (strtolower($target) === strtolower($backend->target)) { + $db = $this->tryToInitBackend($target, $backend); + if (isset($db)) { + break; + } + } + } + if (!isset($db)) { + $msg = 'Failed to create any authentication backend, login will not be possible.'; + Logger::error($msg); return null; } - - $class = '\\Icinga\\Authentication\\Backend\\' . $backend . $authenticationTarget. 'Backend'; - return new $class($authenticationSource); + return $db; } /** - * Tries to authenticate the current user with the Credentials (@see Credentials). - * - * @param Credentials $credentials The credentials to use for authentication - * @param Boolean $persist Whether to persist the authentication result - * in the current session - * - * @return Boolean true on success, otherwise false - **/ + * Try to create the backend with the given configuration + * + * @param String $target "User" or "Group", depending on what + * authentication information the backend should provide + * @param $backendConfig The configuration containing backend description + * + * @return UserBackend|null Return the created backend or null + */ + private function tryToInitBackend($target, $backendConfig) + { + $type = ucwords(strtolower($backendConfig->backend)); + if (!$type) { + return null; + } + try { + if ($backendConfig->backend === 'db') { + $resource = DbAdapterFactory::getDbAdapter($backendConfig->resource); + } else { + $resource = $backendConfig; + } + $class = '\\Icinga\\Authentication\\Backend\\' . $type . $target. 'Backend'; + return new $class($resource); + } catch (\Exception $e) { + $msg = 'Not able to create backend: ' . + print_r($backendConfig->backend, true) + . '. Exception: ' . $e->getMessage(); + Logger::warn($msg); + return null; + } + } + + /** + * Try to authenticate the current user with the Credentials (@see Credentials). + * + * @param Credentials $credentials The credentials to use for authentication + * @param Boolean $persist Whether to persist the authentication result + * in the current session + * + * @return Boolean true on success, otherwise false + **/ public function authenticate(Credentials $credentials, $persist = true) { if (!$this->userBackend) { @@ -211,29 +244,28 @@ class Manager /** - * Writes the current user to the session (only usable when writeSession = true) - * - **/ + * Writes the current user to the session (only usable when writeSession = true) + **/ public function persistCurrentUser() { $this->session->set("user", $this->user); } /** - * Tries to authenticate the user with the current session - **/ + * Tries to authenticate the user with the current session + **/ public function authenticateFromSession() { $this->user = $this->session->get("user", null); } /** - * Returns true when the user is currently authenticated - * - * @param Boolean $ignoreSession Set to true to prevent authentication by session - * - * @param Boolean - **/ + * Returns true when the user is currently authenticated + * + * @param Boolean $ignoreSession Set to true to prevent authentication by session + * + * @param Boolean + **/ public function isAuthenticated($ignoreSession = false) { if ($this->user === null && !$ignoreSession) { @@ -243,9 +275,8 @@ class Manager } /** - * Purges the current authorisation information and deletes the session - * - **/ + * Purges the current authorisation information and deletes the session + **/ public function removeAuthorization() { $this->user = null; @@ -253,18 +284,18 @@ class Manager } /** - * Returns the current user or null if no user is authenticated - * - * @return User - **/ + * Returns the current user or null if no user is authenticated + * + * @return User + **/ public function getUser() { return $this->user; } /** - * @see User::getGroups - **/ + * @see User::getGroups + **/ public function getGroups() { return $this->user->getGroups(); diff --git a/library/Icinga/User/Preferences/StoreFactory.php b/library/Icinga/User/Preferences/StoreFactory.php index 46c46be33..911212181 100644 --- a/library/Icinga/User/Preferences/StoreFactory.php +++ b/library/Icinga/User/Preferences/StoreFactory.php @@ -30,6 +30,7 @@ namespace Icinga\User\Preferences; use Icinga\User; use Icinga\Exception\ProgrammingError; +use \Icinga\Application\DbAdapterFactory; use \Zend_Config; use \Zend_Db; @@ -53,6 +54,7 @@ final class StoreFactory * * @param Zend_Config $config * @param User $user + * * @return FlushObserverInterface * @throws ProgrammingError */ @@ -68,32 +70,11 @@ final class StoreFactory } $items = $config->toArray(); - unset($items['type']); - // TODO(mh): Encapsulate into a db adapter factory (#4503) - if (isset($items['dbname']) - && isset($items['dbuser']) - && isset($items['dbpassword']) - && isset($items['dbhost']) - && isset($items['dbtype']) - ) { - $zendDbType = 'PDO_'. strtoupper($items['dbtype']); - - $zendDbOptions = array( - 'host' => $items['dbhost'], - 'username' => $items['dbuser'], - 'password' => $items['dbpassword'], - 'dbname' => $items['dbname'] - ); - - if (isset($items['port'])) { - $zendDbOptions['port'] = $items['port']; - } - - $dbAdapter = Zend_Db::factory($zendDbType, $zendDbOptions); - - $items['dbAdapter'] = $dbAdapter; + if ($items['type'] == 'db') { + $items['dbAdapter'] = DbAdapterFactory::getDbAdapter($items['resource']); } + unset($items['type']); foreach ($items as $key => $value) { $setter = 'set'. ucfirst($key); diff --git a/library/Icinga/Util/ConfigAwareFactory.php b/library/Icinga/Util/ConfigAwareFactory.php index 4a40c4200..300118b33 100644 --- a/library/Icinga/Util/ConfigAwareFactory.php +++ b/library/Icinga/Util/ConfigAwareFactory.php @@ -1,5 +1,29 @@ + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ // {{{ICINGA_LICENSE_HEADER}}} namespace Icinga\Util; diff --git a/test/php/library/Icinga/Application/DbAdapterFactoryTest.php b/test/php/library/Icinga/Application/DbAdapterFactoryTest.php new file mode 100644 index 000000000..55b855db3 --- /dev/null +++ b/test/php/library/Icinga/Application/DbAdapterFactoryTest.php @@ -0,0 +1,180 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Tests\Icinga\Application; + +require_once('Zend/Db.php'); +require_once('Zend/Db/Adapter/Pdo/Mysql.php'); +require_once('Zend/Config.php'); +require_once('Zend/Log.php'); +require_once('Zend/Config.php'); +require_once('../../library/Icinga/Application/Logger.php'); +require_once('library/Icinga/Application/ZendDbMock.php'); +require_once('../../library/Icinga/Exception/ConfigurationError.php'); +require_once('../../library/Icinga/Exception/ProgrammingError.php'); +require_once('../../library/Icinga/Util/ConfigAwareFactory.php'); +require_once('../../library/Icinga/Application/DbAdapterFactory.php'); + +use \Tests\Icinga\Application\ZendDbMock; +use \Icinga\Application\DbAdapterFactory; + +/* + * Unit test for the class DbAdapterFactory + */ +class DbAdapterFactoryTest extends \PHPUnit_Framework_TestCase { + + /** + * The resources used for this test + */ + private $resources; + + /** + * Set up the test fixture + */ + public function setUp() + { + $this->resources = array( + /* + * PostgreSQL databse + */ + 'resource1' => array( + 'type' => 'db', + 'db' => 'pgsql', + 'dbname' => 'resource1', + 'host' => 'host1', + 'username' => 'username1', + 'password' => 'password1' + ), + /* + * MySQL database + */ + 'resource2' => array( + 'type' => 'db', + 'db' => 'mysql', + 'dbname' => 'resource2', + 'host' => 'host2', + 'username' => 'username2', + 'password' => 'password2' + ), + /* + * Unsupported database type + */ + 'resource3' => array( + 'type' => 'db', + 'db' => 'mssql', + 'dbname' => 'resource3', + 'host' => 'host3', + 'username' => 'username3', + 'password' => 'password3' + ), + /* + * Unsupported resource type + */ + 'resource4' => array( + 'type' => 'ldap', + ), + ); + DbAdapterFactory::setConfig( + $this->resources, + array( + 'factory' => 'Tests\Icinga\Application\ZendDbMock' + ) + ); + } + + public function testGetValidResource() + { + DbAdapterFactory::getDbAdapter('resource2'); + $this->assertEquals( + 'Pdo_Mysql', + ZendDbMock::getAdapter(), + 'The db adapter name must be Pdo_Mysql.'); + $this->assertEquals( + $this->getOptions($this->resources['resource2']), + ZendDbMock::getConfig(), + 'The options must match the original config file content' + ); + } + + public function testResourceExists() + { + $this->assertTrue(DbAdapterFactory::resourceExists('resource2'), + 'resourceExists() called with an existing resource should return true'); + + $this->assertFalse(DbAdapterFactory::resourceExists('not existing'), + 'resourceExists() called with an existing resource should return false'); + + $this->assertFalse(DbAdapterFactory::resourceExists('resource4'), + 'resourceExists() called with an incompatible resource should return false'); + } + + public function testGetResources() + { + $withoutIncompatible = array_merge(array(),$this->resources); + unset($withoutIncompatible['resource4']); + $this->assertEquals( + $withoutIncompatible, + DbAdapterFactory::getResources(), + 'getResources should return an array of all existing resources that are compatible'); + } + + /** + * Test if an exception is thrown, when an invalid database is used. + * + * @expectedException Icinga\Exception\ConfigurationError + */ + public function testGetInvalidDatabase() + { + DbAdapterFactory::getDbAdapter('resource3'); + } + + /** + * Test if an exception is thrown, when an invalid type is used. + * + * @expectedException Icinga\Exception\ConfigurationError + */ + public function testGetInvalidType() + { + DbAdapterFactory::getDbAdapter('resource4'); + } + + /** + * Prepare the options object for assertions + * + * @param Zend_Config $config The configuration to prepare + * + * @return array The prepared options object + */ + private function getOptions($config) + { + $options = array_merge(array(),$config); + unset($options['type']); + unset($options['db']); + return $options; + } +} diff --git a/test/php/library/Icinga/Application/ZendDbMock.php b/test/php/library/Icinga/Application/ZendDbMock.php new file mode 100644 index 000000000..d7e0aaebb --- /dev/null +++ b/test/php/library/Icinga/Application/ZendDbMock.php @@ -0,0 +1,87 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Tests\Icinga\Application; + +/** + * Partially emulate the functionality of Zend_Db + */ +class ZendDbMock +{ + + /** + * The config that was used in the last call of the factory function + * + * @var mixed + */ + private static $config; + + /** + * Name of the adapter class that was used in the last call of the factory function + * + * @var mixed + */ + private static $adapter; + + /** + * Mock the factory-method of Zend_Db and save the given parameters + * + * @param $adapter String name of base adapter class, or Zend_Config object + * @param $config mixed OPTIONAL; an array or Zend_Config object with adapter + * parameters + * + * @return stdClass Empty object + */ + public static function factory($adapter, $config) + { + self::$config = $config; + self::$adapter = $adapter; + return new \stdClass(); + } + + /** + * Get the name of the adapter class that was used in the last call + * of the factory function + * + * @return String + */ + public static function getAdapter() + { + return self::$adapter; + } + + /** + * Get the config that was used in the last call of the factory function + * + * @return mixed + */ + public static function getConfig() + { + return self::$config; + } +} diff --git a/test/php/library/Icinga/Authentication/DbUserBackendTest.php b/test/php/library/Icinga/Authentication/DbUserBackendTest.php index 72a8bdecb..904404952 100644 --- a/test/php/library/Icinga/Authentication/DbUserBackendTest.php +++ b/test/php/library/Icinga/Authentication/DbUserBackendTest.php @@ -29,9 +29,6 @@ namespace Tests\Icinga\Authentication; -//use Icinga\Protocol\Ldap\Exception; -//use Zend_Config_Ini; - require_once('Zend/Config/Ini.php'); require_once('Zend/Db.php'); require_once('../../library/Icinga/Authentication/UserBackend.php'); @@ -147,7 +144,7 @@ class DbUserBackendTest extends \PHPUnit_Framework_TestCase { $config->dbtype = $dbType; $db = $this->createDb($dbType,$config); $this->setUpDb($db); - return new DbUserBackend($config); + return new DbUserBackend($db); } catch(\Exception $e) { echo 'CREATE_BACKEND_ERROR:'.$e->getMessage(); return null;