diff --git a/Controller/Adminhtml/Activity.php b/Controller/Adminhtml/Activity.php new file mode 100644 index 0000000..69d61ab --- /dev/null +++ b/Controller/Adminhtml/Activity.php @@ -0,0 +1,21 @@ +resultPageFactory = $resultPageFactory; + } + + public function execute() + { + $resultPage = $this->resultPageFactory->create(); + $resultPage->getConfig()->getTitle()->prepend((__('User Activity'))); + + return $resultPage; + } +} diff --git a/Controller/Adminhtml/Activity/MassDisable.php b/Controller/Adminhtml/Activity/MassDisable.php new file mode 100644 index 0000000..f90e11f --- /dev/null +++ b/Controller/Adminhtml/Activity/MassDisable.php @@ -0,0 +1,66 @@ +resultPageFactory = $resultPageFactory; + $this->activityModel = $activityModel; + + parent::__construct($context); + } + + public function execute() + { + try { + $userIds = $this->getRequest()->getPost('selected'); + + if (is_array($userIds)) { + $this->activityModel->updateUserStatus($userIds, 0); + + $this->messageManager->addSuccess( + __( + 'Disabled %1 user(s).', + count($userIds) + ) + ); + } + } catch (\Exception $e) { + $this->messageManager->addError($e->getMessage()); + } + + $this->_redirect('*/*/index'); + } +} diff --git a/Controller/Adminhtml/Activity/MassEnable.php b/Controller/Adminhtml/Activity/MassEnable.php new file mode 100644 index 0000000..62245bd --- /dev/null +++ b/Controller/Adminhtml/Activity/MassEnable.php @@ -0,0 +1,66 @@ +resultPageFactory = $resultPageFactory; + $this->activityModel = $activityModel; + + parent::__construct($context); + } + + public function execute() + { + try { + $userIds = $this->getRequest()->getPost('selected'); + + if (is_array($userIds)) { + $this->activityModel->updateUserStatus($userIds, 1); + + $this->messageManager->addSuccess( + __( + 'Enabled %1 user(s).', + count($userIds) + ) + ); + } + } catch (\Exception $e) { + $this->messageManager->addError($e->getMessage()); + } + + $this->_redirect('*/*/index'); + } +} diff --git a/Helper/Data.php b/Helper/Data.php new file mode 100644 index 0000000..f560f4b --- /dev/null +++ b/Helper/Data.php @@ -0,0 +1,68 @@ +dateFactory->create()->gmtDate(); + } + + /** + * Get difference between two dates. Return the number of days. + * + * @param string $dateFrom + * @param $dateTo + * @return float + */ + public function getDateDiff($dateTo, $dateFrom = 'now') + { + if ($dateFrom == 'now') { + $dateFrom = $this->getNow(); + } + + return round(abs(strtotime($dateFrom) - strtotime($dateTo))/86400); + } + + /** + * Get module configuration values from core_config_data + * + * @param $setting + * @return mixed + */ + public function getConfig($setting) + { + return $this->scopeConfig->getValue( + $this->tab . '/' . $setting, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/Model/Activity.php b/Model/Activity.php new file mode 100755 index 0000000..14c5a76 --- /dev/null +++ b/Model/Activity.php @@ -0,0 +1,26 @@ +_init(\Magenizr\AdminUser\Model\ResourceModel\Activity::class); + } + // @codingStandardsIgnoreEnd +} diff --git a/Model/ResourceModel/Activity.php b/Model/ResourceModel/Activity.php new file mode 100755 index 0000000..aef9d57 --- /dev/null +++ b/Model/ResourceModel/Activity.php @@ -0,0 +1,47 @@ +_init(Helper::TABLE_USER, 'user_id'); + } + // @codingStandardsIgnoreEnd + + /** + * Update the user status + * + * @param $userIds + * @param $status + * @return mixed + */ + public function updateUserStatus($userIds, $status) + { + if (!is_array($userIds)) { + $userIds = [$userIds]; + } + + return $this->getConnection()->update( + $this->getMainTable(), + ['is_active' => $status], + $this->getIdFieldName() . ' IN (' . $this->getConnection()->quote($userIds) . ')' + ); + } +} diff --git a/Model/ResourceModel/Activity/Collection.php b/Model/ResourceModel/Activity/Collection.php new file mode 100755 index 0000000..98f3a57 --- /dev/null +++ b/Model/ResourceModel/Activity/Collection.php @@ -0,0 +1,26 @@ +_init(\Magenizr\AdminUser\Model\Activity::class, \Magenizr\AdminUser\Model\ResourceModel\Activity::class); + } + // @codingStandardsIgnoreEnd +} diff --git a/Model/ResourceModel/Activity/Grid/Collection.php b/Model/ResourceModel/Activity/Grid/Collection.php new file mode 100644 index 0000000..872bd2a --- /dev/null +++ b/Model/ResourceModel/Activity/Grid/Collection.php @@ -0,0 +1,141 @@ +aggregations; + } + + /** + * @param AggregationInterface $aggregations + * @return $this + */ + public function setAggregations($aggregations) + { + $this->aggregations = $aggregations; + } + + /** + * Get search criteria. + * + * @return \Magento\Framework\Api\SearchCriteriaInterface|null + */ + public function getSearchCriteria() + { + return null; + } + + /** + * Set search criteria. + * + * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria + * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null) + { + return $this; + } + + /** + * Get total count. + * + * @return int + */ + public function getTotalCount() + { + return $this->getSize(); + } + + /** + * Set total count. + * + * @param int $totalCount + * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function setTotalCount($totalCount) + { + return $this; + } + + /** + * Set items list. + * + * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items + * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function setItems(array $items = null) + { + return $this; + } +} diff --git a/README.md b/README.md new file mode 100755 index 0000000..29e7f0c --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +This module will help you to identify rarely used admin accounts and allow you to disable or delete them directly. + +![Magenizr AdminUser - Backend](https://images2.imgbox.com/da/8f/wcLj1jC4_o.gif) + +## Business Value +The more admin user accounts are enabled in Magento®, the higher is the risk of getting compromised. + +* Disabling rarely used admin accounts will lower the risk of getting compromised. +* It helps your team and client to keep admin user accounts under control. + +## Features +* It shows rarely used admin user accounts at the beginning on the list. +* Bulk `enable` or `disable` admin accounts. + +## Usage +Go to `System > Permissions > User Activity` to see all admin users sorted by the last login date. + +## System Requirements +- Magento 2.3.x, 2.4.x +- PHP 5.6.x, 7.x + +## Installation (Composer) + +1. Update your composer.json `composer require "magenizr/magento2-adminuser":"1.0.0" --no-update` +2. Install dependencies and update your composer.lock `composer update --lock` + +``` +./composer.json has been updated +Loading composer repositories with package information +Updating dependencies (including require-dev) +Package operations: 1 install, 0 updates, 0 removals + - Installing magenizr/magento2-adminuser (1.0.0): Downloading (100%) +Writing lock file +Generating autoload files +``` + +3. Enable the module and clear static content. + +``` +php bin/magento module:enable Magenizr_AdminUser --clear-static-content +php bin/magento setup:upgrade +``` + +## Installation (Manually) + +1. Download the code. +2. Extract the downloaded tar.gz file. Example: `tar -xzf Magenizr_AdminUser_1.0.0.tar.gz`. +3. Copy the code into `./app/code/Magenizr/AdminUser/`. +4. Enable the module and clear static content. + +``` +php bin/magento module:enable Magenizr_AdminUser --clear-static-content +php bin/magento setup:upgrade +``` + +## Support +If you have any issues with this extension, open an issue on [GitHub](https://github.com/magenizr/Magenizr_AdminUser/issues). + +## Contact +Follow us on [GitHub](https://github.com/magenizr), [Twitter](https://twitter.com/magenizr) and [Facebook](https://www.facebook.com/magenizr). + +## History +===== 1.0.0 ===== +* Stable version + +## License +[OSL - Open Software Licence 3.0](https://opensource.org/licenses/osl-3.0.php) diff --git a/Ui/Component/Listing/Column/Highlight.php b/Ui/Component/Listing/Column/Highlight.php new file mode 100644 index 0000000..4b9dcdf --- /dev/null +++ b/Ui/Component/Listing/Column/Highlight.php @@ -0,0 +1,91 @@ +urlBuilder = $urlBuilder; + $this->helper = $helper; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item['user_id'])) { + $logDate = $item['logdate']; + + if (!empty($item['logdate'])) { + // Highlight row after X days + $daysHighlight = (int)$this->helper->getConfig('highlight_users_after_days'); + $daysDiff = (int)$this->helper->getDateDiff('now', $logDate); + + if ($daysDiff >= $daysHighlight) { + $item['highlight'] = 'warning'; + } + } else { + // Highlight row if logdate is empty + $item['highlight'] = 'warning'; + } + + // Highlight row if failed logins were registered + if ($item['failures_num'] > 0) { + $item['highlight'] = 'danger'; + } + } + } + } + + return $dataSource; + } +} diff --git a/Ui/Component/Listing/Column/Status.php b/Ui/Component/Listing/Column/Status.php new file mode 100755 index 0000000..4df5831 --- /dev/null +++ b/Ui/Component/Listing/Column/Status.php @@ -0,0 +1,30 @@ + 0, 'label' => __('Disabled')], + ['value' => 1, 'label' => __('Enabled')] + ]; + } +} diff --git a/Ui/Component/Listing/Column/Username.php b/Ui/Component/Listing/Column/Username.php new file mode 100644 index 0000000..30f8fe4 --- /dev/null +++ b/Ui/Component/Listing/Column/Username.php @@ -0,0 +1,79 @@ +urlBuilder = $urlBuilder; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + $template = '%s'; + + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item['user_id'])) { + $userId = $item['user_id']; + $url = $this->context->getUrl('adminhtml/user/edit/user_id', ['user_id' => $userId]); + + $username = $item['username']; + + $item['username'] = sprintf($template, $url, $username); + } + } + } + + return $dataSource; + } +} diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..815517f --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "magenizr/magento2-adminuser", + "description": "Identify rarely used admin accounts. It shows you the date and time of the last login as well as failed login attempts.", + "type": "magento2-module", + "version": "1.0.0", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Magenizr", + "email": "modules@magenizr.com", + "homepage": "https://agency.magenizr.com" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://repo.magento.com/" + } + ], + "keywords": [ + "Magento 2", + "Admin", + "Backend", + "User", + "Activity" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magenizr\\AdminUser\\": "" + } + } +} diff --git a/etc/acl.xml b/etc/acl.xml new file mode 100755 index 0000000..459a3c9 --- /dev/null +++ b/etc/acl.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/etc/adminhtml/menu.xml b/etc/adminhtml/menu.xml new file mode 100755 index 0000000..4de38fb --- /dev/null +++ b/etc/adminhtml/menu.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/etc/adminhtml/routes.xml b/etc/adminhtml/routes.xml new file mode 100644 index 0000000..23f702a --- /dev/null +++ b/etc/adminhtml/routes.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100755 index 0000000..20d1ec8 --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,27 @@ + + + + + +
+ advanced + + + + + validate-greater-than-zero validate-digits + System > Permissions > User Activity. Default: 30]]> + + +
+
+
diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 0000000..33a6989 --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,21 @@ + + + + + + + + 30 + + + + diff --git a/etc/di.xml b/etc/di.xml new file mode 100644 index 0000000..cfee18f --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,32 @@ + + + + + + + + + + Magenizr\AdminUser\Model\ResourceModel\Activity\Collection + + + + + + + + admin_user + Magenizr\AdminUser\Model\ResourceModel\Activity + + + + diff --git a/etc/module.xml b/etc/module.xml new file mode 100755 index 0000000..032f11f --- /dev/null +++ b/etc/module.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/i18n/de_DE.csv b/i18n/de_DE.csv new file mode 100755 index 0000000..ad47ae0 --- /dev/null +++ b/i18n/de_DE.csv @@ -0,0 +1,9 @@ +"No issues found.","Keine Fehler gefunden." +"No login since a while.","Keine Logins seit einer weile." +"Multiple failed login attempts.","Mehrere fehlgeschlagene Loginversuche." +"Go to Stores > Configuration > Advanced > Admin for module settings.","Gehe zu Stores > Configuration > Advanced > Admin für Moduleinstellungen." +"Disabled %1 user(s).","%1 Benutzer deaktiviert." +"Enabled %1 user(s).","%1 Benutzer aktiviert." +"Disabled","Deaktiviert" +"Enabled","Aktiviert" +"User Activity","Benutzeraktivität" \ No newline at end of file diff --git a/i18n/en_US.csv b/i18n/en_US.csv new file mode 100755 index 0000000..0d516b2 --- /dev/null +++ b/i18n/en_US.csv @@ -0,0 +1,9 @@ +"No issues found.","No issues found." +"No login since a while.","No login since a while." +"Multiple failed login attempts.","Multiple failed login attempts." +"Go to Stores > Configuration > Advanced > Admin for module settings.","Go to Stores > Configuration > Advanced > Admin for module settings." +"Disabled %1 user(s).","Disabled %1 user(s)." +"Enabled %1 user(s).","Enabled %1 user(s)." +"Disabled","Disabled" +"Enabled","Enabled" +"User Activity","User Activity" \ No newline at end of file diff --git a/registration.php b/registration.php new file mode 100755 index 0000000..a56eb3f --- /dev/null +++ b/registration.php @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/view/adminhtml/templates/info.phtml b/view/adminhtml/templates/info.phtml new file mode 100644 index 0000000..6b31fb4 --- /dev/null +++ b/view/adminhtml/templates/info.phtml @@ -0,0 +1,19 @@ + + +
+ +

Stores > Configuration > Advanced > Admin for module settings.', $this->getUrl('adminhtml/system_config/edit/section/admin')); ?>

+
diff --git a/view/adminhtml/ui_component/magenizr_adminuser_activity_index.xml b/view/adminhtml/ui_component/magenizr_adminuser_activity_index.xml new file mode 100755 index 0000000..379e7e4 --- /dev/null +++ b/view/adminhtml/ui_component/magenizr_adminuser_activity_index.xml @@ -0,0 +1,189 @@ + + + ++ + + magenizr_adminuser_activity_index.magenizr_adminuser_activity_index_data_source + magenizr_adminuser_activity_index.magenizr_adminuser_activity_index_data_source + + magenizr_adminuser_activity_columns + + + + Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider + magenizr_adminuser_activity_index_data_source + user_id + user_id + + + Magento_Ui/js/grid/provider + + + user_id + + + + + + + + + + + false + + + + + + + + + + + + enable + Enable + + + Enable User(s) + Are you sure you want to enable selected user(s)? + + + + + + + + + disable + Disable + + + + Disable User(s) + Are you sure you want to disable selected user(s)? + + + + + + + + + + + Magenizr_AdminUser/js/grid/listing + + + + + + + false + 55 + user_id + + + + + + + Magenizr\AdminUser\Ui\Component\Listing\Column\Status + + Status + select + Magento_Ui/js/grid/columns/select + select + + select + + + + + + + + + text + First Name + + + + + + + text + Last Name + + + + + + + text + Email + + + + + + + ui/grid/cells/html + text + Username + + + + + + + text + Created + + + + + + + text + Last Login + + + + + + + text + Number of Logins + + + + + + + text + Failed Logins + + + + + + + text + false + + + + + diff --git a/view/adminhtml/web/css/style.less b/view/adminhtml/web/css/style.less new file mode 100644 index 0000000..03f3b50 --- /dev/null +++ b/view/adminhtml/web/css/style.less @@ -0,0 +1,63 @@ +/** + * Magenizr AdminUser + * + * @category Magenizr + * @package Magenizr_AdminUser + * @copyright Copyright (c) 2021 Magenizr (https://agency.magenizr.com) + * @license https://www.magenizr.com/license Magenizr EULA + */ + +@background-color-warning: #fcf8e3; +@background-color-danger: #f2dede; +@border-color: #f5f5f5; + +.admin__data-grid-wrap { + overflow-x: inherit; +} + +#magenizr-adminuser { + + .agenda { + + list-style-type: none; + + li { + line-height: 25px; + } + + span { + display: inline-block; + float: left; + width: 50px; + height: 20px; + margin: 2px 10px 0 0; + border: 1px solid @border-color; + } + + .warning { + background-color: @background-color-warning; + } + + .danger { + background-color: @background-color-danger; + } + } +} + +.admin__data-grid-wrap-static .data-grid { + + tr td.max-reached, + tr:nth-child(even) td.max-reached { + background-color: @background-color-danger; + } + + tr td.warning, + tr:nth-child(even) td.warning { + background-color: @background-color-warning; + } + + tr td.danger, + tr:nth-child(even) td.danger { + background-color: @background-color-danger; + } +} diff --git a/view/adminhtml/web/js/grid/listing.js b/view/adminhtml/web/js/grid/listing.js new file mode 100644 index 0000000..598b864 --- /dev/null +++ b/view/adminhtml/web/js/grid/listing.js @@ -0,0 +1,36 @@ +/** + * Magenizr AdminUser + * + * @category Magenizr + * @package Magenizr_AdminUser + * @copyright Copyright (c) 2021 Magenizr (https://agency.magenizr.com) + * @license https://www.magenizr.com/license Magenizr EULA + */ + +define( + [ + 'Magento_Ui/js/grid/listing' + ], function (Collection) { + 'use strict'; + + return Collection.extend( + { + defaults: { + template: 'Magenizr_AdminUser/ui/grid/listing' + }, + getRowClass: function (row) { + + if (row.highlight == 'danger') { + return 'danger'; + } else if (row.highlight == 'warning') { + return 'warning'; + } else if (row.highlight == 'max-reached') { + return 'max-reached'; + } else { + return ''; + } + } + } + ); + } +); diff --git a/view/adminhtml/web/template/ui/grid/listing.html b/view/adminhtml/web/template/ui/grid/listing.html new file mode 100644 index 0000000..bd8f4b2 --- /dev/null +++ b/view/adminhtml/web/template/ui/grid/listing.html @@ -0,0 +1,27 @@ + + +
+ + + + + + + + + + +
+
+
+