From 57845b7f3261d2146f08459ef90c27ce82269f77 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Wed, 18 Mar 2015 14:24:56 -0500
Subject: [PATCH] auth: Add concept of bk passwd update and policy

This adds the concept of a PasswordPolicy registration system, which
provides an extensible way of administering and configuring password
complexity policies.

It also adds a setPassword() method for the authentication backends which
will allow for the respective backend up update the password according to
whatever method is suitable for the respective backend (such as remote
updates for LDAP).
---
 include/class.auth.php  | 92 +++++++++++++++++++++++++++++++++++++++++
 include/class.staff.php | 62 ++++++++++++++++++++-------
 2 files changed, 140 insertions(+), 14 deletions(-)

diff --git a/include/class.auth.php b/include/class.auth.php
index 192c3af70..0f13622e3 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 44f7d6302..0a9769453 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);
@@ -492,8 +526,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');
 
@@ -511,13 +543,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;
@@ -539,15 +582,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();
     }
 
-- 
GitLab