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