diff --git a/.gitignore b/.gitignore index 7410ee1c3e71bd8e654216c86323551491589fe9..14cc1f129d9f0af789fb50b9c527e63725fb38a2 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ !/lib/plugins/authldap !/lib/plugins/authmysql !/lib/plugins/authpgsql +!/lib/plugins/authpdo !/lib/plugins/authplain !/lib/plugins/config !/lib/plugins/extension diff --git a/lib/plugins/authpdo/.travis.yml b/lib/plugins/authpdo/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..80368cad3ffc2bb4d8467152f999194e7c547215 --- /dev/null +++ b/lib/plugins/authpdo/.travis.yml @@ -0,0 +1,13 @@ +# Config file for travis-ci.org + +language: php +php: + - "5.5" + - "5.4" + - "5.3" +env: + - DOKUWIKI=master + - DOKUWIKI=stable +before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh +install: sh travis.sh +script: cd _test && phpunit --stderr --group plugin_authpdo \ No newline at end of file diff --git a/lib/plugins/authpdo/README b/lib/plugins/authpdo/README new file mode 100644 index 0000000000000000000000000000000000000000..c99bfbf8143b9ae4d8b1cfaae1fb3b82d42c42f4 --- /dev/null +++ b/lib/plugins/authpdo/README @@ -0,0 +1,27 @@ +authpdo Plugin for DokuWiki + +Authenticate against a database via PDO + +All documentation for this plugin can be found at +https://www.dokuwiki.org/plugin:authpdo + +If you install this plugin manually, make sure it is installed in +lib/plugins/authpdo/ - if the folder is called different it +will not work! + +Please refer to http://www.dokuwiki.org/plugins for additional info +on how to install plugins in DokuWiki. + +---- +Copyright (C) Andreas Gohr <andi@splitbrain.org> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; version 2 of the License + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +See the COPYING file in your DokuWiki folder for details diff --git a/lib/plugins/authpdo/_test/general.test.php b/lib/plugins/authpdo/_test/general.test.php new file mode 100644 index 0000000000000000000000000000000000000000..6c48b6957969d52c13fe36a50523d34994593d6a --- /dev/null +++ b/lib/plugins/authpdo/_test/general.test.php @@ -0,0 +1,33 @@ +<?php +/** + * General tests for the authpdo plugin + * + * @group plugin_authpdo + * @group plugins + */ +class general_plugin_authpdo_test extends DokuWikiTest { + + /** + * Simple test to make sure the plugin.info.txt is in correct format + */ + public function test_plugininfo() { + $file = __DIR__.'/../plugin.info.txt'; + $this->assertFileExists($file); + + $info = confToHash($file); + + $this->assertArrayHasKey('base', $info); + $this->assertArrayHasKey('author', $info); + $this->assertArrayHasKey('email', $info); + $this->assertArrayHasKey('date', $info); + $this->assertArrayHasKey('name', $info); + $this->assertArrayHasKey('desc', $info); + $this->assertArrayHasKey('url', $info); + + $this->assertEquals('authpdo', $info['base']); + $this->assertRegExp('/^https?:\/\//', $info['url']); + $this->assertTrue(mail_isvalid($info['email'])); + $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']); + $this->assertTrue(false !== strtotime($info['date'])); + } +} diff --git a/lib/plugins/authpdo/_test/sqlite.test.php b/lib/plugins/authpdo/_test/sqlite.test.php new file mode 100644 index 0000000000000000000000000000000000000000..b60072d944cbc19a856287ae647003e76e55a193 --- /dev/null +++ b/lib/plugins/authpdo/_test/sqlite.test.php @@ -0,0 +1,49 @@ +<?php + +/** + * General tests for the authpdo plugin + * + * @group plugin_authpdo + * @group plugins + */ +class sqlite_plugin_authpdo_test extends DokuWikiTest { + + protected $dbfile; + + public function setUp() { + parent::setUp(); + $this->dbfile = tempnam('/tmp/', 'pluginpdo_test_'); + copy(__DIR__ . '/test.sqlite3', $this->dbfile); + + global $conf; + + $conf['plugin']['authpdo']['debug'] = 1; + $conf['plugin']['authpdo']['dsn'] = 'sqlite:' . $this->dbfile; + $conf['plugin']['authpdo']['user'] = ''; + $conf['plugin']['authpdo']['pass'] = ''; + + + $conf['plugin']['authpdo']['select-user'] = 'SELECT id as uid, login as user, name, pass as clear, mail FROM user WHERE login = :user'; + } + + public function tearDown() { + parent::tearDown(); + unlink($this->dbfile); + } + + public function test_userinfo() { + global $conf; + $auth = new auth_plugin_authpdo(); + + // clear text pasword (with default config above + $this->assertFalse($auth->checkPass('nobody', 'nope')); + $this->assertFalse($auth->checkPass('admin', 'nope')); + $this->assertTrue($auth->checkPass('admin', 'password')); + + // now with a hashed password + $conf['plugin']['authpdo']['select-user'] = 'SELECT id as uid, login as user, name, pass as hash, mail FROM user WHERE login = :user'; + $this->assertFalse($auth->checkPass('admin', 'password')); + $this->assertFalse($auth->checkPass('user', md5('password'))); + + } +} diff --git a/lib/plugins/authpdo/_test/test.sqlite3 b/lib/plugins/authpdo/_test/test.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..403bf5f7224ab809f443f1af2b4ffc5e0ed9aa85 Binary files /dev/null and b/lib/plugins/authpdo/_test/test.sqlite3 differ diff --git a/lib/plugins/authpdo/auth.php b/lib/plugins/authpdo/auth.php new file mode 100644 index 0000000000000000000000000000000000000000..1325bdcff237d54d7f9b7ccb67b64110e1e90bb5 --- /dev/null +++ b/lib/plugins/authpdo/auth.php @@ -0,0 +1,374 @@ +<?php +/** + * DokuWiki Plugin authpdo (Auth Component) + * + * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html + * @author Andreas Gohr <andi@splitbrain.org> + */ + +// must be run within Dokuwiki +if(!defined('DOKU_INC')) die(); + +class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { + + /** @var PDO */ + protected $pdo; + + /** + * Constructor. + */ + public function __construct() { + parent::__construct(); // for compatibility + + if(!class_exists('PDO')) { + $this->_debug('PDO extension for PHP not found.', -1, __LINE__); + $this->success = false; + return; + } + + if(!$this->getConf('dsn')) { + $this->_debug('No DSN specified', -1, __LINE__); + $this->success = false; + return; + } + + try { + $this->pdo = new PDO( + $this->getConf('dsn'), + $this->getConf('user'), + $this->getConf('pass'), + array( + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC + ) + ); + } catch(PDOException $e) { + $this->_debug($e); + $this->success = false; + return; + } + + // FIXME set capabilities accordingly + //$this->cando['addUser'] = false; // can Users be created? + //$this->cando['delUser'] = false; // can Users be deleted? + //$this->cando['modLogin'] = false; // can login names be changed? + //$this->cando['modPass'] = false; // can passwords be changed? + //$this->cando['modName'] = false; // can real names be changed? + //$this->cando['modMail'] = false; // can emails be changed? + //$this->cando['modGroups'] = false; // can groups be changed? + //$this->cando['getUsers'] = false; // can a (filtered) list of users be retrieved? + //$this->cando['getUserCount']= false; // can the number of users be retrieved? + //$this->cando['getGroups'] = false; // can a list of available groups be retrieved? + //$this->cando['external'] = false; // does the module do external auth checking? + //$this->cando['logout'] = true; // can the user logout again? (eg. not possible with HTTP auth) + + // FIXME intialize your auth system and set success to true, if successful + $this->success = true; + } + + /** + * Check user+password + * + * May be ommited if trustExternal is used. + * + * @param string $user the user name + * @param string $pass the clear text password + * @return bool + */ + public function checkPass($user, $pass) { + + $data = $this->_selectUser($user); + if($data == false) return false; + + if(isset($data['hash'])) { + // hashed password + $passhash = new PassHash(); + return $passhash->verify_hash($pass, $data['hash']); + } else { + // clear text password in the database O_o + return ($pass == $data['clear']); + } + } + + /** + * Return user info + * + * Returns info about the given user needs to contain + * at least these fields: + * + * name string full name of the user + * mail string email addres of the user + * grps array list of groups the user is in + * + * @param string $user the user name + * @param bool $requireGroups whether or not the returned data must include groups + * @return array containing user data or false + */ + public function getUserData($user, $requireGroups = true) { + $data = $this->_selectUser($user); + if($data == false) return false; + + if($requireGroups) { + + } + + return $data; + } + + + /** + * Create a new User [implement only where required/possible] + * + * Returns false if the user already exists, null when an error + * occurred and true if everything went well. + * + * The new user HAS TO be added to the default group by this + * function! + * + * Set addUser capability when implemented + * + * @param string $user + * @param string $pass + * @param string $name + * @param string $mail + * @param null|array $grps + * @return bool|null + */ + //public function createUser($user, $pass, $name, $mail, $grps = null) { + // FIXME implement + // return null; + //} + + /** + * Modify user data [implement only where required/possible] + * + * Set the mod* capabilities according to the implemented features + * + * @param string $user nick of the user to be changed + * @param array $changes array of field/value pairs to be changed (password will be clear text) + * @return bool + */ + //public function modifyUser($user, $changes) { + // FIXME implement + // return false; + //} + + /** + * Delete one or more users [implement only where required/possible] + * + * Set delUser capability when implemented + * + * @param array $users + * @return int number of users deleted + */ + //public function deleteUsers($users) { + // FIXME implement + // return false; + //} + + /** + * Bulk retrieval of user data [implement only where required/possible] + * + * Set getUsers capability when implemented + * + * @param int $start index of first user to be returned + * @param int $limit max number of users to be returned + * @param array $filter array of field/pattern pairs, null for no filter + * @return array list of userinfo (refer getUserData for internal userinfo details) + */ + //public function retrieveUsers($start = 0, $limit = -1, $filter = null) { + // FIXME implement + // return array(); + //} + + /** + * Return a count of the number of user which meet $filter criteria + * [should be implemented whenever retrieveUsers is implemented] + * + * Set getUserCount capability when implemented + * + * @param array $filter array of field/pattern pairs, empty array for no filter + * @return int + */ + //public function getUserCount($filter = array()) { + // FIXME implement + // return 0; + //} + + /** + * Define a group [implement only where required/possible] + * + * Set addGroup capability when implemented + * + * @param string $group + * @return bool + */ + //public function addGroup($group) { + // FIXME implement + // return false; + //} + + /** + * Retrieve groups [implement only where required/possible] + * + * Set getGroups capability when implemented + * + * @param int $start + * @param int $limit + * @return array + */ + //public function retrieveGroups($start = 0, $limit = 0) { + // FIXME implement + // return array(); + //} + + /** + * Return case sensitivity of the backend + * + * When your backend is caseinsensitive (eg. you can login with USER and + * user) then you need to overwrite this method and return false + * + * @return bool + */ + public function isCaseSensitive() { + return true; + } + + /** + * Sanitize a given username + * + * This function is applied to any user name that is given to + * the backend and should also be applied to any user name within + * the backend before returning it somewhere. + * + * This should be used to enforce username restrictions. + * + * @param string $user username + * @return string the cleaned username + */ + public function cleanUser($user) { + return $user; + } + + /** + * Sanitize a given groupname + * + * This function is applied to any groupname that is given to + * the backend and should also be applied to any groupname within + * the backend before returning it somewhere. + * + * This should be used to enforce groupname restrictions. + * + * Groupnames are to be passed without a leading '@' here. + * + * @param string $group groupname + * @return string the cleaned groupname + */ + public function cleanGroup($group) { + return $group; + } + + /** + * Check Session Cache validity [implement only where required/possible] + * + * DokuWiki caches user info in the user's session for the timespan defined + * in $conf['auth_security_timeout']. + * + * This makes sure slow authentication backends do not slow down DokuWiki. + * This also means that changes to the user database will not be reflected + * on currently logged in users. + * + * To accommodate for this, the user manager plugin will touch a reference + * file whenever a change is submitted. This function compares the filetime + * of this reference file with the time stored in the session. + * + * This reference file mechanism does not reflect changes done directly in + * the backend's database through other means than the user manager plugin. + * + * Fast backends might want to return always false, to force rechecks on + * each page load. Others might want to use their own checking here. If + * unsure, do not override. + * + * @param string $user - The username + * @return bool + */ + //public function useSessionCache($user) { + // FIXME implement + //} + + /** + * Select data of a specified user + * + * @param $user + * @return bool|array + */ + protected function _selectUser($user) { + $sql = $this->getConf('select-user'); + + try { + $sth = $this->pdo->prepare($sql); + $sth->execute(array(':user' => $user)); + $result = $sth->fetchAll(); + $sth->closeCursor(); + $sth = null; + } catch(PDOException $e) { + $this->_debug($e); + $result = array(); + } + $found = count($result); + if($found == 0) return false; + + if($found > 1) { + $this->_debug('Found more than one matching user', -1, __LINE__); + return false; + } + + $data = array_shift($result); + $dataok = true; + + if(!isset($data['user'])) { + $this->_debug("Statement did not return 'user' attribute", -1, __LINE__); + $dataok = false; + } + if(!isset($data['hash']) && !isset($data['clear'])) { + $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__); + $dataok = false; + } + if(!isset($data['name'])) { + $this->_debug("Statement did not return 'name' attribute", -1, __LINE__); + $dataok = false; + } + if(!isset($data['mail'])) { + $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__); + $dataok = false; + } + + if(!$dataok) return false; + return $data; + } + + /** + * Wrapper around msg() but outputs only when debug is enabled + * + * @param string|Exception $message + * @param int $err + * @param int $line + */ + protected function _debug($message, $err = 0, $line = 0) { + if(!$this->getConf('debug')) return; + if(is_a($message, 'Exception')) { + $err = -1; + $line = $message->getLine(); + $msg = $message->getMessage(); + } else { + $msg = $message; + } + + if(defined('DOKU_UNITTEST')) { + printf("\n%s, %s:%d\n", $msg, __FILE__, $line); + } else { + msg('authpdo: ' . $msg, $err, $line, __FILE__); + } + } +} + +// vim:ts=4:sw=4:et: diff --git a/lib/plugins/authpdo/conf/default.php b/lib/plugins/authpdo/conf/default.php new file mode 100644 index 0000000000000000000000000000000000000000..22f8369d0bb4c4648a45cbbea01d655c20888534 --- /dev/null +++ b/lib/plugins/authpdo/conf/default.php @@ -0,0 +1,21 @@ +<?php +/** + * Default settings for the authpdo plugin + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + +//$conf['fixme'] = 'FIXME'; + +$conf['debug'] = 0; +$conf['dsn'] = ''; +$conf['user'] = ''; +$conf['pass'] = ''; + +/** + * statement to select a single user identified by its login name given as :user + * + * return; user, name, mail, (clear|hash), [uid] + * other fields are returned but not used + */ +$conf['select-user'] = ''; diff --git a/lib/plugins/authpdo/conf/metadata.php b/lib/plugins/authpdo/conf/metadata.php new file mode 100644 index 0000000000000000000000000000000000000000..e020d5c450c871b4e2cf192cca7b69c8e7061028 --- /dev/null +++ b/lib/plugins/authpdo/conf/metadata.php @@ -0,0 +1,10 @@ +<?php +/** + * Options for the authpdo plugin + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + + +//$meta['fixme'] = array('string'); + diff --git a/lib/plugins/authpdo/lang/en/lang.php b/lib/plugins/authpdo/lang/en/lang.php new file mode 100644 index 0000000000000000000000000000000000000000..de9f8125289a6fe9ef83f92dc526af6c3d07d968 --- /dev/null +++ b/lib/plugins/authpdo/lang/en/lang.php @@ -0,0 +1,16 @@ +<?php +/** + * English language file for authpdo plugin + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + +// menu entry for admin plugins +// $lang['menu'] = 'Your menu entry'; + +// custom language strings for the plugin +// $lang['fixme'] = 'FIXME'; + + + +//Setup VIM: ex: et ts=4 : diff --git a/lib/plugins/authpdo/lang/en/settings.php b/lib/plugins/authpdo/lang/en/settings.php new file mode 100644 index 0000000000000000000000000000000000000000..503511aceb668b53e09a2b92eaa3f337ae2eb1b4 --- /dev/null +++ b/lib/plugins/authpdo/lang/en/settings.php @@ -0,0 +1,13 @@ +<?php +/** + * english language file for authpdo plugin + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + +// keys need to match the config setting name +// $lang['fixme'] = 'FIXME'; + + + +//Setup VIM: ex: et ts=4 : diff --git a/lib/plugins/authpdo/plugin.info.txt b/lib/plugins/authpdo/plugin.info.txt new file mode 100644 index 0000000000000000000000000000000000000000..6784fd08368d1f4d9787c227bfa5a6642af98fb9 --- /dev/null +++ b/lib/plugins/authpdo/plugin.info.txt @@ -0,0 +1,7 @@ +base authpdo +author Andreas Gohr +email andi@splitbrain.org +date 2016-01-29 +name authpdo plugin +desc Authenticate against a database via PDO +url https://www.dokuwiki.org/plugin:authpdo