From cbbc9652e545c81dd3df21bee47fbaf2b65b167c Mon Sep 17 00:00:00 2001
From: Damien <damien.burel@maarch.org>
Date: Thu, 5 Jul 2018 14:23:18 +0200
Subject: [PATCH] FEAT #7659 Password history

---
 sql/data_fr.sql                             |  2 +-
 sql/develop.sql                             | 13 +++-
 src/app/user/controllers/UserController.php |  9 ++-
 src/core/controllers/PasswordController.php | 32 ++++++++++
 src/core/lang/lang-en.php                   |  1 +
 src/core/lang/lang-fr.php                   |  1 +
 src/core/models/PasswordModel.php           | 68 +++++++++++++++------
 7 files changed, 102 insertions(+), 24 deletions(-)

diff --git a/sql/data_fr.sql b/sql/data_fr.sql
index 8610e21c344..219bece1123 100755
--- a/sql/data_fr.sql
+++ b/sql/data_fr.sql
@@ -1756,7 +1756,7 @@ INSERT INTO password_rules (label, "value") VALUES ('complexityNumber', 0);
 INSERT INTO password_rules (label, "value") VALUES ('complexitySpecial', 0);
 INSERT INTO password_rules (label, "value") VALUES ('lockAttempts', 3);
 INSERT INTO password_rules (label, "value") VALUES ('lockTime', 5);
-INSERT INTO password_rules (label, "value") VALUES ('UseNumber', 2);
+INSERT INTO password_rules (label, "value") VALUES ('useNumber', 2);
 INSERT INTO password_rules (label, "value") VALUES ('renewal', 90);
 
 
diff --git a/sql/develop.sql b/sql/develop.sql
index 164f9721a84..3c5e3cd6c70 100644
--- a/sql/develop.sql
+++ b/sql/develop.sql
@@ -77,19 +77,28 @@ CREATE TABLE password_rules
 (
   id serial,
   label character varying(64) NOT NULL,
-  "value" integer NOT NULL,
+  "value" INTEGER NOT NULL,
   enabled boolean DEFAULT FALSE,
   CONSTRAINT password_rules_pkey PRIMARY KEY (id),
   CONSTRAINT password_rules_label_key UNIQUE (label)
 )
 WITH (OIDS=FALSE);
+DROP TABLE IF EXISTS password_history;
+CREATE TABLE password_history
+(
+  id serial,
+  user_serial_id INTEGER NOT NULL,
+  password character varying(255) NOT NULL,
+  CONSTRAINT password_history_pkey PRIMARY KEY (id)
+)
+WITH (OIDS=FALSE);
 INSERT INTO password_rules (label, "value") VALUES ('minLength', 6);
 INSERT INTO password_rules (label, "value") VALUES ('complexityUpper', 0);
 INSERT INTO password_rules (label, "value") VALUES ('complexityNumber', 0);
 INSERT INTO password_rules (label, "value") VALUES ('complexitySpecial', 0);
 INSERT INTO password_rules (label, "value") VALUES ('lockAttempts', 3);
 INSERT INTO password_rules (label, "value") VALUES ('lockTime', 5);
-INSERT INTO password_rules (label, "value") VALUES ('UseNumber', 2);
+INSERT INTO password_rules (label, "value") VALUES ('useNumber', 2);
 INSERT INTO password_rules (label, "value") VALUES ('renewal', 90);
 ALTER TABLE users DROP COLUMN IF EXISTS password_modification_date;
 ALTER TABLE users ADD COLUMN password_modification_date timestamp without time zone;
diff --git a/src/app/user/controllers/UserController.php b/src/app/user/controllers/UserController.php
index 3d15a761505..3ff3e98f4ed 100644
--- a/src/app/user/controllers/UserController.php
+++ b/src/app/user/controllers/UserController.php
@@ -31,6 +31,7 @@ use Resource\models\ResModel;
 use Respect\Validation\Validator;
 use Slim\Http\Request;
 use Slim\Http\Response;
+use SrcCore\controllers\PasswordController;
 use SrcCore\models\CoreConfigModel;
 use SrcCore\models\PasswordModel;
 use SrcCore\models\SecurityModel;
@@ -303,16 +304,20 @@ class UserController
             return $response->withStatus(400)->withJson(['errors' => 'Bas request']);
         }
 
+        $user = UserModel::getByUserId(['userId' => $GLOBALS['userId'], 'select' => ['id']]);
+
         if ($data['newPassword'] != $data['reNewPassword']) {
             return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
         } elseif (!SecurityModel::authentication(['userId' => $GLOBALS['userId'], 'password' => $data['currentPassword']])) {
             return $response->withStatus(401)->withJson(['errors' => _WRONG_PSW]);
-        } elseif (PasswordModel::isPasswordValid(['password' => $data['newPassword']])) {
+        } elseif (!PasswordController::isPasswordValid(['password' => $data['newPassword']])) {
             return $response->withStatus(400)->withJson(['errors' => 'Password does not match security criteria']);
+        } elseif (!PasswordModel::isPasswordHistoryValid(['password' => $data['newPassword'], 'userSerialId' => $user['id']])) {
+            return $response->withStatus(400)->withJson(['errors' => _ALREADY_USED_PSW]);
         }
 
-        $user = UserModel::getByUserId(['userId' => $GLOBALS['userId'], 'select' => ['id']]);
         UserModel::updatePassword(['id' => $user['id'], 'password' => $data['newPassword']]);
+        PasswordModel::setHistoryPassword(['userSerialId' => $user['id'], 'password' => $data['newPassword']]);
 
         return $response->withJson(['success' => 'success']);
     }
diff --git a/src/core/controllers/PasswordController.php b/src/core/controllers/PasswordController.php
index dbe9cc5b8ff..26900b8e680 100644
--- a/src/core/controllers/PasswordController.php
+++ b/src/core/controllers/PasswordController.php
@@ -20,6 +20,7 @@ use Respect\Validation\Validator;
 use Slim\Http\Request;
 use Slim\Http\Response;
 use SrcCore\models\PasswordModel;
+use SrcCore\models\ValidatorModel;
 
 class PasswordController
 {
@@ -70,4 +71,35 @@ class PasswordController
 
         return $response->withJson(['success' => 'success']);
     }
+
+    public static function isPasswordValid(array $aArgs)
+    {
+        ValidatorModel::notEmpty($aArgs, ['password']);
+        ValidatorModel::stringType($aArgs, ['password']);
+
+        $passwordRules = PasswordModel::getEnabledRules();
+
+        if (!empty($passwordRules['minLength'])) {
+            if (strlen($aArgs['password']) < $passwordRules['minLength']) {
+                return false;
+            }
+        }
+        if (!empty($passwordRules['complexityUpper'])) {
+            if (!preg_match('/[A-Z]/', $aArgs['password'])) {
+                return false;
+            }
+        }
+        if (!empty($passwordRules['complexityNumber'])) {
+            if (!preg_match('/[0-9]/', $aArgs['password'])) {
+                return false;
+            }
+        }
+        if (!empty($passwordRules['complexitySpecial'])) {
+            if (!preg_match('/[^a-zA-Z0-9]/', $aArgs['password'])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/src/core/lang/lang-en.php b/src/core/lang/lang-en.php
index a6a23c06564..39617f9641a 100644
--- a/src/core/lang/lang-en.php
+++ b/src/core/lang/lang-en.php
@@ -122,6 +122,7 @@ define('_NUMERIC_PACKAGE', 'Numeric package');
 define('_OTHER', 'Other');
 define('_NOTIFICATION_ALREADY_EXIST', 'Notification already exists');
 define('_WRONG_PSW', 'Wrong password');
+define('_ALREADY_USED_PSW', 'The password has already been used');
 define('_MAX_SIZE_UPLOAD_REACHED', 'File maximum size is exceeded');
 define('_PATH_OF_DOCSERVER_UNAPPROACHABLE', 'Inaccessible Docserver path');
 define('_BACK_FROM_VACATION', 'back from vacation');
diff --git a/src/core/lang/lang-fr.php b/src/core/lang/lang-fr.php
index e2126dde174..6639580132a 100644
--- a/src/core/lang/lang-fr.php
+++ b/src/core/lang/lang-fr.php
@@ -122,6 +122,7 @@ define('_NUMERIC_PACKAGE', 'Pli numérique');
 define('_OTHER', 'Autre');
 define('_NOTIFICATION_ALREADY_EXIST', 'Notification déjà existante');
 define('_WRONG_PSW', 'Le mot de passe actuel n\'est pas correct');
+define('_ALREADY_USED_PSW', 'Le mot de passe a déjà été utilisé');
 define('_MAX_SIZE_UPLOAD_REACHED', 'Taille maximum de fichier dépassée');
 define('_PATH_OF_DOCSERVER_UNAPPROACHABLE', 'Chemin de la zone de stockage inaccessible');
 define('_BACK_FROM_VACATION', 'de retour de son absence');
diff --git a/src/core/models/PasswordModel.php b/src/core/models/PasswordModel.php
index 15141a3889a..d9a12715558 100644
--- a/src/core/models/PasswordModel.php
+++ b/src/core/models/PasswordModel.php
@@ -86,34 +86,64 @@ class PasswordModel
         return true;
     }
 
-    public static function isPasswordValid(array $aArgs)
+    public static function isPasswordHistoryValid(array $aArgs)
     {
-        ValidatorModel::notEmpty($aArgs, ['password']);
+        ValidatorModel::notEmpty($aArgs, ['password', 'userSerialId']);
         ValidatorModel::stringType($aArgs, ['password']);
+        ValidatorModel::intVal($aArgs, ['userSerialId']);
 
         $passwordRules = PasswordModel::getEnabledRules();
 
-        if (!empty($passwordRules['minLength'])) {
-            if (strlen($aArgs['password']) < $passwordRules['minLength']) {
-                return false;
+        if (!empty($passwordRules['useNumber'])) {
+            $passwordHistory = DatabaseModel::select([
+                'select'    => ['password'],
+                'table'     => ['password_history'],
+                'where'     => ['user_serial_id = ?'],
+                'data'      => [$aArgs['userSerialId']],
+                'order_by'  => ['id DESC'],
+                'limit'     => $passwordRules['useNumber']
+            ]);
+
+            foreach ($passwordHistory as $value) {
+                if (password_verify($aArgs['password'], $value['password'])) {
+                    return false;
+                }
             }
         }
-        if (!empty($passwordRules['complexityUpper'])) {
-            if (!preg_match('/[A-Z]/', $aArgs['password'])) {
-                return false;
-            }
-        }
-        if (!empty($passwordRules['complexityNumber'])) {
-            if (!preg_match('/[0-9]/', $aArgs['password'])) {
-                return false;
-            }
-        }
-        if (!empty($passwordRules['complexitySpecial'])) {
-            if (!preg_match('/[^a-zA-Z0-9]/', $aArgs['password'])) {
-                return false;
-            }
+
+        return true;
+    }
+
+    public static function setHistoryPassword(array $aArgs)
+    {
+        ValidatorModel::notEmpty($aArgs, ['password', 'userSerialId']);
+        ValidatorModel::stringType($aArgs, ['password']);
+        ValidatorModel::intVal($aArgs, ['userSerialId']);
+
+        $passwordHistory = DatabaseModel::select([
+            'select'    => ['id'],
+            'table'     => ['password_history'],
+            'where'     => ['user_serial_id = ?'],
+            'data'      => [$aArgs['userSerialId']],
+            'order_by'  => ['id DESC']
+        ]);
+
+        if (count($passwordHistory) >= 10) {
+            DatabaseModel::delete([
+                'table'     => 'password_history',
+                'where'     => ['id < ?', 'user_serial_id = ?'],
+                'data'      => [$passwordHistory[8], $aArgs['userSerialId']]
+            ]);
         }
 
+        DatabaseModel::insert([
+            'table'     => 'password_history',
+            'columnsValues'     => [
+                'user_serial_id'    => $aArgs['userSerialId'],
+                'password'          => SecurityModel::getPasswordHash($aArgs['password'])
+            ],
+        ]);
+
         return true;
     }
 }
-- 
GitLab