From c8806d95fd153cd8a2e0b9f7c372fae26c0e0afb Mon Sep 17 00:00:00 2001 From: lisps Date: Tue, 17 Jun 2014 16:30:36 +0200 Subject: [PATCH 1/8] add old passwords to policy check add password expire time force user to change password warn user to change password add start_date to have a transition time before first password change --- action.php | 69 ++++++++++++++++- conf/default.php | 6 ++ conf/metadata.php | 7 ++ helper.php | 171 ++++++++++++++++++++++++++++++++++++++++--- lang/en/lang.php | 4 + lang/en/settings.php | 5 ++ script.js | 15 +++- 7 files changed, 263 insertions(+), 14 deletions(-) diff --git a/action.php b/action.php index 71fe16e..794571a 100644 --- a/action.php +++ b/action.php @@ -24,12 +24,75 @@ public function register(Doku_Event_Handler &$controller) { $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_forms'); $controller->register_hook('AUTH_USER_CHANGE', 'BEFORE', $this, 'handle_passchange'); + $controller->register_hook('AUTH_USER_CHANGE', 'BEFORE', $this, 'save_pass'); $controller->register_hook('AUTH_PASSWORD_GENERATE', 'BEFORE', $this, 'handle_passgen'); $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_ajax_call'); + + $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'check_act'); + + } + /** + * Handles the warn message and redirects user to the profile page in case of password expired + * + * @param Doku_Event $event + * @param unknown $param + */ + function check_act(Doku_Event &$event,$param) { + if(!$_SERVER['REMOTE_USER']) return; + + if(in_array($event->data,array('login','logout','profile'))) return; + + /* @var $passpolicy helper_plugin_passpolicy */ + $passpolicy = $this->loadHelper('passpolicy'); + + if($expireDate = $passpolicy->checkPasswordExpired()) { //password is expired + msg(sprintf($this->getLang('expired'), date('Y-m-d',$expireDate))); + $event->data = 'profile'; + } else if($expireDate = $passpolicy->checkPasswordExpireWarn()) { //show warn message + if(!isset($_COOKIE['passpolicy_msg_hide'])) { + msg(sprintf($this->getLang('expirewarn'), date('Y-m-d',$expireDate),tpl_action('profile',1,false,true))); + } + + } + + } + + /** + * Save the password to the pass history + * + * @param Doku_Event $event + * @param unknown $param + */ + function save_pass(Doku_Event &$event,$param) { + if($event->data['type'] == 'create') { + $user = $event->data['params'][0]; + $pass = $event->data['params'][1]; + } elseif($event->data['type'] == 'modify') { + $user = $event->data['params'][0]; + if(!isset($event->data['params'][1]['pass'])) { + return; //password is not changed, nothing to do + } + $pass = $event->data['params'][1]['pass']; + } else { + return; + } + + /* @var $passpolicy helper_plugin_passpolicy */ + $passpolicy = $this->loadHelper('passpolicy'); + + $passpolicy->savePassword2PassHistory($user,$pass); + } + + /** + * Check for password policy + * + * @param Doku_Event $event + * @param unknown $param + */ function _ajax_call(Doku_Event &$event,$param) { if ($event->data !== 'plugin_passpolicy') { return; @@ -42,10 +105,13 @@ function _ajax_call(Doku_Event &$event,$param) { /* @var $INPUT \Input */ global $INPUT; + $user = $INPUT->post->str('user',$_SERVER['REMOTE_USER']); $pass = $INPUT->post->str('pass'); + + $passpolicy = $this->loadHelper('passpolicy'); - if(!$passpolicy->checkPolicy($pass, $_SERVER['REMOTE_USER'])) { + if(!$passpolicy->checkPolicy($pass, $user)) { // passpolicy not matched, throw error echo '0'; } else { @@ -70,6 +136,7 @@ public function handle_forms(Doku_Event &$event, $param) { $passpolicy = plugin_load('helper', 'passpolicy'); $html = '

'.$passpolicy->explainPolicy().'

'; $event->data->insertElement(++$pos, $html); + } /** diff --git a/conf/default.php b/conf/default.php index 465d57d..a40e192 100644 --- a/conf/default.php +++ b/conf/default.php @@ -12,3 +12,9 @@ $conf['autotype'] = 'random'; $conf['autobits'] = 44; + + +$conf['oldpass'] = 5; +$conf['expire'] = 30; +$conf['expirewarn'] = 2; +$conf['date_start'] = '2014-06-18'; diff --git a/conf/metadata.php b/conf/metadata.php index ea4d347..159020e 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -12,3 +12,10 @@ $meta['autotype'] = array('multichoice', '_choices' => array('random', 'phrase', 'pronouncable')); $meta['autobits'] = array('numeric', '_min' => 24); + +$meta['oldpass'] = array('numeric', '_min' => 0); +$meta['expire'] = array('numeric', '_min' => 0); //days +$meta['expirewarn'] = array('numeric', '_min' => 0); //days before expire +$meta['date_start'] = array('string', '_pattern' => '/\20d{2}-\d{2}-\d{2}/'); + + diff --git a/helper.php b/helper.php index 067eca5..bf64b07 100644 --- a/helper.php +++ b/helper.php @@ -32,6 +32,10 @@ class helper_plugin_passpolicy extends DokuWiki_Plugin { 'numeric' => true, 'special' => false ); + + /** @var string path to pass history file */ + public $passhistory_file = null; + /** @var int number of consecutive letters that may not be in the username, 0 to disable */ public $usernamecheck = 0; @@ -54,6 +58,7 @@ class helper_plugin_passpolicy extends DokuWiki_Plugin { const LENGTH_VIOLATION = 1; const POOL_VIOLATION = 2; const USERNAME_VIOLATION = 4; + const OLDPASS_VIOLATION = 8; /** * Constructor @@ -66,7 +71,10 @@ public function __construct() { $this->usernamecheck = $this->getConf('user'); $this->autotype = $this->getConf('autotype'); $this->autobits = $this->getConf('autobits'); + $this->oldpass = $this->getConf('oldpass'); + $this->passhistory_file = DOKU_CONF .'/policy.userpasshistory.json'; + $opts = explode(',', $this->getConf('pools')); if(count($opts)) { // ignore empty pool setups $this->usepools = array(); @@ -129,13 +137,15 @@ public function explainPolicy() { $text = ''; if($this->min_length) - $text .= sprintf($this->getLang('length'), $this->min_length)."\n"; + $text .= sprintf($this->getLang('length'), $this->min_length)."
"; if($this->min_pools) - $text .= sprintf($this->getLang('pools'), $this->min_pools, join(', ', $pools))."\n"; + $text .= sprintf($this->getLang('pools'), $this->min_pools, join(', ', $pools))."
"; if($this->usernamecheck == 1) - $text .= $this->getLang('user1')."\n"; + $text .= $this->getLang('user1')."
"; if($this->usernamecheck > 1) - $text .= sprintf($this->getLang('user2'), $this->usernamecheck)."\n"; + $text .= sprintf($this->getLang('user2'), $this->usernamecheck)."
"; + if($this->oldpass > 0) + $text .= sprintf($this->getLang('oldpass'), $this->oldpass)."
"; return trim($text); } @@ -151,7 +161,7 @@ public function checkPolicy($pass, $username) { $this->error = 0; // check length first: - if(strlen($pass) < $this->min_length) { + if(utf8_strlen($pass) < $this->min_length) { $this->error = helper_plugin_passpolicy::LENGTH_VIOLATION; return false; } @@ -167,11 +177,11 @@ public function checkPolicy($pass, $username) { } if($this->usernamecheck && $username) { - $pass = utf8_strtolower($pass); - $username = utf8_strtolower($username); + $pass2 = utf8_strtolower($pass); + $username = utf8_strtolower($username); // simplest case first - if(utf8_stripspecials($pass, '', '\._\-:\*') == utf8_stripspecials($username, '', '\._\-:\*')) { + if(utf8_stripspecials($pass2, '', '\._\-:\*') == utf8_stripspecials($username, '', '\._\-:\*')) { $this->error = helper_plugin_passpolicy::USERNAME_VIOLATION; return false; } @@ -179,8 +189,8 @@ public function checkPolicy($pass, $username) { // find possible chunks in the lenght defined in policy if($this->usernamecheck > 1) { $chunks = array(); - for($i = 0; $i < utf8_strlen($pass) - $this->usernamecheck + 1; $i++) { - $chunk = utf8_substr($pass, $i, $this->usernamecheck + 1); + for($i = 0; $i < utf8_strlen($pass2) - $this->usernamecheck + 1; $i++) { + $chunk = utf8_substr($pass2, $i, $this->usernamecheck + 1); if($chunk == utf8_stripspecials($chunk, '', '\._\-:\*')) { $chunks[] = $chunk; // only word chars are checked } @@ -196,6 +206,20 @@ public function checkPolicy($pass, $username) { } } } + + //dbg($pass); + if($this->oldpass > 0 && + $passHistory = $this->getUserHistory($username) + ) { + $oldPasswords = array_slice($passHistory['pass'], 0, $this->getConf('oldpass')); + foreach($oldPasswords as $oldPassword) { + if(auth_verifyPassword($pass, $oldPassword)) { + $this->error = helper_plugin_passpolicy::OLDPASS_VIOLATION; + return false; + } + } + + } return true; } @@ -455,6 +479,133 @@ protected function loadwordlist() { $this->wordlist += file(dirname(__FILE__).'/words.txt', FILE_IGNORE_NEW_LINES); $this->wordlistlength = count($this->wordlist); } + + + /** + * password is expired + * + * @param string $user + * @return boolean|timestamp timestamp when password is expired with expireing day + */ + public function checkPasswordExpired($user=false) { + $dateStart = strtotime($this->getConf('date_start')); + + $datePassExpire = $this->getDatePassExpire($user); + + if($datePassExpire && + $datePassExpire >= $dateStart && + $datePassExpire < time() + ) { + return $datePassExpire; + } + + return false; + } + + /** + * warn user about an expireing password + * + * @param string $user + * @return boolean|timestamp false or timestamp when the password will expire. + */ + public function checkPasswordExpireWarn($user=false) { + $timespanWarn = intval($this->getConf('expirewarn')); + if(!$timespanWarn) return false; + + $dateStart = strtotime($this->getConf('date_start')); + + $datePassExpire = $this->getDatePassExpire($user); + + if($datePassExpire && + max(array($datePassExpire,$dateStart)) < (time() + $timespanWarn*3600*24) + ) { + return max(array($datePassExpire,$dateStart)); + } + return false; + + } + + /** + * returns the date when to actual password will expire, wont respect dateStart to extend the time + * + * @param string $user + * @return false or timestamp + */ + protected function getDatePassExpire($user = false) { + $userHistoryPolicy = $this->getUserPassHistory($user); + $dateStart = strtotime($this->getConf('date_start')); + $passChanged = false; + + if($userHistoryPolicy) { //user has already changed password since plugin installation + $passChanged = $userHistoryPolicy['date']; + $expire_interval = $this->getConf('expire') * 3600*24; + if($expire_interval) {//next password change will be then + $expireDate = $passChanged + $expire_interval; + } else {//no need to change password + $expireDate = false; + } + } else { //user has not changed password since plugin installation, password will expire at start date + $expireDate = $dateStart; + } + + return $expireDate; + } + + /** + * returns the passHistory entry for a given user + * + * @param string $user + * @return NULL|boolean|array assotiative array 'date','pass'[] + */ + protected function getUserPassHistory($user=false) { + if(!$user && $_SERVER['REMOTE_USER']) $user = $_SERVER['REMOTE_USER']; + + $jsonData = io_readFile($this->passhistory_file); + if(!$jsonData) { + //msg('cannot read file'); + return null; + } + $userPassHistory = json_decode($jsonData,true); + + if(isset($userPassHistory[$user])) { + return $userPassHistory[$user]; + } else { + return false; + } + + } + + /** + * saves the given password to the passHistory file + * + * @param string $user + * @param string $pass + */ + public function savePassword2PassHistory($user,$pass) { + $jsonData = io_readFile($this->passhistory_file); + if(!$jsonData) { + $userPassHistory = array(); + } else { + $userPassHistory = json_decode($jsonData,true); + } + + if(isset($userPassHistory[$user])) { + array_unshift($userPassHistory[$user]['pass'], auth_cryptPassword($pass)); + $userPassHistory[$user]['pass'] = array_slice($userPassHistory[$user]['pass'], 0, $this->getConf('oldpass')); + + } else { + $userPassHistory[$user]['pass'] = array(auth_cryptPassword($pass)); + } + + $userPassHistory[$user]['date'] = time(); + + io_saveFile($this->passhistory_file, json_encode($userPassHistory)); + + } + + + + } /** diff --git a/lang/en/lang.php b/lang/en/lang.php index 783c8c4..47aa285 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -10,10 +10,14 @@ $lang['pools'] = 'Your password needs to use characters from at least %d of the following types: %s.'; $lang['user1'] = 'Your password may not contain your username.'; $lang['user2'] = 'Your password may only use %d or less consecutive characters that appear in your username.'; +$lang['oldpass'] = 'Your password may not be equal to the last %d password(s).'; $lang['js']['strength0'] = 'very weak'; $lang['js']['strength1'] = 'weak'; $lang['js']['strength2'] = 'decent'; $lang['js']['strength3'] = 'strong'; +$lang['expirewarn'] = 'Your password is going to be expired at %s. You can change your password here: %s. Or hide it for today'; +$lang['expired'] = 'Your password is expired since %s, you have to change your password! If you forgot your password, logout/login and use the reset password link.'; + //Setup VIM: ex: et ts=4 : diff --git a/lang/en/settings.php b/lang/en/settings.php index 2d88c0e..a306198 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -22,4 +22,9 @@ $lang['pools_numeric'] = 'numbers'; $lang['pools_special'] = 'special chars (eg. !, $, #, %)'; +$lang['oldpass'] = 'Number of old passwords, which will be checked. 0 to disable checking old passwords.'; +$lang['expire'] = 'Number in days when password will expire. 0 to disable expireing passwords'; +$lang['expirewarn'] = 'Number in days before user should be informed about an expireing password.'; +$lang['date_start'] = 'Date when password need to be changed for the first time. Format YYYY-MM-DD.'; + //Setup VIM: ex: et ts=4 : diff --git a/script.js b/script.js index 8890c8b..d560389 100644 --- a/script.js +++ b/script.js @@ -51,11 +51,14 @@ jQuery(function () { function checkpolicy($field,indicator) { var pass = $field.val(); + var user = $field.closest('form').find('input[name="userid"]').val(); + jQuery.post( DOKU_BASE+'lib/exe/ajax.php', { call:'plugin_passpolicy', - pass:pass + pass:pass, + user:user }, function(response){ if(response === '1') { @@ -96,7 +99,7 @@ jQuery(function () { */ $passfield.each(function(){ var $field = jQuery(this); - + var indicator = document.createElement('p'); indicator.className = 'passpolicy__indicator'; @@ -106,5 +109,11 @@ jQuery(function () { }); +}); -}); \ No newline at end of file +jQuery(function(){ + jQuery('#passpolicy_msg_hide').click(function(){ + jQuery(this).closest('div').hide(200); + jQuery.cookie('passpolicy_msg_hide','hide',{expires:1}); + }); +}); From 0fafd30b0245781d1630e506c97ad7c3959d3903 Mon Sep 17 00:00:00 2001 From: lisps Date: Wed, 18 Jun 2014 09:29:44 +0200 Subject: [PATCH 2/8] store passhistory in one file per user do not user json anymore get last password change date by filemtime from passhistory file rewrite conf language file --- conf/default.php | 6 ++-- helper.php | 77 +++++++++++++++++++++----------------------- lang/en/settings.php | 8 ++--- script.js | 1 + 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/conf/default.php b/conf/default.php index a40e192..e8889da 100644 --- a/conf/default.php +++ b/conf/default.php @@ -14,7 +14,7 @@ $conf['autobits'] = 44; -$conf['oldpass'] = 5; -$conf['expire'] = 30; +$conf['oldpass'] = 0; +$conf['expire'] = 90; $conf['expirewarn'] = 2; -$conf['date_start'] = '2014-06-18'; +$conf['date_start'] = '2030-01-01'; diff --git a/helper.php b/helper.php index bf64b07..4dda806 100644 --- a/helper.php +++ b/helper.php @@ -33,8 +33,8 @@ class helper_plugin_passpolicy extends DokuWiki_Plugin { 'special' => false ); - /** @var string path to pass history file */ - public $passhistory_file = null; + /** @var string path to pass history dir */ + public $passhistorydir = null; /** @var int number of consecutive letters that may not be in the username, 0 to disable */ @@ -73,7 +73,7 @@ public function __construct() { $this->autobits = $this->getConf('autobits'); $this->oldpass = $this->getConf('oldpass'); - $this->passhistory_file = DOKU_CONF .'/policy.userpasshistory.json'; + $this->passhistorydir = DOKU_DATA .'/passhistory/'; $opts = explode(',', $this->getConf('pools')); if(count($opts)) { // ignore empty pool setups @@ -209,9 +209,8 @@ public function checkPolicy($pass, $username) { //dbg($pass); if($this->oldpass > 0 && - $passHistory = $this->getUserHistory($username) + $oldPasswords = $this->getUserPassHistory($username) ) { - $oldPasswords = array_slice($passHistory['pass'], 0, $this->getConf('oldpass')); foreach($oldPasswords as $oldPassword) { if(auth_verifyPassword($pass, $oldPassword)) { $this->error = helper_plugin_passpolicy::OLDPASS_VIOLATION; @@ -482,7 +481,7 @@ protected function loadwordlist() { /** - * password is expired + * check if password is expired * * @param string $user * @return boolean|timestamp timestamp when password is expired with expireing day @@ -503,7 +502,7 @@ public function checkPasswordExpired($user=false) { } /** - * warn user about an expireing password + * check if we have to warn the user about an expireing password * * @param string $user * @return boolean|timestamp false or timestamp when the password will expire. @@ -526,18 +525,16 @@ public function checkPasswordExpireWarn($user=false) { } /** - * returns the date when to actual password will expire, wont respect dateStart to extend the time + * returns the date when the current password will expire, wont respect dateStart to extend the time * * @param string $user * @return false or timestamp */ protected function getDatePassExpire($user = false) { - $userHistoryPolicy = $this->getUserPassHistory($user); + $passChanged = $this->getPassHistoryChangeDate($user); $dateStart = strtotime($this->getConf('date_start')); - $passChanged = false; - - if($userHistoryPolicy) { //user has already changed password since plugin installation - $passChanged = $userHistoryPolicy['date']; + + if($passChanged) { //user has already changed password since plugin installation $expire_interval = $this->getConf('expire') * 3600*24; if($expire_interval) {//next password change will be then $expireDate = $passChanged + $expire_interval; @@ -560,21 +557,31 @@ protected function getDatePassExpire($user = false) { protected function getUserPassHistory($user=false) { if(!$user && $_SERVER['REMOTE_USER']) $user = $_SERVER['REMOTE_USER']; - $jsonData = io_readFile($this->passhistory_file); - if(!$jsonData) { - //msg('cannot read file'); - return null; + $passhistory = io_readFile($this->passhistorydir . $user.'.txt'); + if(!$passhistory) { + return false; } - $userPassHistory = json_decode($jsonData,true); + $passhistory = explode("\n", $passhistory); - if(isset($userPassHistory[$user])) { - return $userPassHistory[$user]; + if(is_array($passhistory)) { + $passhistory = array_slice($passhistory, 0, $this->getConf('oldpass')); + return $passhistory; } else { return false; } } + /** + * returns the modification date of the passhistory file, which is the date when user changed password + * @param string $user + */ + protected function getPassHistoryChangeDate($user=false) { + if(!$user && $_SERVER['REMOTE_USER']) $user = $_SERVER['REMOTE_USER']; + + return @filemtime($this->passhistorydir . $user .'.txt'); + } + /** * saves the given password to the passHistory file * @@ -582,30 +589,18 @@ protected function getUserPassHistory($user=false) { * @param string $pass */ public function savePassword2PassHistory($user,$pass) { - $jsonData = io_readFile($this->passhistory_file); - if(!$jsonData) { - $userPassHistory = array(); - } else { - $userPassHistory = json_decode($jsonData,true); - } - - if(isset($userPassHistory[$user])) { - array_unshift($userPassHistory[$user]['pass'], auth_cryptPassword($pass)); - $userPassHistory[$user]['pass'] = array_slice($userPassHistory[$user]['pass'], 0, $this->getConf('oldpass')); - + $passhistory = $this->getUserPassHistory($user); + if(!$passhistory) { + $passhistory = auth_cryptPassword($pass); } else { - $userPassHistory[$user]['pass'] = array(auth_cryptPassword($pass)); + array_unshift($passhistory, auth_cryptPassword($pass)); + $passhistory = array_slice($passhistory, 0, $this->getConf('oldpass')); + $passhistory = implode("\n", $passhistory); } - - $userPassHistory[$user]['date'] = time(); - - io_saveFile($this->passhistory_file, json_encode($userPassHistory)); - + + io_saveFile($this->passhistorydir .$user.'.txt', $passhistory); } - - - - + } /** diff --git a/lang/en/settings.php b/lang/en/settings.php index a306198..9d4d19a 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -22,9 +22,9 @@ $lang['pools_numeric'] = 'numbers'; $lang['pools_special'] = 'special chars (eg. !, $, #, %)'; -$lang['oldpass'] = 'Number of old passwords, which will be checked. 0 to disable checking old passwords.'; -$lang['expire'] = 'Number in days when password will expire. 0 to disable expireing passwords'; -$lang['expirewarn'] = 'Number in days before user should be informed about an expireing password.'; -$lang['date_start'] = 'Date when password need to be changed for the first time. Format YYYY-MM-DD.'; +$lang['oldpass'] = 'Number of old passwords, which will be checked. 0 to disable checking of old passwords.'; +$lang['expire'] = 'Number in days of password interval. 0 to disable expiring passwords'; +$lang['expirewarn'] = 'Number in days the user will be informed before the password expires.'; +$lang['date_start'] = 'Set the beginning day when passwords will first expire after plugin installation, so that user are forced to change their password according to the passpolicy. This date is used to give the user a transition time to change their password after plugin installation. Setting the date to far future will disable expiring passwords. (Format YYYY-MM-DD)'; //Setup VIM: ex: et ts=4 : diff --git a/script.js b/script.js index d560389..6a6ec66 100644 --- a/script.js +++ b/script.js @@ -109,6 +109,7 @@ jQuery(function () { }); + }); jQuery(function(){ From 1bb9e345bd6ecc165caa90c0c26a8a6b1ebcaef6 Mon Sep 17 00:00:00 2001 From: lisps Date: Tue, 1 Jul 2014 08:58:01 +0200 Subject: [PATCH 3/8] format date with dformat --- action.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.php b/action.php index 794571a..71e3838 100644 --- a/action.php +++ b/action.php @@ -50,11 +50,11 @@ function check_act(Doku_Event &$event,$param) { $passpolicy = $this->loadHelper('passpolicy'); if($expireDate = $passpolicy->checkPasswordExpired()) { //password is expired - msg(sprintf($this->getLang('expired'), date('Y-m-d',$expireDate))); + msg(sprintf($this->getLang('expired'), dformat($expireDate))); $event->data = 'profile'; } else if($expireDate = $passpolicy->checkPasswordExpireWarn()) { //show warn message if(!isset($_COOKIE['passpolicy_msg_hide'])) { - msg(sprintf($this->getLang('expirewarn'), date('Y-m-d',$expireDate),tpl_action('profile',1,false,true))); + msg(sprintf($this->getLang('expirewarn'), dformat($expireDate),tpl_action('profile',1,false,true))); } } From 33cd76895b24ac9014fab0fa6edc6909b23c44c7 Mon Sep 17 00:00:00 2001 From: lisps Date: Tue, 1 Jul 2014 08:58:09 +0200 Subject: [PATCH 4/8] fix date validation in config --- conf/metadata.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/metadata.php b/conf/metadata.php index 159020e..83775a0 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -16,6 +16,6 @@ $meta['oldpass'] = array('numeric', '_min' => 0); $meta['expire'] = array('numeric', '_min' => 0); //days $meta['expirewarn'] = array('numeric', '_min' => 0); //days before expire -$meta['date_start'] = array('string', '_pattern' => '/\20d{2}-\d{2}-\d{2}/'); +$meta['date_start'] = array('string', '_pattern' => '/20\d{2}-\d{2}-\d{2}/'); From 69ef21335d35d8a782a5f717ce6a50943a14faf5 Mon Sep 17 00:00:00 2001 From: lisps Date: Tue, 1 Jul 2014 09:00:04 +0200 Subject: [PATCH 5/8] move passhistory folder to metadir/_passhistory --- helper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helper.php b/helper.php index 4dda806..9785290 100644 --- a/helper.php +++ b/helper.php @@ -66,6 +66,8 @@ class helper_plugin_passpolicy extends DokuWiki_Plugin { * Sets the policy from the DokuWiki config */ public function __construct() { + global $conf; + $this->min_length = $this->getConf('minlen'); $this->min_pools = $this->getConf('minpools'); $this->usernamecheck = $this->getConf('user'); @@ -73,7 +75,7 @@ public function __construct() { $this->autobits = $this->getConf('autobits'); $this->oldpass = $this->getConf('oldpass'); - $this->passhistorydir = DOKU_DATA .'/passhistory/'; + $this->passhistorydir = $conf['metadir'].'/_passhistory/'; $opts = explode(',', $this->getConf('pools')); if(count($opts)) { // ignore empty pool setups From 50aba2d4d76590ca9297a0f811ed229ef88fff1b Mon Sep 17 00:00:00 2001 From: lisps Date: Tue, 1 Jul 2014 09:11:51 +0200 Subject: [PATCH 6/8] encode username with utf8_encodeFN() --- helper.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helper.php b/helper.php index 9785290..c3a85be 100644 --- a/helper.php +++ b/helper.php @@ -559,7 +559,7 @@ protected function getDatePassExpire($user = false) { protected function getUserPassHistory($user=false) { if(!$user && $_SERVER['REMOTE_USER']) $user = $_SERVER['REMOTE_USER']; - $passhistory = io_readFile($this->passhistorydir . $user.'.txt'); + $passhistory = io_readFile($this->passhistorydir . utf8_encodeFN($user).'.txt'); if(!$passhistory) { return false; } @@ -581,7 +581,7 @@ protected function getUserPassHistory($user=false) { protected function getPassHistoryChangeDate($user=false) { if(!$user && $_SERVER['REMOTE_USER']) $user = $_SERVER['REMOTE_USER']; - return @filemtime($this->passhistorydir . $user .'.txt'); + return @filemtime($this->passhistorydir . utf8_encodeFN($user) .'.txt'); } /** @@ -600,7 +600,7 @@ public function savePassword2PassHistory($user,$pass) { $passhistory = implode("\n", $passhistory); } - io_saveFile($this->passhistorydir .$user.'.txt', $passhistory); + io_saveFile($this->passhistorydir .utf8_encodeFN($user).'.txt', $passhistory); } } From c5f6253947075605b5f89dbe338b13a15d69457f Mon Sep 17 00:00:00 2001 From: lisps Date: Mon, 7 Jul 2014 14:13:45 +0200 Subject: [PATCH 7/8] add tests --- _test/helper.test.php | 134 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/_test/helper.test.php b/_test/helper.test.php index 1dfa807..db142e0 100644 --- a/_test/helper.test.php +++ b/_test/helper.test.php @@ -7,7 +7,24 @@ class helper_plugin_passpolicy_test extends DokuWikiTest { protected $pluginsEnabled = array('passpolicy'); - public function newPolicy($minl, $minp, $lower, $upper, $num, $special, $ucheck, $pron=true) { + /** + * + * @param int $minl + * @param int $minp + * @param boolean $lower + * @param boolean $upper + * @param boolean $num + * @param boolean $special + * @param boolean $ucheck + * @param boolean $pron + * @param int $oldpass + * @param int $expire_days + * @param int $expirewarn_days + * @param string $data_start date YYYY-MM-DD + * @return helper_plugin_passpolicy + */ + public function newPolicy($minl, $minp, $lower, $upper, $num, $special, $ucheck, $pron=true,$oldpass=0,$date_start='2030-01-01',$expire_days=0,$expirewarn_days=2) { + /* @var $policy helper_plugin_passpolicy */ $policy = plugin_load('helper', 'passpolicy'); $policy->min_pools = $minp; $policy->min_length = $minl; @@ -18,10 +35,25 @@ public function newPolicy($minl, $minp, $lower, $upper, $num, $special, $ucheck, 'special' => $special ); $policy->usernamecheck = $ucheck; - $policy->pronouncable = $pron; + $policy->pronouncable = $pron; + + $policy->oldpass = $oldpass; + $policy->conf['oldpass'] = $oldpass; + $policy->conf['expire'] = $expire_days; + $policy->conf['expirewarn'] = $expirewarn_days; + $policy->conf['date_start'] = $date_start; return $policy; } + + public function changePass($pass,$user = 'testuser') { + global $auth; + + return $auth->triggerUserMod('modify',array( + $user, + array('pass'=>$pass) + )); + } public function test_policies() { $policy = $this->newPolicy(6, 1, true, true, true, true, 0); @@ -173,5 +205,103 @@ public function test_selfcheck() { //echo "\n$pw1\n$pw2\n"; } + + public function test_passhistory() { + + $policy = $this->newPolicy(6, 4, true, true, true, true, 0, true,2); + $pw1 = $policy->generatePassword('testuser'); + $pw2 = $policy->generatePassword('testuser'); + $pw3 = $policy->generatePassword('testuser'); + + $this->assertTrue($this->changePass($pw1),'cannot change password'); + $this->assertNull($this->changePass($pw1),'last password can be used'); + $this->assertTrue($this->changePass($pw2),'cannot change password'); + $this->assertNull($this->changePass($pw1),'second last password can be used'); + $this->assertTrue($this->changePass($pw3),'cannot change password'); + $this->assertTrue($this->changePass($pw1),'third last password can be used'); + $this->assertNull($this->changePass($pw3),'second last password can be used'); + + //$policy = $this->newPolicy(18, 1, true, false, false, false, 0, false,0,0,2,date('Y-m-d',time()+3600*2)); + + } + + public function test_pass_expire() { + $policy = $this->newPolicy(6, 4, true, true, true, true, 0, true, + 0, //oldpass + '2010-01-01', //$date_start + 2,//expire_days=0 + 0//expirewarn_days + ); + + $userFile = $policy->passhistorydir .'testuser.txt'; + + TestUtils::rdelete($userFile); + $this->assertEquals(strtotime($policy->getConf('date_start')),$policy->checkPasswordExpired('testuser')); + $this->assertFalse($policy->checkPasswordExpireWarn('testuser')); + + touch($userFile,time()); + $this->assertFalse($policy->checkPasswordExpired('testuser')); + $this->assertFalse($policy->checkPasswordExpireWarn('testuser')); + TestUtils::rdelete($userFile); + + $changedTime = time() - 3600*48-1; + touch($userFile,$changedTime); + $this->assertEquals($changedTime+3600*48,$policy->checkPasswordExpired('testuser'),'password is not expired'); + $this->assertFalse($policy->checkPasswordExpireWarn('testuser')); + + } + + public function test_pass_expire_warn() { + $policy = $this->newPolicy(6, 4, true, true, true, true, 0, true, + 0, //oldpass + '2010-01-01', //$date_start + 14,//expire_days=0 + 2//expirewarn_days + ); + + $userFile = $policy->passhistorydir .'testuser.txt'; + + TestUtils::rdelete($userFile); + $this->assertEquals(strtotime($policy->getConf('date_start')),$policy->checkPasswordExpireWarn('testuser')); + + touch($userFile,time()); + $this->assertFalse($policy->checkPasswordExpired('testuser')); + $this->assertFalse($policy->checkPasswordExpireWarn('testuser')); + TestUtils::rdelete($userFile); + + $changedTime = time() - 3600*24*12-1; + touch($userFile,$changedTime); + $this->assertFalse($policy->checkPasswordExpired('testuser')); + $this->assertEquals($changedTime+3600*24*14,$policy->checkPasswordExpireWarn('testuser'),'we have to warn!'); + + } + + public function test_pass_date_start() { + $policy = $this->newPolicy(6, 4, true, true, true, true, 0, true, + 0, //oldpass + date('Y-m-d',time()+3600*24*5), //$date_start + 2,//expire_days=0 + 6//expirewarn_days + ); + + $userFile = $policy->passhistorydir .'testuser.txt'; + TestUtils::rdelete($userFile); + + $this->assertEquals(strtotime($policy->getConf('date_start')),$policy->checkPasswordExpireWarn('testuser'),'we have to warn!'); + $this->assertFalse($policy->checkPasswordExpired('testuser'),'date start is in future'); + + $policy = $this->newPolicy(6, 4, true, true, true, true, 0, true, + 0, //oldpass + date('Y-m-d',time()+3600*24*5), //$date_start + 2,//expire_days=0 + 4//expirewarn_days + ); + + $userFile = $policy->passhistorydir .'testuser.txt'; + + $this->assertFalse($policy->checkPasswordExpireWarn('testuser'),'its not time to warn'); + $this->assertFalse($policy->checkPasswordExpired('testuser'),'date start is in future'); + + } } From 12d0eb1ec937b7f930f4b894a95be5e004db3a31 Mon Sep 17 00:00:00 2001 From: lisps Date: Wed, 9 Jul 2014 14:51:41 +0200 Subject: [PATCH 8/8] allow action admin when password is expired --- action.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.php b/action.php index 71e3838..3c16b84 100644 --- a/action.php +++ b/action.php @@ -44,7 +44,7 @@ public function register(Doku_Event_Handler &$controller) { function check_act(Doku_Event &$event,$param) { if(!$_SERVER['REMOTE_USER']) return; - if(in_array($event->data,array('login','logout','profile'))) return; + if(in_array($event->data,array('login','logout','profile','admin'))) return; /* @var $passpolicy helper_plugin_passpolicy */ $passpolicy = $this->loadHelper('passpolicy');