From f64dbc90055403db700941e4691ea451bb971cef Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Fri, 29 Jan 2016 21:25:50 +0100 Subject: [PATCH] began work on PDO auth plugin --- .gitignore | 1 + lib/plugins/authpdo/.travis.yml | 13 + lib/plugins/authpdo/README | 27 ++ lib/plugins/authpdo/_test/general.test.php | 33 ++ lib/plugins/authpdo/_test/sqlite.test.php | 49 +++ lib/plugins/authpdo/_test/test.sqlite3 | Bin 0 -> 14336 bytes lib/plugins/authpdo/auth.php | 374 +++++++++++++++++++++ lib/plugins/authpdo/conf/default.php | 21 ++ lib/plugins/authpdo/conf/metadata.php | 10 + lib/plugins/authpdo/lang/en/lang.php | 16 + lib/plugins/authpdo/lang/en/settings.php | 13 + lib/plugins/authpdo/plugin.info.txt | 7 + 12 files changed, 564 insertions(+) create mode 100644 lib/plugins/authpdo/.travis.yml create mode 100644 lib/plugins/authpdo/README create mode 100644 lib/plugins/authpdo/_test/general.test.php create mode 100644 lib/plugins/authpdo/_test/sqlite.test.php create mode 100644 lib/plugins/authpdo/_test/test.sqlite3 create mode 100644 lib/plugins/authpdo/auth.php create mode 100644 lib/plugins/authpdo/conf/default.php create mode 100644 lib/plugins/authpdo/conf/metadata.php create mode 100644 lib/plugins/authpdo/lang/en/lang.php create mode 100644 lib/plugins/authpdo/lang/en/settings.php create mode 100644 lib/plugins/authpdo/plugin.info.txt diff --git a/.gitignore b/.gitignore index 7410ee1c3..14cc1f129 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 000000000..80368cad3 --- /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 000000000..c99bfbf81 --- /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 000000000..6c48b6957 --- /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 000000000..b60072d94 --- /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 GIT binary patch literal 14336 zcmeI3PjAym6u{?CYR6q#${`v>h}}^yZPXN6N@=UATC%KLRhy<XB!`|NH+G9foRlUJ zT-y^zz6s(3?0yHn0usAgX@Lt+q+WL3gp@cWY*rwZT5qB^6VD%e-p`)#c;?OBt*qbh z<ZkUCa2j%s#0aJ2mXw5$5hbHajE^#=!kCh_qwPOm2uH~DpVkO}MogL{yu|)yf3Pw7 z7hR=E74h^7C3`1LTr|XTf)c;#dXMT4Dyr@!r`f3K<5IKk9h7Fl|6zj}!ddQ@u~o9( zXu5u_)bN7+Qp4G)c%|7-2~(F0@pH1@2?8(JforwG_~DOZrbs5~k<jO9#kTy>+j%=v zuw^0hOV*aHI62w$UAdks*lTuP=IvEGZ|82?cO_I^PWo<2<~9p5SIlP9a<6yR@x6mu zbAP-$_nrLuMkc>4f3vssRdo4OYG7`MQ=_IxB<RtS)&wd9VurM6SMyYb&Z}mMh0XPx z%D-Xf3TauX?fF$%%&l(~?GCN>oqE0Fu<8Wf>7o49Ud<G<1vxn}kxI+J@hiR4QYp(2 zE27^`(u7L0Iv8%mO(6t5T+@2#IPF;>%{r+5w{oYIO3QY+E<ICsxFM#d=(fJ-$LF=~ zJ!pE>ve$JO3ss~maS{NfRmqfU7n@)8V27S~4vjq2s(ZIPj70)SAc6#jL8s2QdIWVu zm5=}u=uKdRzaoskAp8Y?-#Z7!A^{|DehFBV3#vbLm{P*B;NvaMc!I(ohU5S9duOrc zNZ=b07*gi}xu68P1g-?S3v@^T2}GCxG<L=x6aHLz=#T&sI1>VE%pi%4iD?*=&+p!F z%jLP9dB<6}Iq%+_br<KZFSy>$;^Ou4?$T05R$;lWqL+)Hz%B2Q6YN*KtL0iSO${Qh z{B+H61HTHZEDvi3ZsDFMGZ3L;JF~<5ALsuNe!}0X0J;NM^Meq^0_czc5{L)^Ge#+8 zj2kfv!dT3*tk(Pg72(HfA`l%CKmySvpr-wZ&_e)|a*}|d1HAtke@*yn{vo=Gqh?3| z38>kBwG^P+KQnnkuLPX7|6|n&phE&kAi@OV?Pb3>7nbe_VEi8k+uUbi>)Z%K7)&*^ z{{@R^7$Sn8{cp>pI%M2@CicHyZP?yr|4V;Y=6`PpTVSK)4SfQ?!5YZ9%=O(H>i@59 zhrJ|Q<&}Qs@1OQEgl_pv=PrkEiV@Lm0))`Z1MTG?V}>}qf58+hEA+_kFxvlYtH?}0 zx<er|{_%d<)W(fIdqDaX`E0AlH`y2h?|%;8|0nz%egT}zJ`F341O_B9#JQgNXWXKU IQ*K%R0$46pk^lez literal 0 HcmV?d00001 diff --git a/lib/plugins/authpdo/auth.php b/lib/plugins/authpdo/auth.php new file mode 100644 index 000000000..1325bdcff --- /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 000000000..22f8369d0 --- /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 000000000..e020d5c45 --- /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 000000000..de9f81252 --- /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 000000000..503511ace --- /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 000000000..6784fd083 --- /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 -- GitLab