From 3ff0c0f02a7426ec9255bb75b3e5dcca4a4e19dd Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Fri, 28 Jun 2013 19:00:30 +0200 Subject: [PATCH] Adds DbUserBackend to handle the authentication against a sql db. Users should be able to authenticate against an internal DB without setting up additional authentication domains. refs #3769 --- config/authentication.ini | 13 +- config/backends.ini | 3 +- configure.ac | 1 + etc/schema/users.mysql.sql | 12 + etc/schema/users.pgsql.sql | 12 + .../Authentication/Backend/DbUserBackend.php | 185 ++++++++++++++++ library/Icinga/Util/Crypto.php | 57 +++++ .../Authentication/DbUserBackendTest.php | 208 ++++++++++++++++++ 8 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 etc/schema/users.mysql.sql create mode 100644 etc/schema/users.pgsql.sql create mode 100644 library/Icinga/Authentication/Backend/DbUserBackend.php create mode 100644 library/Icinga/Util/Crypto.php create mode 100644 test/php/library/Icinga/Authentication/DbUserBackendTest.php diff --git a/config/authentication.ini b/config/authentication.ini index c01e78b17..89334e609 100644 --- a/config/authentication.ini +++ b/config/authentication.ini @@ -1,9 +1,14 @@ [users] -backend=ldap -hostname=localhost +backend=Db +dbtype=mysql +table=icinga_users +host=localhost +password=icinga +user=icinga +db=icinga + root_dn="ou=people,dc=icinga,dc=org" bind_dn="cn=admin,cn=config" bind_pw=admin user_class=inetOrgPerson -user_name_attribute=uid - +user_name_attribute=uid \ No newline at end of file diff --git a/config/backends.ini b/config/backends.ini index 6e00b04b6..50b6a1b6a 100755 --- a/config/backends.ini +++ b/config/backends.ini @@ -16,5 +16,4 @@ objects_file = "/usr/local/icinga/var/objects.cache" [localfailsafe] type = combo -backends = localdb, locallive, localfile - +backends = localdb, locallive, localfile \ No newline at end of file diff --git a/configure.ac b/configure.ac index 803fd5b6b..71ecd6bdc 100755 --- a/configure.ac +++ b/configure.ac @@ -490,6 +490,7 @@ AS_IF([test "x$ldap_authentication" != xno], >>>>>>> ws-jmosshammer:icinga2-web moja$ git commit ======= AC_CHECK_PHP_MODULE([ldap]) + AC_CHECK_PHP_MODULE([mcrypt]) ldap_enabled="" ) >>>>>>> Add Autoconf based installation with most parameters diff --git a/etc/schema/users.mysql.sql b/etc/schema/users.mysql.sql new file mode 100644 index 000000000..a3e0095f4 --- /dev/null +++ b/etc/schema/users.mysql.sql @@ -0,0 +1,12 @@ +create table icinga_users ( + user_name varchar(255) NOT NULL, + first_name varchar(255), + last_name varchar(255), + email varchar(255), + domain varchar(255), + last_login timestamp, + salt varchar(255), + password varchar(255) NOT NULL, + active BOOL, + PRIMARY KEY (user_name) +); \ No newline at end of file diff --git a/etc/schema/users.pgsql.sql b/etc/schema/users.pgsql.sql new file mode 100644 index 000000000..a3e0095f4 --- /dev/null +++ b/etc/schema/users.pgsql.sql @@ -0,0 +1,12 @@ +create table icinga_users ( + user_name varchar(255) NOT NULL, + first_name varchar(255), + last_name varchar(255), + email varchar(255), + domain varchar(255), + last_login timestamp, + salt varchar(255), + password varchar(255) NOT NULL, + active BOOL, + PRIMARY KEY (user_name) +); \ No newline at end of file diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/Backend/DbUserBackend.php new file mode 100644 index 000000000..10e908d21 --- /dev/null +++ b/library/Icinga/Authentication/Backend/DbUserBackend.php @@ -0,0 +1,185 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Authentication\Backend; + +use Icinga\Util\Crypto as Crypto; +use Icinga\Authentication\User as User; +use Icinga\Authentication\UserBackend; +use Icinga\Authentication\Credentials; +use Icinga\Authentication; + + +/** + * Authenticates users using a sql db as backend. + * @package Icinga\Authentication\Backend + */ +class DbUserBackend implements UserBackend { + + private $db; + + private $userTable; + + 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"; + + /* + * maps the configuration dbtypes to the corresponding Zend-PDOs + */ + private $dbTypeMap = Array( + "mysql" => "PDO_MYSQL", + "pgsql" => "PDO_PGSQL" + ); + + /** + * Creates a DbUserBackend with the given configuration. + * @param $config The configuration-object containing the members host,user,password,db + */ + public function __construct($config){ + $this->dbtype = $config->dbtype; + $this->userTable = $config->table; + + $this->db = \Zend_Db::factory( + $this->dbTypeMap[$config->dbtype], + array( + 'host' => $config->host, + 'username' => $config->user, + 'password' => $config->password, + 'dbname' => $config->db + )); + + /* + * Test the connection settings + */ + $this->db->getConnection(); + $this->db->select()->from($this->userTable,new \Zend_Db_Expr("TRUE")); + } + + /** + * Checks if the user in the given Credentials-object is available. + * @param Credentials $credentials The login credentials of the user. + * @return boolean True when the username is known and currently active. + */ + public function hasUsername(Credentials $credential) + { + $user = $this->getUserByName($credential->getUsername()); + return !empty($user); + } + + /** + * Authenticate a user with the given credentials. + * @param Credentials $credentials + * @return User|null The authenticated user or Null. + */ + public function authenticate(Credentials $credential){ + $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. ' = ?',Crypto::hashPassword( + $credential->getPassword(), + $this->getUserSalt($credential->getUsername()) + )) + ->query()->fetch(); + if(!empty($res)){ + $this->updateLastLogin($credential->getUsername()); + return $this->createUserFromResult($res); + } + } + + /** + * Updates the timestamp containing the time of the last login for + * the user with the given username. + * @param $username The login-name of the user. + */ + private function updateLastLogin($username){ + $this->db->getConnection(); + $this->db->update( + $this->userTable, + array( + $this->LAST_LOGIN_COLUMN => new \Zend_Db_Expr("NOW()") + ), + $this->USER_NAME_COLUMN.' = '.$this->db->quoteInto('?',$username)); + } + + /** + * Fetches the user's 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) + ->query()->fetch(); + return $res[$this->SALT_COLUMN]; + } + + /** + * Fetches 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){ + $this->db->getConnection(); + $res = $this->db-> + select()->from($this->userTable) + ->where($this->USER_NAME_COLUMN.' = ?',$username) + ->where($this->ACTIVE_COLUMN.' = ?',true) + ->query()->fetch(); + if(empty($res)){ + return null; + } + return $this->createUserFromResult($res); + } + + /** + * Creates a new instance of User from the given result-array. + * @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]); + return $usr; + } +} \ No newline at end of file diff --git a/library/Icinga/Util/Crypto.php b/library/Icinga/Util/Crypto.php new file mode 100644 index 000000000..51b3d03a4 --- /dev/null +++ b/library/Icinga/Util/Crypto.php @@ -0,0 +1,57 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Util; + +/** + * Defines cryptographic algorithms that should be globally used to avoid + * inconsistency. + * + * @package Icinga\Util + */ +class Crypto { + + /** + * Creates the hash for a given password. + * @param $password The password that should be hashed. + * @param $salt The salt that will be used. + * @return string The hashed password. + */ + public static function hashPassword($password,$salt){ + return crypt($password,$salt); + } + + /** + * Creates a new randomly generated salt. + * @return string the generated salt. + */ + public static function createSalt(){ + return bin2hex(mcrypt_create_iv(16,MCRYPT_RAND)); + } +} \ No newline at end of file diff --git a/test/php/library/Icinga/Authentication/DbUserBackendTest.php b/test/php/library/Icinga/Authentication/DbUserBackendTest.php new file mode 100644 index 000000000..75f89dfb4 --- /dev/null +++ b/test/php/library/Icinga/Authentication/DbUserBackendTest.php @@ -0,0 +1,208 @@ + 'PDO_MYSQL', + 'pgsql' => 'PDO_PGSQL' + ); + + protected function setUp() + { + $this->users = Array( + 0 => Array( + $this->USER_NAME_COLUMN => 'user1', + $this->PASSWORD_COLUMN => 'secret1', + $this->SALT_COLUMN => '8a7487a539c5d1d6766639d04d1ed1e6', + $this->ACTIVE_COLUMN => true + ), + 1 => Array( + $this->USER_NAME_COLUMN => 'user2', + $this->PASSWORD_COLUMN => 'secret2', + $this->SALT_COLUMN => '04b5521ddd761b5a5b633be83faa494d', + $this->ACTIVE_COLUMN => true + ), + 2 => Array( + $this->USER_NAME_COLUMN => 'user3', + $this->PASSWORD_COLUMN => 'secret3', + $this->SALT_COLUMN => '08bb94ba3120338ae56db80ef551d324', + $this->ACTIVE_COLUMN => false + ) + ); + + // TODO: Fetch config folder from somewhere instead of defining it statically. + Config::$configDir = "/vagrant/config"; + $config = Config::app('authentication')->users; + $config->table = $this->testTable; + + $this->db = \Zend_Db::factory($this->dbTypeMap[$config->dbtype], + array( + 'host' => $config->host, + 'username' => $config->user, + 'password' => $config->password, + 'dbname' => $config->db + )); + + if($config->dbtype == 'pgsql'){ + $this->users[0][$this->ACTIVE_COLUMN] = "TRUE"; + $this->users[1][$this->ACTIVE_COLUMN] = "TRUE"; + $this->users[2][$this->ACTIVE_COLUMN] = "FALSE"; + } + $this->setUpDb($this->db); + $this->dbUserBackend = new DbUserBackend($config); + } + + public function tearDown() + { + $this->tearDownDb($this->db); + } + + private function setUpDb($db){ + + $db->exec('CREATE TABLE '.$this->testTable.' ( + '.$this->USER_NAME_COLUMN.' varchar(255) NOT NULL, + '.$this->FIRST_NAME_COLUMN.' varchar(255), + '.$this->LAST_NAME_COLUMN.' varchar(255), + '.$this->LAST_LOGIN_COLUMN.' timestamp, + '.$this->SALT_COLUMN.' varchar(255), + '.$this->DOMAIN_COLUMN.' varchar(255), + '.$this->EMAIL_COLUMN.' varchar(255), + '.$this->PASSWORD_COLUMN.' varchar(255) NOT NULL, + '.$this->ACTIVE_COLUMN.' BOOL, + PRIMARY KEY ('.$this->USER_NAME_COLUMN.') + )'); + + for($i = 0; $i < count($this->users); $i++){ + $usr = $this->users[$i]; + $data = Array( + $this->USER_NAME_COLUMN => $usr[$this->USER_NAME_COLUMN], + $this->PASSWORD_COLUMN => Crypto::hashPassword( + $usr[$this->PASSWORD_COLUMN], + $usr[$this->SALT_COLUMN]), + $this->ACTIVE_COLUMN => $usr[$this->ACTIVE_COLUMN], + $this->SALT_COLUMN => $usr[$this->SALT_COLUMN] + ); + $db->insert($this->testTable,$data); + } + } + + private function tearDownDb($db){ + $db->exec('DROP TABLE '.$this->testTable); + } + + /** + * Test for DbUserBackend::HasUsername() + **/ + public function testHasUsername(){ + + // Known user + $this->assertTrue($this->dbUserBackend->hasUsername( + new Credentials( + $this->users[0][$this->USER_NAME_COLUMN], + $this->users[0][$this->PASSWORD_COLUMN]) + )); + + // Unknown user + $this->assertFalse($this->dbUserBackend->hasUsername( + new Credentials( + 'unkown user', + 'secret') + )); + + // Inactive user + $this->assertFalse($this->dbUserBackend->hasUsername( + new Credentials( + $this->users[2][$this->USER_NAME_COLUMN], + $this->users[2][$this->PASSWORD_COLUMN]) + )); + + } + + /** + * Test for DbUserBackend::Authenticate() + * + **/ + public function testAuthenticate(){ + // Known user + $this->assertNotNull($this->dbUserBackend->authenticate( + new Credentials( + $this->users[0][$this->USER_NAME_COLUMN], + $this->users[0][$this->PASSWORD_COLUMN]) + )); + + // Wrong password + $this->assertNull( + $this->dbUserBackend->authenticate( + new Credentials( + $this->users[1][$this->USER_NAME_COLUMN], + 'wrongpassword') + ) + ); + + // Nonexistend user + $this->assertNull( + $this->dbUserBackend->authenticate( + new Credentials( + 'nonexistend user', + $this->users[1][$this->PASSWORD_COLUMN]) + ) + ); + + // Inactive user + $this->assertNull($this->dbUserBackend->authenticate( + new Credentials( + $this->users[2][$this->USER_NAME_COLUMN], + $this->users[2][$this->PASSWORD_COLUMN]) + )); + } +} \ No newline at end of file