From bb14772abae4d01cecad8ec2fb00fdc7c1ba8f0b Mon Sep 17 00:00:00 2001 From: Mohab E Date: Fri, 8 Dec 2023 09:53:52 +0100 Subject: [PATCH] New feature #19201: Add column "active" to users, to be able to activate or deactivate users (#3550) * Feature #CT-1343: Limit creation of new users * Feature #CT-1343: Limit creation of new users * Feature #CT-1343: Limit creation of new users * Feature #CT-1343: Limit creation of new users * Feature #CT-1343: Limit creation of new users * Feature #CT-1343: Handle user status permission * Feature #CT-1343: Handle user status permission * Feature #CT-1343: Update the users count in navigation * Feature #CT-1343: Update the users count in navigation * Feature #CT-1343: DB update * Feature #CT-1343: DB update * Feature #CT-1343: remove user limit checks * Feature #CT-1343: remove user limit checks * Feature #CT-1343: Add status column * Feature #CR-1343: remove status from insertUser * Feature #CR-1343: remove user limit checks * Feature #CR-1343: rename Update_616 to Update_617 * Feature #CR-1343: rename Update_616 to Update_618 * Feature #CT-1343: Limit creation of new users * Feature #Cr-1343: Add column "active" to users, to be able to activate or deactivate users * Feature #CR-1343: Add status to Auth plugin * Feature #CR-1343: Add beforeFooterRender * Feature #CR-1343: rename status to user_status * Only merge arrays if data is an array * Show more precise error message in user management if present * Feature #CR-1343: rename status to user_status * Feature #CR-1343: remove beforeFooterRender * Block any non-active user from logging in * Don't check user_status in authdb plugin --------- Co-authored-by: Olle Haerstedt --- application/config/version.php | 2 +- .../controllers/UserManagementController.php | 144 ++++++++++++++++++ application/core/LSUserIdentity.php | 5 + .../core/plugins/AuthLDAP/AuthLDAP.php | 9 +- application/core/plugins/Authdb/Authdb.php | 11 +- .../views/massive_actions/_action_results.php | 3 + .../helpers/update/updates/Update_619.php | 16 ++ application/models/User.php | 65 +++++++- application/views/userManagement/index.php | 20 +++ .../massiveAction/_selector.php | 27 +++- .../massiveAction/_updatestatus.php | 13 ++ .../partial/confirmuseractivation.php | 18 +++ .../partial/topbarBtns/leftSideButtons.php | 22 +-- .../Sea_Green/admin_customizations.scss | 6 + .../usermanagement/js/usermanagement.js | 30 +++- installer/create-database.php | 3 +- themes/admin/Sea_Green/css/sea_green-rtl.css | 6 + .../admin/Sea_Green/css/sea_green-rtl.min.css | 2 +- themes/admin/Sea_Green/css/sea_green.css | 6 + themes/admin/Sea_Green/css/sea_green.min.css | 2 +- 20 files changed, 380 insertions(+), 30 deletions(-) create mode 100644 application/helpers/update/updates/Update_619.php create mode 100644 application/views/userManagement/massiveAction/_updatestatus.php create mode 100644 application/views/userManagement/partial/confirmuseractivation.php diff --git a/application/config/version.php b/application/config/version.php index ace7633ece1..55f37a7e222 100644 --- a/application/config/version.php +++ b/application/config/version.php @@ -12,7 +12,7 @@ */ $config['versionnumber'] = '6.4.0-dev'; -$config['dbversionnumber'] = 618; +$config['dbversionnumber'] = 619; $config['buildnumber'] = ''; $config['updatable'] = true; $config['templateapiversion'] = 3; diff --git a/application/controllers/UserManagementController.php b/application/controllers/UserManagementController.php index 7541b8a0e0d..e3290b6c860 100644 --- a/application/controllers/UserManagementController.php +++ b/application/controllers/UserManagementController.php @@ -420,6 +420,150 @@ public function actionDeleteUser() ]); } + /** + * Show user activation confirmation + * + * @return void|string + * @throws CException + */ + public function actionActivationConfirm() + { + if (!Permission::model()->hasGlobalPermission('users', 'update')) { + throw new CHttpException(403, gT("You do not have permission to access this page.")); + } + $userId = Yii::app()->request->getParam('userid'); + $action = Yii::app()->request->getParam('action'); + + $userId = sanitize_int($userId); + + $aData['userId'] = $userId; + $aData['action'] = $action; + + return $this->renderPartial('partial/confirmuseractivation', $aData); + } + + + /** + * Stores the status settings + * + * @return void|string + * @throws CException + */ + public function actionUserActivateDeactivate() + { + if (!Permission::model()->hasGlobalPermission('users', 'delete')) { + throw new CHttpException(403, gT("You do not have permission to access this page.")); + } + $userId = sanitize_int(Yii::app()->request->getParam('userid')); + $action = Yii::app()->request->getParam('action'); + $oUser = User::model()->findByPk($userId); + + if ($oUser == null) { + throw new CHttpException(404, gT("Invalid user id")); + } else { + if ($oUser->setActivationStatus($action)) { + return App()->getController()->renderPartial('/admin/super/_renderJson', [ + 'data' => [ + 'success' => true, + 'message' => gT('Status successfully updated') + ] + ]); + }; + } + return App()->getController()->renderPartial('/admin/super/_renderJson', [ + 'data' => [ + 'success' => false + ] + ]); + } + + /** + * Stores the status settings run via MassEdit + * + * @throws CException + */ + public function actionBatchStatus() + { + if (!Permission::model()->hasGlobalPermission('users', 'update')) { + throw new CHttpException(403, gT("You do not have permission to access this page.")); + } + + $userIds = json_decode(Yii::app()->request->getPost('sItems', "[]")); + $operation = Yii::app()->request->getPost('status_selector', ''); + $results = $this->userActivation($userIds, $operation); + + $error = null; + foreach ($results as $result) { + if (isset($result['error']) && $result['error'] == 'Reached the limit') { + $error = true; + } + } + + $tableLabels = array(gT('User ID'), gT('Username'), gT('Status')); + + $data = array( + 'aResults' => $results, + 'successLabel' => gT('Saved successfully'), + 'tableLabels' => $tableLabels, + ); + + if ($error) { + $data['additionalMessage'] = $this->renderPartial('/userManagement/partial/planupgradebutton'); + } + + Yii::app()->getController()->renderPartial('ext.admin.survey.ListSurveysWidget.views.massive_actions._action_results', $data); + } + + + /** + * Activate / deactivate user + * + * @param array $userIds + * @param string $operation activate or deactivate + * @return array + */ + public function userActivation($userIds, $operation) + { + $results = []; + foreach ($userIds as $iUserId) { + $oUser = User::model()->findByPk($iUserId); + if ($oUser == null) { + throw new CHttpException(404, gT("Invalid user id")); + } else { + $results[$iUserId]['title'] = $oUser->users_name; + if (!$this->isAllowedToEdit($oUser)) { + $results[$iUserId]['error'] = gT('Unauthorized'); + $results[$iUserId]['result'] = false; + continue; + } + $results[$iUserId]['result'] = $oUser->setActivationStatus($operation); + if (!$results[$iUserId]['result']) { + $results[$iUserId]['error'] = gT('Error'); + } + } + } + return $results; + } + + /** + * Check if the current user allowed to update $user + * + * @return boolean + */ + private function isAllowedToEdit($user) + { + $permission_superadmin_read = Permission::model()->hasGlobalPermission('superadmin', 'read'); + $permission_users_update = Permission::model()->hasGlobalPermission('users', 'update'); + $ownedOrCreated = $user->parent_id == App()->session['loginID']; // User is owned or created by you + + return ( $permission_superadmin_read && !(Permission::isForcedSuperAdmin($user->uid) || $user->uid == App()->user->getId())) + || (!$permission_superadmin_read && ($user->uid != App()->session['loginID'] //Can't change your own permissions + && ( $permission_users_update && $ownedOrCreated) + && !Permission::isForcedSuperAdmin($user->uid) + ) + ); + } + /** * Show user delete confirmation * diff --git a/application/core/LSUserIdentity.php b/application/core/LSUserIdentity.php index df5b2824481..7b05ba10830 100644 --- a/application/core/LSUserIdentity.php +++ b/application/core/LSUserIdentity.php @@ -67,6 +67,11 @@ public function authenticate() if (is_null($this->plugin)) { $result->setError(self::ERROR_UNKNOWN_HANDLER); } else { + // Never allow login for non-active users. + $user = User::model()->findByAttributes(array('users_name' => $this->username)); + if ($user && (int) $user->user_status === 0) { + throw new CHttpException(403, gT("You do not have permission to access this page.")); + } // Delegate actual authentication to plugin $authEvent = new PluginEvent('newUserSession', $this); // TODO: rename the plugin function authenticate() $authEvent->set('identity', $this); diff --git a/application/core/plugins/AuthLDAP/AuthLDAP.php b/application/core/plugins/AuthLDAP/AuthLDAP.php index 7872bafad99..c21abbc7b9f 100644 --- a/application/core/plugins/AuthLDAP/AuthLDAP.php +++ b/application/core/plugins/AuthLDAP/AuthLDAP.php @@ -293,7 +293,14 @@ private function _createNewUser($oEvent, $new_user, $password = null) } else { $parentID = 1; } - $iNewUID = User::insertUser($new_user, $new_pass, $new_full_name, $parentID, $new_email); + $status = true; + $preCollectedUserArray = $oEvent->get('preCollectedUserArray', []); + if (!empty($preCollectedUserArray)) { + if (!empty($preCollectedUserArray['status'])) { + $status = $preCollectedUserArray['status']; + } + } + $iNewUID = User::insertUser($new_user, $new_pass, $new_full_name, $parentID, $new_email, null, $status); if (!$iNewUID) { $oEvent->set('errorCode', self::ERROR_ALREADY_EXISTING_USER); $oEvent->set('errorMessageTitle', ''); diff --git a/application/core/plugins/Authdb/Authdb.php b/application/core/plugins/Authdb/Authdb.php index 8fb3f57d0ed..50bea754684 100644 --- a/application/core/plugins/Authdb/Authdb.php +++ b/application/core/plugins/Authdb/Authdb.php @@ -44,6 +44,7 @@ public function createNewUser() $oEvent = $this->getEvent(); $preCollectedUserArray = $oEvent->get('preCollectedUserArray', []); $expires = null; + $status = true; if (empty($preCollectedUserArray)) { // Do nothing if the user to be added is not DB type @@ -54,6 +55,9 @@ public function createNewUser() $new_email = flattenText(Yii::app()->request->getPost('new_email'), false, true); $new_full_name = flattenText(Yii::app()->request->getPost('new_full_name'), false, true); $presetPassword = null; + if (Yii::app()->request->getPost('status')) { + $status = flattenText(Yii::app()->request->getPost('status'), false, true); + } if (Yii::app()->request->getPost('expires')) { $expires = flattenText(Yii::app()->request->getPost('expires'), false, true); } @@ -62,11 +66,14 @@ public function createNewUser() $new_email = flattenText($preCollectedUserArray['email']); $new_full_name = flattenText($preCollectedUserArray['full_name']); $presetPassword = flattenText($preCollectedUserArray['password']); + if (!empty($preCollectedUserArray['status'])) { + $status = $preCollectedUserArray['status']; + } if (!empty($preCollectedUserArray['expires'])) { $expires = $preCollectedUserArray['expires']; } } - + if (!LimeMailer::validateAddress($new_email)) { $oEvent->set('errorCode', self::ERROR_INVALID_EMAIL); $oEvent->set('errorMessageTitle', gT("Failed to add user")); @@ -75,7 +82,7 @@ public function createNewUser() } $new_pass = $presetPassword ?? createPassword(); - $iNewUID = User::insertUser($new_user, $new_pass, $new_full_name, Yii::app()->session['loginID'], $new_email, $expires); + $iNewUID = User::insertUser($new_user, $new_pass, $new_full_name, Yii::app()->session['loginID'], $new_email, $expires, $status); if (!$iNewUID) { $oEvent->set('errorCode', self::ERROR_ALREADY_EXISTING_USER); $oEvent->set('errorMessageTitle', ''); diff --git a/application/extensions/admin/survey/ListSurveysWidget/views/massive_actions/_action_results.php b/application/extensions/admin/survey/ListSurveysWidget/views/massive_actions/_action_results.php index 1fb2efd4bfe..6dc5a8c29e1 100644 --- a/application/extensions/admin/survey/ListSurveysWidget/views/massive_actions/_action_results.php +++ b/application/extensions/admin/survey/ListSurveysWidget/views/massive_actions/_action_results.php @@ -10,6 +10,9 @@ $tableLabels = array(gT('ID'), gT('Title'), gT('Status')); } ?> + + + diff --git a/application/helpers/update/updates/Update_619.php b/application/helpers/update/updates/Update_619.php new file mode 100644 index 00000000000..252e1f59df2 --- /dev/null +++ b/application/helpers/update/updates/Update_619.php @@ -0,0 +1,16 @@ +db->createCommand()->addColumn('{{users}}', 'user_status', 'BOOLEAN DEFAULT TRUE'); + } +} diff --git a/application/models/User.php b/application/models/User.php index b80e932811e..f4cddde6fac 100644 --- a/application/models/User.php +++ b/application/models/User.php @@ -43,6 +43,7 @@ * @property string $last_login * @property Permissiontemplates[] $roles * @property UserGroup[] $groups + * @property bool $user_status User's account status (true: activated | false: deactivated) */ class User extends LSActiveRecord { @@ -59,6 +60,7 @@ class User extends LSActiveRecord * @var string $lang Default value for user language */ public $lang = 'auto'; + public $searched_value; /** @@ -159,6 +161,7 @@ public function attributeLabels() 'modified' => gT('Modified at'), 'last_login' => gT('Last recorded login'), 'expires' => gT("Expiry date/time:"), + 'user_status' => gT("Status"), ]; } @@ -230,9 +233,10 @@ public function getFormattedDateCreated() * @param int $parent_user * @param string $new_email * @param string|null $expires + * @param boolean $status * @return integer|boolean User ID if success */ - public static function insertUser($new_user, $new_pass, $new_full_name, $parent_user, $new_email, $expires = null) + public static function insertUser($new_user, $new_pass, $new_full_name, $parent_user, $new_email, $expires = null, $status = true) { $oUser = new self(); $oUser->users_name = $new_user; @@ -244,6 +248,7 @@ public static function insertUser($new_user, $new_pass, $new_full_name, $parent_ $oUser->created = date('Y-m-d H:i:s'); $oUser->modified = date('Y-m-d H:i:s'); $oUser->expires = $expires; + $oUser->user_status = $status; if ($oUser->save()) { return $oUser->uid; } else { @@ -504,7 +509,6 @@ public function getManagementButtons() $permission_users_update = Permission::model()->hasGlobalPermission('users', 'update'); $permission_users_delete = Permission::model()->hasGlobalPermission('users', 'delete'); $userManager = new UserManager(App()->user, $this); - // User is owned or created by you $ownedOrCreated = $this->parent_id == App()->session['loginID']; @@ -516,7 +520,6 @@ public function getManagementButtons() $changeOwnershipUrl = App()->getController()->createUrl('userManagement/takeOwnership'); $deleteUrl = App()->getController()->createUrl('userManagement/deleteConfirm', ['userid' => $this->uid, 'user' => $this->full_name]); - $dropdownItems = []; $dropdownItems[] = [ 'title' => gT('User details'), @@ -540,6 +543,37 @@ public function getManagementButtons() ) ) ]; + + $permission = ( $permission_superadmin_read && !(Permission::isForcedSuperAdmin($this->uid) || $this->uid == App()->user->getId())) + || (!$permission_superadmin_read && ($this->uid != App()->session['loginID'] //Can't change your own permissions + && ( $permission_users_update && $ownedOrCreated) + && !Permission::isForcedSuperAdmin($this->uid) + ) + ); + + if ($this->user_status) { + $activateUrl = App()->getController()->createUrl('userManagement/activationConfirm', ['userid' => $this->uid, 'action' => 'deactivate']); + $dropdownItems[] = [ + 'title' => gT('Deactivate'), + 'iconClass' => "ri-user-unfollow-fill text-danger", + 'linkClass' => $permission ? "UserManagement--action--openmodal UserManagement--action--status" : '', + 'linkAttributes' => [ + 'data-href' => $permission ? $activateUrl : '#', + ], + 'enabledCondition' => $permission + ]; + } else { + $activateUrl = App()->getController()->createUrl('userManagement/activationConfirm', ['userid' => $this->uid, 'action' => 'activate']); + $dropdownItems[] = [ + 'title' => gT('Activate'), + 'iconClass' => "ri-user-follow-fill", + 'linkClass' => $permission ? "UserManagement--action--openmodal UserManagement--action--status" : '', + 'linkAttributes' => [ + 'data-href' => $permission ? $activateUrl : '#', + ], + 'enabledCondition' => $permission + ]; + } $dropdownItems[] = [ 'title' => gT('Edit permissions'), 'iconClass' => "ri-lock-fill", @@ -731,6 +765,12 @@ public function getManagementColums() "name" => "parentUserName", "header" => gT("Created by"), ], + [ + "name" => "user_status", + "header" => gT("Status"), + 'headerHtmlOptions' => ['class' => 'hidden'], + 'htmlOptions' => ['class' => 'hidden activation'] + ], ]; // NOTE: Super Administrators with just the "read" flag also have these flags @@ -866,7 +906,7 @@ public function search() $getUser = Yii::app()->request->getParam('User'); if (!empty($getUser['parentUserName'])) { - $getParentName = $getUser['parentUserName']; + $getParentName = $getUser['parentUserName']; $criteria->join = "LEFT JOIN {{users}} u ON t.parent_id = u.uid"; $criteria->compare('u.users_name', $getParentName, true, 'OR'); } @@ -1040,4 +1080,21 @@ public function canEdit($managerId = null) return Permission::model()->hasGlobalPermission('users', 'update', $managerId) && $this->parent_id == $managerId; } + + /** + * Set user activation status + * + * @param string $status + * @return bool + */ + public function setActivationStatus($status = 'activate') + { + if ($status == 'activate') { + $this->user_status = 1; + } else { + $this->user_status = 0; + } + + return $this->save(); + } } diff --git a/application/views/userManagement/index.php b/application/views/userManagement/index.php index ee906f27a14..2af8a9c023c 100644 --- a/application/views/userManagement/index.php +++ b/application/views/userManagement/index.php @@ -31,6 +31,7 @@ 'lsAfterAjaxUpdate' => [ 'bindListItemclick();', 'LS.UserManagement.bindButtons();', + 'showDeactivatedUserTooltip();' ], 'filter' => $model, 'summaryText' => gT('Displaying {start}-{end} of {count} result(s).') . ' ' @@ -59,6 +60,25 @@ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) }) + + function showDeactivatedUserTooltip() { + $('#usermanagement--identity-gridPanel #bottom-scroller table .activation').each(function (i, item) { + if (item.innerHTML == '0'){ + var tr = item.closest('tr') + tr.classList += ' disabled'; + tr.setAttribute('data-toggle', 'tooltip'); + tr.setAttribute('data-placement', 'top'); + tr.setAttribute('title', ''); + } + }); + $(function () { + $('[data-toggle="tooltip"]').tooltip() + }) + } + $(document).on('ready pjax:scriptcomplete', function(){ + showDeactivatedUserTooltip() + }); +