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'); + + } } diff --git a/action.php b/action.php index 71fe16e..3c16b84 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','admin'))) return; + + /* @var $passpolicy helper_plugin_passpolicy */ + $passpolicy = $this->loadHelper('passpolicy'); + + if($expireDate = $passpolicy->checkPasswordExpired()) { //password is expired + 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'), dformat($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..e8889da 100644 --- a/conf/default.php +++ b/conf/default.php @@ -12,3 +12,9 @@ $conf['autotype'] = 'random'; $conf['autobits'] = 44; + + +$conf['oldpass'] = 0; +$conf['expire'] = 90; +$conf['expirewarn'] = 2; +$conf['date_start'] = '2030-01-01'; diff --git a/conf/metadata.php b/conf/metadata.php index ea4d347..83775a0 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' => '/20\d{2}-\d{2}-\d{2}/'); + + diff --git a/helper.php b/helper.php index 067eca5..c3a85be 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 dir */ + public $passhistorydir = 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 @@ -61,12 +66,17 @@ 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'); $this->autotype = $this->getConf('autotype'); $this->autobits = $this->getConf('autobits'); + $this->oldpass = $this->getConf('oldpass'); + $this->passhistorydir = $conf['metadir'].'/_passhistory/'; + $opts = explode(',', $this->getConf('pools')); if(count($opts)) { // ignore empty pool setups $this->usepools = array(); @@ -129,13 +139,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)."