diff --git a/include/class.auth.php b/include/class.auth.php index 192c3af7079265b5d99527390ca497db9bd45fe2..0f13622e39661c46d166f09ec18f32f21576d62c 100644 --- a/include/class.auth.php +++ b/include/class.auth.php @@ -347,6 +347,38 @@ abstract class AuthenticationBackend { return false; } + /** + * Request the backend to update the password for a user. This method is + * the main entry for password updates so that password policies can be + * applied to the new password before passing the new password to the + * backend for updating. + * + * Throws: + * BadPassword — if password does not meet policy requirement + * PasswordUpdateFailed — if backend failed to update the password + */ + function setPassword($user, $password, $current=false) { + PasswordPolicy::checkPassword($password, $current); + $rv = $this->syncPassword($user, $password); + if ($rv) { + $info = array('password' => $password, 'current' => $current); + Signal::send('auth.pwchange', $user, $info); + } + return $rv; + } + + /** + * Request the backend to update the user's password with the password + * given. This method should only be used if the backend advertises + * supported password updates with the supportsPasswordChange() method. + * + * Returns: + * true if the password was successfully updated and false otherwise. + */ + protected function syncPassword($user, $password) { + return false; + } + function supportsPasswordReset() { return false; } @@ -946,6 +978,14 @@ class osTicketAuthentication extends StaffAuthenticationBackend { } } + function supportsPasswordChange() { + return true; + } + + function syncPassword($staff, $password) { + $staff->passwd = Passwd::hash($password); + } + } StaffAuthenticationBackend::register('osTicketAuthentication'); @@ -1228,4 +1268,56 @@ class ClientAcctConfirmationTokenBackend extends UserAuthenticationBackend { } } UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend'); + +// ----- Password Policy -------------------------------------- + +class BadPassword extends Exception {} +class PasswordUpdateFailed extends Exception {} + +abstract class PasswordPolicy { + static protected $registry = array(); + + /** + * Check a password and throw BadPassword with a meaningful message if + * the password cannot be accepted. + */ + abstract function processPassword($new, $current); + + static function checkPassword($new, $current) { + foreach (static::allActivePolicies() as $P) { + $P->processPassword($new, $current); + } + } + + static function allActivePolicies() { + $policies = array(); + foreach (static::$registry as $P) { + if (is_string($P) && class_exists($P)) + $P = new $P(); + if ($P instanceof PasswordPolicy) + $policies[] = $P; + } + return $policies; + } + + static function register($policy) { + static::$registry[] = $policy; + } +} + +class osTicketPasswordPolicy +extends PasswordPolicy { + function processPassword($passwd, $current) { + if (strlen($passwd) < 6) { + throw new BadPassword( + __('Password must be at least 6 characters')); + } + // XXX: Changing case is technicall changing the password + if (0 === strcasecmp($passwd, $current)) { + throw new BadPassword( + __('New password MUST be different from the current password!')); + } + } +} +PasswordPolicy::register('osTicketPasswordPolicy'); ?> diff --git a/include/class.staff.php b/include/class.staff.php index cc75aa0030c88a307d12df300456190a7d44ec0c..2654387c9896a379fb58afd50e2e1d2c838dd490 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -96,7 +96,13 @@ implements AuthenticatedUser, EmailContact { } function getAuthBackend() { - list($authkey, ) = explode(':', $this->getAuthKey()); + list($bk, ) = explode(':', $this->getAuthKey()); + + // If administering a user other than yourself, fallback to the + // agent's declared backend, if any + if (!$bk && $this->backend) + $bk = $this->backend; + return StaffAuthenticationBackend::getBackend($authkey); } @@ -157,6 +163,34 @@ implements AuthenticatedUser, EmailContact { && $this->passwd_change>($cfg->getPasswdResetPeriod()*30*24*60*60)); } + function setPassword($new, $current=false) { + // Allow the backend to update the password. This is the preferred + // method as it allows for integration with password policies and + // also allows for remotely updating the password where possible and + // supported. + if (!($bk = $this->getAuthBackend()) + || !$bk instanceof AuthBackend + ) { + // Fallback to osTicket authentication token udpates + $bk = new osTicketAuthentication(); + } + + // And now for the magic + if (!$bk->supportsPasswordChange()) { + throw new PasswordUpdateFailed( + __('Authentication backend does not support password updates')); + } + if (!$bk->setPassword($this, $new, $current)) { + // Backend should throw PasswordUpdateFailed directly + return false; + } + + // Successfully updated authentication tokens + $this->change_passwd = 0; + $this->cancelResetTokens(); + $this->passwdreset = SqlFunction::NOW(); + } + function canAccess($something) { if ($something instanceof RestrictedAccess) return $something->checkStaffPerm($this); @@ -493,8 +527,6 @@ implements AuthenticatedUser, EmailContact { if(!$vars['passwd1']) $errors['passwd1']=__('New password is required'); - elseif($vars['passwd1'] && strlen($vars['passwd1'])<6) - $errors['passwd1']=__('Password must be at least 6 characters'); elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) $errors['passwd2']=__('Passwords do not match'); @@ -512,13 +544,24 @@ implements AuthenticatedUser, EmailContact { $errors['cpasswd']=__('Current password is required'); elseif(!$this->cmp_passwd($vars['cpasswd'])) $errors['cpasswd']=__('Invalid current password!'); - elseif(!strcasecmp($vars['passwd1'], $vars['cpasswd'])) - $errors['passwd1']=__('New password MUST be different from the current password!'); } if($vars['default_signature_type']=='mine' && !$vars['signature']) $errors['default_signature_type'] = __("You don't have a signature"); + // Update the user's password if requested + if ($vars['passwd1']) { + try { + $this->setPassword($vars['passwd1'], $vars['cpasswd']); + } + catch (BadPassword $ex) { + $errors['passwd1'] = $ex->getMessage(); + } + catch (PasswordUpdateFailed $ex) { + // TODO: Add a warning banner or crash the update + } + } + if($errors) return false; $_SESSION['staff:lang'] = null; @@ -540,15 +583,6 @@ implements AuthenticatedUser, EmailContact { $this->default_paper_size = $vars['default_paper_size']; $this->lang = $vars['lang']; - if ($vars['passwd1']) { - $this->change_passwd = 0; - $this->passwdreset = SqlFunction::NOW(); - $this->passwd = Passwd::hash($vars['passwd1']); - $info = array('password' => $vars['passwd1']); - Signal::send('auth.pwchange', $this, $info); - $this->cancelResetTokens(); - } - return $this->save(); }