diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml index 93ee464a17efa..40f80ce52c2de 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml @@ -35,8 +35,9 @@ - + + diff --git a/app/code/Magento/Security/Api/Data/UserExpirationInterface.php b/app/code/Magento/Security/Api/Data/UserExpirationInterface.php new file mode 100644 index 0000000000000..4f602c8b58a87 --- /dev/null +++ b/app/code/Magento/Security/Api/Data/UserExpirationInterface.php @@ -0,0 +1,67 @@ +localeDate = $localeDate; + $this->userExpirationResource = $userExpirationResource; + $this->userExpirationFactory = $userExpirationFactory; + } + + /** + * Add the `expires_at` field to the admin user edit form. + * + * @param \Magento\User\Block\User\Edit\Tab\Main $subject + * @param \Closure $proceed + * @return mixed + */ + public function aroundGetFormHtml( + \Magento\User\Block\User\Edit\Tab\Main $subject, + \Closure $proceed + ) { + /** @var \Magento\Framework\Data\Form $form */ + $form = $subject->getForm(); + if (is_object($form)) { + $dateFormat = $this->localeDate->getDateFormat( + \IntlDateFormatter::MEDIUM + ); + $timeFormat = $this->localeDate->getTimeFormat( + \IntlDateFormatter::MEDIUM + ); + $fieldset = $form->getElement('base_fieldset'); + $userIdField = $fieldset->getElements()->searchById('user_id'); + $userExpirationValue = null; + if ($userIdField) { + $userId = $userIdField->getValue(); + $userExpirationValue = $this->loadUserExpirationByUserId($userId); + } + $fieldset->addField( + 'expires_at', + 'date', + [ + 'name' => 'expires_at', + 'label' => __('Expiration Date'), + 'title' => __('Expiration Date'), + 'date_format' => $dateFormat, + 'time_format' => $timeFormat, + 'class' => 'validate-date', + 'value' => $userExpirationValue, + ] + ); + + $subject->setForm($form); + } + + return $proceed(); + } + + /** + * Loads a user expiration record by user ID. + * + * @param string $userId + * @return string + */ + private function loadUserExpirationByUserId($userId) + { + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $this->userExpirationFactory->create(); + $this->userExpirationResource->load($userExpiration, $userId); + return $userExpiration->getExpiresAt(); + } +} diff --git a/app/code/Magento/Security/Model/Plugin/AuthSession.php b/app/code/Magento/Security/Model/Plugin/AuthSession.php index 01203caaa31cd..6dc5e796d8950 100644 --- a/app/code/Magento/Security/Model/Plugin/AuthSession.php +++ b/app/code/Magento/Security/Model/Plugin/AuthSession.php @@ -7,6 +7,7 @@ use Magento\Backend\Model\Auth\Session; use Magento\Security\Model\AdminSessionsManager; +use Magento\Security\Model\UserExpirationManager; /** * Magento\Backend\Model\Auth\Session decorator @@ -33,22 +34,32 @@ class AuthSession */ protected $securityCookie; + /** + * @var UserExpirationManager + */ + private $userExpirationManager; + /** * @param \Magento\Framework\App\RequestInterface $request * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param AdminSessionsManager $sessionsManager * @param \Magento\Security\Model\SecurityCookie $securityCookie + * @param UserExpirationManager|null $userExpirationManager */ public function __construct( \Magento\Framework\App\RequestInterface $request, \Magento\Framework\Message\ManagerInterface $messageManager, AdminSessionsManager $sessionsManager, - \Magento\Security\Model\SecurityCookie $securityCookie + \Magento\Security\Model\SecurityCookie $securityCookie, + \Magento\Security\Model\UserExpirationManager $userExpirationManager = null ) { $this->request = $request; $this->messageManager = $messageManager; $this->sessionsManager = $sessionsManager; $this->securityCookie = $securityCookie; + $this->userExpirationManager = $userExpirationManager ?: + \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Security\Model\UserExpirationManager::class); } /** @@ -64,6 +75,11 @@ public function aroundProlong(Session $session, \Closure $proceed) $session->destroy(); $this->addUserLogoutNotification(); return null; + } elseif ($this->userExpirationManager->isUserExpired($session->getUser()->getId())) { + $this->userExpirationManager->deactivateExpiredUsersById([$session->getUser()->getId()]); + $session->destroy(); + $this->addUserLogoutNotification(); + return null; } $result = $proceed(); $this->sessionsManager->processProlong(); diff --git a/app/code/Magento/Security/Model/Plugin/UserValidationRules.php b/app/code/Magento/Security/Model/Plugin/UserValidationRules.php new file mode 100644 index 0000000000000..7fddbb21200f4 --- /dev/null +++ b/app/code/Magento/Security/Model/Plugin/UserValidationRules.php @@ -0,0 +1,42 @@ +validator = $validator; + } + + /** + * Add the Expires At validator to user validation rules. + * + * @param \Magento\User\Model\UserValidationRules $userValidationRules + * @param \Magento\Framework\Validator\DataObject $result + * @return \Magento\Framework\Validator\DataObject + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterAddUserInfoRules(\Magento\User\Model\UserValidationRules $userValidationRules, $result) + { + return $result->addRule($this->validator, 'expires_at'); + } +} diff --git a/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php new file mode 100644 index 0000000000000..71a331a178006 --- /dev/null +++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php @@ -0,0 +1,88 @@ +timezone = $timezone; + } + + /** + * Define main table + * + * @return void + */ + protected function _construct() + { + $this->_init('admin_user_expiration', 'user_id'); + } + + /** + * Convert to UTC time. + * + * @param \Magento\Framework\Model\AbstractModel $userExpiration + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _beforeSave(\Magento\Framework\Model\AbstractModel $userExpiration) + { + /** @var $userExpiration \Magento\Security\Model\UserExpiration */ + $expiresAt = $userExpiration->getExpiresAt(); + $utcValue = $this->timezone->convertConfigTimeToUtc($expiresAt); + $userExpiration->setExpiresAt($utcValue); + + return $this; + } + + /** + * Convert to store time. + * + * @param \Magento\Framework\Model\AbstractModel $userExpiration + * @return $this|\Magento\Framework\Model\ResourceModel\Db\AbstractDb + * @throws \Exception + */ + protected function _afterLoad(\Magento\Framework\Model\AbstractModel $userExpiration) + { + /** @var $userExpiration \Magento\Security\Model\UserExpiration */ + if ($userExpiration->getExpiresAt()) { + $storeValue = $this->timezone->date($userExpiration->getExpiresAt()); + $userExpiration->setExpiresAt($storeValue->format('Y-m-d H:i:s')); + } + + return $this; + } +} diff --git a/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php b/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php new file mode 100644 index 0000000000000..2f2971bc90225 --- /dev/null +++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php @@ -0,0 +1,75 @@ +_init( + \Magento\Security\Model\UserExpiration::class, + \Magento\Security\Model\ResourceModel\UserExpiration::class + ); + } + + /** + * Filter for expired, active users. + * + * @return $this + */ + public function addActiveExpiredUsersFilter(): Collection + { + $currentTime = new \DateTime(); + $currentTime->format('Y-m-d H:i:s'); + $this->getSelect()->joinLeft( + ['user' => $this->getTable('admin_user')], + 'main_table.user_id = user.user_id', + ['is_active'] + ); + $this->addFieldToFilter('expires_at', ['lt' => $currentTime]) + ->addFieldToFilter('user.is_active', 1); + + return $this; + } + + /** + * Filter collection by user id. + * + * @param int[] $userIds + * @return Collection + */ + public function addUserIdsFilter(array $userIds = []): Collection + { + return $this->addFieldToFilter('main_table.user_id', ['in' => $userIds]); + } + + /** + * Get any expired records for the given user. + * + * @param string $userId + * @return Collection + */ + public function addExpiredRecordsForUserFilter(string $userId): Collection + { + return $this->addActiveExpiredUsersFilter() + ->addFieldToFilter('main_table.user_id', (int)$userId); + } +} diff --git a/app/code/Magento/Security/Model/UserExpiration.php b/app/code/Magento/Security/Model/UserExpiration.php new file mode 100644 index 0000000000000..e6c711b7ac049 --- /dev/null +++ b/app/code/Magento/Security/Model/UserExpiration.php @@ -0,0 +1,87 @@ +_init(\Magento\Security\Model\ResourceModel\UserExpiration::class); + } + + /** + * `expires_at` getter. + * + * @return string + */ + public function getExpiresAt() + { + return $this->getData(self::EXPIRES_AT); + } + + /** + * `expires_at` setter. + * + * @param string $expiresAt + * @return $this + */ + public function setExpiresAt($expiresAt) + { + return $this->setData(self::EXPIRES_AT, $expiresAt); + } + + /** + * `user_id` getter. + * + * @return string + */ + public function getUserId() + { + return $this->getData(self::USER_ID); + } + + /** + * `user_id` setter. + * + * @param string $userId + * @return $this + */ + public function setUserId($userId) + { + return $this->setData(self::USER_ID, $userId); + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes(UserExpirationExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/Security/Model/UserExpiration/Validator.php b/app/code/Magento/Security/Model/UserExpiration/Validator.php new file mode 100644 index 0000000000000..62dbd7852ff33 --- /dev/null +++ b/app/code/Magento/Security/Model/UserExpiration/Validator.php @@ -0,0 +1,69 @@ +timezone = $timezone; + $this->dateTime = $dateTime; + } + + /** + * Ensure that the given date is later than the current date. + * + * @param string $value + * @return bool + * @throws \Exception + */ + public function isValid($value) + { + $this->_clearMessages(); + $messages = []; + $expiresAt = $value; + $label = 'Expiration date'; + if (\Zend_Validate::is($expiresAt, 'NotEmpty')) { + if (strtotime($expiresAt)) { + $currentTime = $this->dateTime->gmtTimestamp(); + $utcExpiresAt = $this->timezone->convertConfigTimeToUtc($expiresAt); + $expiresAt = $this->timezone->date($utcExpiresAt)->getTimestamp(); + if ($expiresAt < $currentTime) { + $messages['expires_at'] = __('"%1" must be later than the current date.', $label); + } + } else { + $messages['expires_at'] = __('"%1" is not a valid date.', $label); + } + } + $this->_addMessages($messages); + + return empty($messages); + } +} diff --git a/app/code/Magento/Security/Model/UserExpirationManager.php b/app/code/Magento/Security/Model/UserExpirationManager.php new file mode 100644 index 0000000000000..fe6b87de5a8ec --- /dev/null +++ b/app/code/Magento/Security/Model/UserExpirationManager.php @@ -0,0 +1,153 @@ +dateTime = $dateTime; + $this->securityConfig = $securityConfig; + $this->adminSessionInfoCollectionFactory = $adminSessionInfoCollectionFactory; + $this->authSession = $authSession; + $this->userExpirationCollectionFactory = $userExpirationCollectionFactory; + $this->userCollectionFactory = $userCollectionFactory; + } + + /** + * Deactivate expired user accounts and invalidate their sessions. + */ + public function deactivateExpiredUsers(): void + { + /** @var ExpiredUsersCollection $expiredRecords */ + $expiredRecords = $this->userExpirationCollectionFactory->create()->addActiveExpiredUsersFilter(); + $this->processExpiredUsers($expiredRecords); + } + + /** + * Deactivate specific expired users. + * + * @param array $userIds + */ + public function deactivateExpiredUsersById(array $userIds): void + { + $expiredRecords = $this->userExpirationCollectionFactory->create() + ->addActiveExpiredUsersFilter() + ->addUserIdsFilter($userIds); + $this->processExpiredUsers($expiredRecords); + } + + /** + * Deactivate expired user accounts and invalidate their sessions. + * + * @param ExpiredUsersCollection $expiredRecords + */ + private function processExpiredUsers(ExpiredUsersCollection $expiredRecords): void + { + if ($expiredRecords->getSize() > 0) { + // get all active sessions for the users and set them to logged out + /** @var \Magento\Security\Model\ResourceModel\AdminSessionInfo\Collection $currentSessions */ + $currentSessions = $this->adminSessionInfoCollectionFactory->create() + ->addFieldToFilter('user_id', ['in' => $expiredRecords->getAllIds()]) + ->filterExpiredSessions($this->securityConfig->getAdminSessionLifetime()); + /** @var \Magento\Security\Model\AdminSessionInfo $currentSession */ + $currentSessions->setDataToAll('status', \Magento\Security\Model\AdminSessionInfo::LOGGED_OUT) + ->save(); + } + + // delete expired records + $expiredRecordIds = $expiredRecords->getAllIds(); + + // set user is_active to 0 + $users = $this->userCollectionFactory->create() + ->addFieldToFilter('main_table.user_id', ['in' => $expiredRecordIds]); + $users->setDataToAll('is_active', 0)->save(); + $expiredRecords->walk('delete'); + } + + /** + * Check if the given user is expired. + * + * @param string $userId + * @return bool + */ + public function isUserExpired(string $userId): bool + { + $isExpired = false; + /** @var \Magento\Security\Api\Data\UserExpirationInterface $expiredRecord */ + $expiredRecord = $this->userExpirationCollectionFactory->create() + ->addExpiredRecordsForUserFilter($userId) + ->getFirstItem(); + if ($expiredRecord && $expiredRecord->getId()) { + $expiresAt = $this->dateTime->timestamp($expiredRecord->getExpiresAt()); + $isExpired = $expiresAt < $this->dateTime->gmtTimestamp(); + } + + return $isExpired; + } +} diff --git a/app/code/Magento/Security/Observer/AdminUserAuthenticateBefore.php b/app/code/Magento/Security/Observer/AdminUserAuthenticateBefore.php new file mode 100644 index 0000000000000..2d0f7bc0f0ac0 --- /dev/null +++ b/app/code/Magento/Security/Observer/AdminUserAuthenticateBefore.php @@ -0,0 +1,69 @@ +userExpirationManager = $userExpirationManager; + $this->userFactory = $userFactory; + } + + /** + * Check for expired user when logging in. + * + * @param Observer $observer + * @return void + * @throws AuthenticationException + */ + public function execute(Observer $observer) + { + $username = $observer->getEvent()->getUsername(); + $user = $this->userFactory->create(); + /** @var \Magento\User\Model\User $user */ + $user->loadByUsername($username); + + if ($user->getId() && $this->userExpirationManager->isUserExpired($user->getId())) { + $this->userExpirationManager->deactivateExpiredUsersById([$user->getId()]); + throw new AuthenticationException( + __( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) + ); + } + } +} diff --git a/app/code/Magento/Security/Observer/AfterAdminUserSave.php b/app/code/Magento/Security/Observer/AfterAdminUserSave.php new file mode 100644 index 0000000000000..d11c1bfdcdf17 --- /dev/null +++ b/app/code/Magento/Security/Observer/AfterAdminUserSave.php @@ -0,0 +1,76 @@ +userExpirationFactory = $userExpirationFactory; + $this->userExpirationResource = $userExpirationResource; + } + + /** + * Save user expiration. + * + * @param Observer $observer + * @return void + * @throws \Magento\Framework\Exception\AlreadyExistsException + */ + public function execute(Observer $observer) + { + /* @var $user \Magento\User\Model\User */ + $user = $observer->getEvent()->getObject(); + if ($user->getId()) { + $expiresAt = $user->getExpiresAt(); + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $this->userExpirationFactory->create(); + $this->userExpirationResource->load($userExpiration, $user->getId()); + + if (empty($expiresAt)) { + // delete it if the admin user clears the field + if ($userExpiration->getId()) { + $this->userExpirationResource->delete($userExpiration); + } + } else { + if (!$userExpiration->getId()) { + $userExpiration->setId($user->getId()); + } + $userExpiration->setExpiresAt($expiresAt); + $this->userExpirationResource->save($userExpiration); + } + } + } +} diff --git a/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminFillInUserWithExpirationActionGroup.xml b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminFillInUserWithExpirationActionGroup.xml new file mode 100644 index 0000000000000..d1d5427506e40 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminFillInUserWithExpirationActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + Goes to the Admin Users grid page. Clicks on Create User. Fills in the provided User details with a expiration date. + + + + + + + diff --git a/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserInvalidExpirationActionGroup.xml b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserInvalidExpirationActionGroup.xml new file mode 100644 index 0000000000000..02280ed809124 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserInvalidExpirationActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + Error message for saving an admin user with an invalid expiration date. + + + + + + + + + diff --git a/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserSuccessActionGroup.xml b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserSuccessActionGroup.xml new file mode 100644 index 0000000000000..7aed1e07112cf --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserSuccessActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + Success message for saving an admin user successfully. + + + + + + diff --git a/app/code/Magento/Security/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/Security/Test/Mftf/Section/AdminEditUserSection.xml new file mode 100644 index 0000000000000..d7acf2466c09e --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Section/AdminEditUserSection.xml @@ -0,0 +1,13 @@ + + + +
+ +
+
diff --git a/app/code/Magento/Security/Test/Mftf/Section/AdminNewUserFormSection.xml b/app/code/Magento/Security/Test/Mftf/Section/AdminNewUserFormSection.xml new file mode 100644 index 0000000000000..1d1aba4da07dd --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Section/AdminNewUserFormSection.xml @@ -0,0 +1,13 @@ + + + +
+ +
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml new file mode 100644 index 0000000000000..02a8170b308d3 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml @@ -0,0 +1,35 @@ + + + + + + + + + <description value="Create new user with invalid expiration date."/> + <testCaseId value="" /> + <severity value="CRITICAL"/> + <group value="security"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + <generateDate date="-5 day" format="M j, Y g:i:s A" stepKey="expiresDateTime"/> + <actionGroup ref="AdminFillInUserWithExpirationActionGroup" stepKey="fillInNewUserWithValidExpiration"> + <argument name="expires_at" value="{$expiresDateTime}"/> + </actionGroup> + <actionGroup ref="AdminSaveUserInvalidExpirationActionGroup" stepKey="saveNewUserWithInvalidExpirationError" /> + </test> +</tests> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml new file mode 100644 index 0000000000000..dc971f2044760 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateNewUserWithValidExpirationTest"> + <annotations> + <features value="Security"/> + <stories value="Create new user with valid expiration date."/> + <title value="Create new user with valid expiration date"/> + <description value="Create new user with valid expiration date."/> + <testCaseId value="" /> + <severity value="CRITICAL"/> + <group value="security"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + <generateDate date="+5 day" format="M j, Y g:i:s A" stepKey="expiresDateTime"/> + <actionGroup ref="AdminFillInUserWithExpirationActionGroup" stepKey="fillInNewUserWithValidExpiration"> + <argument name="expires_at" value="{$expiresDateTime}"/> + </actionGroup> + <actionGroup ref="AdminSaveUserSuccessActionGroup" stepKey="saveNewUserWithValidExpirationSuccess" /> + + <actionGroup ref="AdminOpenUserEditPageActionGroup" stepKey="openUserEditPage"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + <seeInField userInput="{$expiresDateTime}" selector="input#user_expires_at" stepKey="verifyUserExpirationInField"/> + + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml new file mode 100644 index 0000000000000..bdea845c81a56 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + + <test name="AdminLoginAdminUserWithInvalidExpiration"> + <annotations> + <features value="Security"/> + <stories value="Try to login as a user with an invalid expiration date."/> + <title value="Try to login as a user with an invalid expiration date"/> + <description value="Try to login as a user with an invalid expiration date."/> + <testCaseId value=""/> + <severity value="CRITICAL"/> + <group value="security"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + <generateDate date="+2 minute" format="M d, Y g:i:s A" stepKey="expiresDateTime"/> + <actionGroup ref="AdminFillInUserWithExpirationActionGroup" stepKey="fillInNewUserWithValidExpiration"> + <argument name="expires_at" value="{$expiresDateTime}"/> + </actionGroup> + <grabValueFrom selector="{{AdminNewUserFormSection.username}}" stepKey="grabUsername"/> + <grabValueFrom selector="{{AdminNewUserFormSection.password}}" stepKey="grabPassword"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewUserFormSection.userInfoTab}}" stepKey="openUserInfoTab"/> + <actionGroup ref="AdminSaveUserSuccessActionGroup" stepKey="saveNewUserWithValidExpirationSuccess"/> + <actionGroup ref="logout" stepKey="logout"/> + <wait time="120" stepKey="waitForUserToExpire"/> + <actionGroup ref="LoginAdminWithCredentialsActionGroup" stepKey="loginAsNewAdmin"> + <argument name="adminUser" value="{$grabUsername}"/> + <argument name="adminPassword" value="{$grabPassword}"/> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="checkLoginMessage" /> + + <!-- delete user --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="NewAdminUser" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml new file mode 100644 index 0000000000000..12bba27f21269 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + + <test name="AdminLoginAdminUserWithValidExpiration"> + <annotations> + <features value="Security"/> + <stories value="Login as a user with a valid expiration date."/> + <title value="Login as a user with a valid expiration date"/> + <description value="Login as a user with a valid expiration date."/> + <testCaseId value=""/> + <severity value="CRITICAL"/> + <group value="security"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + <generateDate date="+5 day" format="M d, Y g:i:s A" stepKey="expiresDateTime"/> + <actionGroup ref="AdminFillInUserWithExpirationActionGroup" stepKey="fillInNewUserWithValidExpiration"> + <argument name="expires_at" value="{$expiresDateTime}"/> + </actionGroup> + <grabValueFrom selector="{{AdminNewUserFormSection.username}}" stepKey="grabUsername"/> + <grabValueFrom selector="{{AdminNewUserFormSection.password}}" stepKey="grabPassword"/> + <actionGroup ref="AdminSaveUserSuccessActionGroup" stepKey="saveNewUserWithValidExpirationSuccess"/> + <actionGroup ref="logout" stepKey="logout"/> + <actionGroup ref="LoginAdminWithCredentialsActionGroup" stepKey="loginAsNewAdmin"> + <argument name="adminUser" value="{$grabUsername}"/> + <argument name="adminPassword" value="{$grabPassword}"/> + </actionGroup> + <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> + + <actionGroup ref="logout" stepKey="logoutAsUserWithValidExpiration"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml new file mode 100644 index 0000000000000..c4603a88c56c4 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + + <test name="AdminNavigateWhileUserExpiredTest"> + <annotations> + <features value="Security"/> + <stories value="Navigate to an admin page after user expiration date passes."/> + <title value="Navigate to an admin page after user expiration date passes"/> + <description value="Navigate to an admin page after user expiration date passes."/> + <testCaseId value="" /> + <severity value="CRITICAL"/> + <group value="security"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create user --> + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + <generateDate date="+2 minute" format="M d, Y g:i:s A" stepKey="expiresDateTime"/> + <actionGroup ref="AdminFillInUserWithExpirationActionGroup" stepKey="fillInNewUserWithValidExpiration"> + <argument name="expires_at" value="{$expiresDateTime}"/> + </actionGroup> + <grabValueFrom selector="{{AdminNewUserFormSection.username}}" stepKey="grabUsername"/> + <grabValueFrom selector="{{AdminNewUserFormSection.password}}" stepKey="grabPassword"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewUserFormSection.userInfoTab}}" stepKey="openUserInfoTab"/> + <actionGroup ref="AdminSaveUserSuccessActionGroup" stepKey="saveNewUserWithValidExpirationSuccess"/> + <actionGroup ref="logout" stepKey="logout"/> + + <!-- Login as that user --> + <actionGroup ref="LoginAdminWithCredentialsActionGroup" stepKey="loginAsNewAdmin"> + <argument name="adminUser" value="{$grabUsername}"/> + <argument name="adminPassword" value="{$grabPassword}"/> + </actionGroup> + <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> + <wait time="120" stepKey="waitForUserToExpire"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <!-- Confirm that user is logged out --> + <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> + + <!-- Delete created user --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php index 0f7f590b71de4..9870de8fc2cb4 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php @@ -37,6 +37,12 @@ class AuthSessionTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ protected $objectManager; + /**@var \Magento\Security\Model\UserExpirationManager */ + protected $userExpirationManagerMock; + + /**@var \Magento\User\Model\User */ + protected $userMock; + /** * Init mocks for tests * @return void @@ -61,20 +67,31 @@ public function setUp() $this->securityCookieMock = $this->createPartialMock(SecurityCookie::class, ['setLogoutReasonCookie']); - $this->authSessionMock = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['destroy']); + $this->authSessionMock = $this->createPartialMock( + \Magento\Backend\Model\Auth\Session::class, + ['destroy', 'getUser'] + ); $this->currentSessionMock = $this->createPartialMock( \Magento\Security\Model\AdminSessionInfo::class, ['isLoggedInStatus', 'getStatus', 'isActive'] ); + $this->userExpirationManagerMock = $this->createPartialMock( + \Magento\Security\Model\UserExpirationManager::class, + ['isUserExpired', 'deactivateExpiredUsersById'] + ); + + $this->userMock = $this->createMock(\Magento\User\Model\User::class); + $this->model = $this->objectManager->getObject( \Magento\Security\Model\Plugin\AuthSession::class, [ 'request' => $this->requestMock, 'messageManager' => $this->messageManagerMock, 'sessionsManager' => $this->adminSessionsManagerMock, - 'securityCookie' => $this->securityCookieMock + 'securityCookie' => $this->securityCookieMock, + 'userExpirationManager' => $this->userExpirationManagerMock, ] ); @@ -154,6 +171,59 @@ public function testAroundProlongSessionIsNotActiveAndIsAjaxRequest() $this->model->aroundProlong($this->authSessionMock, $proceed); } + /** + * @return void + */ + public function testAroundProlongSessionIsActiveUserIsExpired() + { + $result = 'result'; + $errorMessage = 'Error Message'; + + $proceed = function () use ($result) { + return $result; + }; + + $adminUserId = '12345'; + $this->currentSessionMock->expects($this->once()) + ->method('isLoggedInStatus') + ->willReturn(true); + + $this->authSessionMock->expects($this->exactly(2)) + ->method('getUser') + ->willReturn($this->userMock); + + $this->userMock->expects($this->exactly(2)) + ->method('getId') + ->willReturn($adminUserId); + + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('isAjax') + ->willReturn(false); + + $this->userExpirationManagerMock->expects($this->once()) + ->method('isUserExpired') + ->with($adminUserId) + ->willReturn(true); + + $this->userExpirationManagerMock->expects($this->once()) + ->method('deactivateExpiredUsersById') + ->with([$adminUserId]); + + $this->authSessionMock->expects($this->once()) + ->method('destroy'); + + $this->adminSessionsManagerMock->expects($this->once()) + ->method('getLogoutReasonMessage') + ->willReturn($errorMessage); + + $this->messageManagerMock->expects($this->once()) + ->method('addErrorMessage') + ->with($errorMessage); + + $this->model->aroundProlong($this->authSessionMock, $proceed); + } + /** * @return void */ @@ -164,10 +234,24 @@ public function testAroundProlongSessionIsActive() return $result; }; + $adminUserId = '12345'; $this->currentSessionMock->expects($this->any()) ->method('isLoggedInStatus') ->willReturn(true); + $this->authSessionMock->expects($this->once()) + ->method('getUser') + ->willReturn($this->userMock); + + $this->userMock->expects($this->once()) + ->method('getId') + ->willReturn($adminUserId); + + $this->userExpirationManagerMock->expects($this->once()) + ->method('isUserExpired') + ->with($adminUserId) + ->willReturn(false); + $this->adminSessionsManagerMock->expects($this->any()) ->method('processProlong'); diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php new file mode 100644 index 0000000000000..00b3356c2e11d --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php @@ -0,0 +1,61 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Security\Test\Unit\Model\Plugin; + +/** + * Test class for expiration date user validation rule. + */ +class UserValidationRulesTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var \Magento\Security\Model\Plugin\UserValidationRules|\PHPUnit\Framework\MockObject\MockObject + */ + private $plugin; + + /** + * @var \Magento\User\Model\UserValidationRules|\PHPUnit\Framework\MockObject\MockObject + */ + private $userValidationRules; + + /** + * @var \Magento\Framework\Validator\DataObject|\PHPUnit\Framework\MockObject\MockObject + */ + private $validator; + + /** + * @var \Magento\User\Model\UserValidationRules + */ + private $rules; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $userExpirationValidator = $this->createMock(\Magento\Security\Model\UserExpiration\Validator::class); + $this->userValidationRules = $this->createMock(\Magento\User\Model\UserValidationRules::class); + $this->rules = $objectManager->getObject(\Magento\User\Model\UserValidationRules::class); + $this->validator = $this->createMock(\Magento\Framework\Validator\DataObject::class); + $this->plugin = + $objectManager->getObject( + \Magento\Security\Model\Plugin\UserValidationRules::class, + ['validator' => $userExpirationValidator] + ); + } + + public function testAfterAddUserInfoRules() + { + $this->validator->expects(static::exactly(5))->method('addRule')->willReturn($this->validator); + static::assertSame($this->validator, $this->rules->addUserInfoRules($this->validator)); + static::assertSame($this->validator, $this->callAfterAddUserInfoRulesPlugin($this->validator)); + } + + protected function callAfterAddUserInfoRulesPlugin($validator) + { + return $this->plugin->afterAddUserInfoRules($this->userValidationRules, $validator); + } +} diff --git a/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php b/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php new file mode 100644 index 0000000000000..28541231cb123 --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Test\Unit\Model\UserExpiration; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Test class for \Magento\Security\Model\UserExpiration\Validator. + */ +class ValidatorTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var \Magento\Security\Model\UserExpiration\Validator + */ + private $validator; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Stdlib\DateTime\DateTime + */ + private $dateTimeMock; + + /**@var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Stdlib\DateTime\TimezoneInterface */ + private $timezoneMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->dateTimeMock = + $this->createPartialMock(\Magento\Framework\Stdlib\DateTime\DateTime::class, ['gmtTimestamp']); + $this->timezoneMock = + $this->createPartialMock( + \Magento\Framework\Stdlib\DateTime\Timezone::class, + ['date', 'convertConfigTimeToUtc'] + ); + $this->validator = $objectManager->getObject( + \Magento\Security\Model\UserExpiration\Validator::class, + ['dateTime' => $this->dateTimeMock, 'timezone' => $this->timezoneMock] + ); + } + + public function testWithInvalidDate() + { + $expireDate = 'invalid_date'; + + static::assertFalse($this->validator->isValid($expireDate)); + static::assertContains( + '"Expiration date" is not a valid date.', + $this->validator->getMessages() + ); + } + + public function testWithPastDate() + { + /** @var \DateTime|\PHPUnit_Framework_MockObject_MockObject $dateObject */ + $dateObject = $this->createMock(\DateTime::class); + $this->timezoneMock->expects(static::once()) + ->method('date') + ->will(static::returnValue($dateObject)); + + $currentDate = new \DateTime(); + $currentDate = $currentDate->getTimestamp(); + $expireDate = new \DateTime(); + $expireDate->modify('-10 days'); + + $this->dateTimeMock->expects(static::once())->method('gmtTimestamp')->willReturn($currentDate); + $this->timezoneMock->expects(static::once())->method('date')->willReturn($expireDate); + $dateObject->expects(static::once())->method('getTimestamp')->willReturn($expireDate->getTimestamp()); + static::assertFalse($this->validator->isValid($expireDate->format('Y-m-d H:i:s'))); + static::assertContains( + '"Expiration date" must be later than the current date.', + $this->validator->getMessages() + ); + } + + public function testWithFutureDate() + { + /** @var \DateTime|\PHPUnit_Framework_MockObject_MockObject $dateObject */ + $dateObject = $this->createMock(\DateTime::class); + $this->timezoneMock->expects(static::once()) + ->method('date') + ->will(static::returnValue($dateObject)); + $currentDate = new \DateTime(); + $currentDate = $currentDate->getTimestamp(); + $expireDate = new \DateTime(); + $expireDate->modify('+10 days'); + + $this->dateTimeMock->expects(static::once())->method('gmtTimestamp')->willReturn($currentDate); + $this->timezoneMock->expects(static::once())->method('date')->willReturn($expireDate); + $dateObject->expects(static::once())->method('getTimestamp')->willReturn($expireDate->getTimestamp()); + static::assertTrue($this->validator->isValid($expireDate->format('Y-m-d H:i:s'))); + static::assertEquals([], $this->validator->getMessages()); + } +} diff --git a/app/code/Magento/Security/Test/Unit/Observer/AdminUserAuthenticateBeforeTest.php b/app/code/Magento/Security/Test/Unit/Observer/AdminUserAuthenticateBeforeTest.php new file mode 100644 index 0000000000000..3f5717abe2a2c --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Observer/AdminUserAuthenticateBeforeTest.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Test\Unit\Observer; + +/** + * Test for \Magento\Security\Observer\AdminUserAuthenticateBefore + */ +class AdminUserAuthenticateBeforeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Security\Model\UserExpirationManager + */ + private $userExpirationManagerMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\User\Model\User + */ + private $userMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\User\Model\UserFactory + */ + private $userFactoryMock; + + /** + * @var \Magento\Security\Observer\AdminUserAuthenticateBefore + */ + private $observer; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Event\Observer::class + */ + private $eventObserverMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Event + */ + private $eventMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Security\Api\Data\UserExpirationInterface + */ + private $userExpirationMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->userExpirationManagerMock = $this->createPartialMock( + \Magento\Security\Model\UserExpirationManager::class, + ['isUserExpired', 'deactivateExpiredUsersById'] + ); + $this->userFactoryMock = $this->createPartialMock(\Magento\User\Model\UserFactory::class, ['create']); + $this->userMock = $this->createPartialMock(\Magento\User\Model\User::class, ['loadByUsername', 'getId']); + $this->observer = $this->objectManager->getObject( + \Magento\Security\Observer\AdminUserAuthenticateBefore::class, + [ + 'userExpirationManager' => $this->userExpirationManagerMock, + 'userFactory' => $this->userFactoryMock, + ] + ); + $this->eventObserverMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']); + $this->eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getUsername']); + $this->userExpirationMock = $this->createPartialMock( + \Magento\Security\Api\Data\UserExpirationInterface::class, + [ + 'getUserId', + 'getExpiresAt', + 'setUserId', + 'setExpiresAt', + 'getExtensionAttributes', + 'setExtensionAttributes' + ] + ); + } + + /** + * @expectedException \Magento\Framework\Exception\Plugin\AuthenticationException + * @expectedExceptionMessage The account sign-in was incorrect or your account is disabled temporarily. + * Please wait and try again later + */ + public function testWithExpiredUser() + { + $adminUserId = '123'; + $username = 'testuser'; + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getUsername')->willReturn($username); + $this->userFactoryMock->expects(static::once())->method('create')->willReturn($this->userMock); + $this->userMock->expects(static::once())->method('loadByUsername')->willReturnSelf(); + + $this->userExpirationManagerMock->expects(static::once()) + ->method('isUserExpired') + ->with($adminUserId) + ->willReturn(true); + $this->userMock->expects(static::exactly(3))->method('getId')->willReturn($adminUserId); + $this->userExpirationManagerMock->expects(static::once()) + ->method('deactivateExpiredUsersById') + ->with([$adminUserId]) + ->willReturn(null); + $this->observer->execute($this->eventObserverMock); + } + + public function testWithNonExpiredUser() + { + $adminUserId = '123'; + $username = 'testuser'; + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getUsername')->willReturn($username); + $this->userFactoryMock->expects(static::once())->method('create')->willReturn($this->userMock); + $this->userMock->expects(static::once())->method('loadByUsername')->willReturnSelf(); + $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($adminUserId); + $this->userExpirationManagerMock->expects(static::once()) + ->method('isUserExpired') + ->with($adminUserId) + ->willReturn(false); + $this->observer->execute($this->eventObserverMock); + } +} diff --git a/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php new file mode 100644 index 0000000000000..85505632e1eb6 --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Test\Unit\Observer; + +/** + * Test class for \Magento\Security\Observer\AfterAdminUserSave + */ +class AfterAdminUserSaveTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Security\Model\UserExpirationFactory + */ + private $userExpirationFactoryMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Security\Model\ResourceModel\UserExpiration + */ + private $userExpirationResourceMock; + + /** + * @var \Magento\Security\Observer\AfterAdminUserSave + */ + private $observer; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Event\Observer + */ + private $eventObserverMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Event + */ + private $eventMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\User\Model\User + */ + private $userMock; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Security\Model\UserExpiration + */ + private $userExpirationMock; + + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->userExpirationFactoryMock = $this->createMock(\Magento\Security\Model\UserExpirationFactory::class); + $this->userExpirationResourceMock = $this->createPartialMock( + \Magento\Security\Model\ResourceModel\UserExpiration::class, + ['load', 'save', 'delete'] + ); + $this->observer = $this->objectManager->getObject( + \Magento\Security\Observer\AfterAdminUserSave::class, + [ + 'userExpirationFactory' => $this->userExpirationFactoryMock, + 'userExpirationResource' => $this->userExpirationResourceMock, + ] + ); + $this->eventObserverMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']); + $this->eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getObject']); + $this->userMock = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', 'getExpiresAt']); + $this->userExpirationMock = $this->createPartialMock( + \Magento\Security\Model\UserExpiration::class, + ['getId', 'getExpiresAt', 'setId', 'setExpiresAt'] + ); + } + + public function testSaveNewUserExpiration() + { + $userId = '123'; + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); + $this->userMock->expects(static::exactly(3))->method('getId')->willReturn($userId); + $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn($this->getExpiresDateTime()); + $this->userExpirationFactoryMock->expects(static::once())->method('create') + ->willReturn($this->userExpirationMock); + $this->userExpirationResourceMock->expects(static::once())->method('load') + ->willReturn($this->userExpirationMock); + + $this->userExpirationMock->expects(static::once())->method('getId')->willReturn(null); + $this->userExpirationMock->expects(static::once())->method('setId')->willReturn($this->userExpirationMock); + $this->userExpirationMock->expects(static::once())->method('setExpiresAt') + ->willReturn($this->userExpirationMock); + $this->userExpirationResourceMock->expects(static::once())->method('save') + ->willReturn($this->userExpirationResourceMock); + $this->observer->execute($this->eventObserverMock); + } + + /** + * @throws \Exception + */ + public function testClearUserExpiration() + { + $userId = '123'; + $this->userExpirationMock->setId($userId); + + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); + $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($userId); + $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn(null); + $this->userExpirationFactoryMock->expects(static::once())->method('create') + ->willReturn($this->userExpirationMock); + $this->userExpirationResourceMock->expects(static::once())->method('load') + ->willReturn($this->userExpirationMock); + + $this->userExpirationMock->expects(static::once())->method('getId')->willReturn($userId); + $this->userExpirationResourceMock->expects(static::once())->method('delete') + ->willReturn($this->userExpirationResourceMock); + $this->observer->execute($this->eventObserverMock); + } + + public function testChangeUserExpiration() + { + $userId = '123'; + $this->userExpirationMock->setId($userId); + + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); + $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($userId); + $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn($this->getExpiresDateTime()); + $this->userExpirationFactoryMock->expects(static::once())->method('create') + ->willReturn($this->userExpirationMock); + $this->userExpirationResourceMock->expects(static::once())->method('load') + ->willReturn($this->userExpirationMock); + + $this->userExpirationMock->expects(static::once())->method('getId')->willReturn($userId); + $this->userExpirationMock->expects(static::once())->method('setExpiresAt') + ->willReturn($this->userExpirationMock); + $this->userExpirationResourceMock->expects(static::once())->method('save') + ->willReturn($this->userExpirationResourceMock); + $this->observer->execute($this->eventObserverMock); + } + + /** + * @return string + * @throws \Exception + */ + private function getExpiresDateTime() + { + $testDate = new \DateTime(); + $testDate->modify('+10 days'); + return $testDate->format('Y-m-d H:i:s'); + } +} diff --git a/app/code/Magento/Security/etc/adminhtml/di.xml b/app/code/Magento/Security/etc/adminhtml/di.xml index 79477e9443097..388a1eac742a5 100644 --- a/app/code/Magento/Security/etc/adminhtml/di.xml +++ b/app/code/Magento/Security/etc/adminhtml/di.xml @@ -15,6 +15,9 @@ <type name="Magento\Backend\Controller\Adminhtml\Auth\Login"> <plugin name="security_login_form" type="Magento\Security\Model\Plugin\LoginController" /> </type> + <type name="Magento\User\Model\UserValidationRules"> + <plugin name="user_expiration_validator" type="Magento\Security\Model\Plugin\UserValidationRules" /> + </type> <type name="Magento\Security\Model\Plugin\AccountManagement"> <arguments> <argument name="passwordRequestEvent" xsi:type="const">Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST</argument> @@ -28,4 +31,7 @@ </argument> </arguments> </type> + <type name="Magento\User\Block\User\Edit\Tab\Main"> + <plugin name="user_expiration_user_form_field" type="Magento\Security\Model\Plugin\AdminUserForm"/> + </type> </config> diff --git a/app/code/Magento/Security/etc/crontab.xml b/app/code/Magento/Security/etc/crontab.xml index a30a43730e6fa..7ee046ee44850 100644 --- a/app/code/Magento/Security/etc/crontab.xml +++ b/app/code/Magento/Security/etc/crontab.xml @@ -13,5 +13,8 @@ <job name="security_clean_password_reset_request_event_records" instance="Magento\Security\Model\SecurityManager" method="cleanExpiredRecords"> <schedule>0 0 * * *</schedule> </job> + <job name="security_deactivate_expired_users" instance="Magento\Security\Model\UserExpirationManager" method="deactivateExpiredUsers"> + <schedule>0 * * * *</schedule> + </job> </group> </config> diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml index 5052f5642cb53..34bb497954a7e 100644 --- a/app/code/Magento/Security/etc/db_schema.xml +++ b/app/code/Magento/Security/etc/db_schema.xml @@ -55,4 +55,15 @@ <column name="created_at"/> </index> </table> + <table name="admin_user_expiration" resource="default" engine="innodb" comment="Admin User expiration dates table"> + <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="User ID"/> + <column xsi:type="timestamp" name="expires_at" nullable="false" default="0" comment="User Expiration Date"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="user_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="ADMIN_USER_EXPIRATION_USER_ID_ADMIN_USER_USER_ID" + table="admin_user_expiration" column="user_id" referenceTable="admin_user" + referenceColumn="user_id" onDelete="CASCADE"/> + </table> </schema> diff --git a/app/code/Magento/Security/etc/db_schema_whitelist.json b/app/code/Magento/Security/etc/db_schema_whitelist.json index c387b7591c7a5..1f8183e123956 100644 --- a/app/code/Magento/Security/etc/db_schema_whitelist.json +++ b/app/code/Magento/Security/etc/db_schema_whitelist.json @@ -33,5 +33,15 @@ "constraint": { "PRIMARY": true } + }, + "admin_user_expiration": { + "column": { + "user_id": true, + "expires_at": true + }, + "constraint": { + "PRIMARY": true, + "ADMIN_USER_EXPIRATION_USER_ID_ADMIN_USER_USER_ID": true + } } } \ No newline at end of file diff --git a/app/code/Magento/Security/etc/di.xml b/app/code/Magento/Security/etc/di.xml index 4fe88f219cf74..3b07bb84b1161 100644 --- a/app/code/Magento/Security/etc/di.xml +++ b/app/code/Magento/Security/etc/di.xml @@ -18,4 +18,5 @@ </argument> </arguments> </type> + <preference for="Magento\Security\Api\Data\UserExpirationInterface" type="Magento\Security\Model\UserExpiration"/> </config> diff --git a/app/code/Magento/Security/etc/events.xml b/app/code/Magento/Security/etc/events.xml new file mode 100644 index 0000000000000..f85cfc9387c48 --- /dev/null +++ b/app/code/Magento/Security/etc/events.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="admin_user_save_after"> + <observer name="add_user_expiration" instance="Magento\Security\Observer\AfterAdminUserSave"/> + </event> + <event name="admin_user_authenticate_before"> + <observer name="check_user_expiration" instance="Magento\Security\Observer\AdminUserAuthenticateBefore"/> + </event> +</config> diff --git a/app/code/Magento/Security/i18n/en_US.csv b/app/code/Magento/Security/i18n/en_US.csv index 0cf998b21a1c8..3d5bb6e8b59d4 100644 --- a/app/code/Magento/Security/i18n/en_US.csv +++ b/app/code/Magento/Security/i18n/en_US.csv @@ -26,3 +26,6 @@ None,None "Limit the number of password reset request per hour. Use 0 to disable.","Limit the number of password reset request per hour. Use 0 to disable." "Min Time Between Password Reset Requests","Min Time Between Password Reset Requests" "Delay in minutes between password reset requests. Use 0 to disable.","Delay in minutes between password reset requests. Use 0 to disable." +"""%1"" must be later than the current date.","""%1"" must be later than the current date." +"User Expiration","User Expiration" +"""%1"" is not a valid date.","""%1"" is not a valid date." diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml index eb3ef37056b2f..15cb4e5319904 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml @@ -13,7 +13,7 @@ <description>Fills in the provided User details on the New User creation page.</description> </annotations> <arguments> - <argument name="user" type="entity"/> + <argument name="user" defaultValue="NewAdminUser"/> </arguments> <fillField selector="{{AdminNewUserFormSection.username}}" userInput="{{user.username}}" stepKey="fillUser"/> diff --git a/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php b/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php index 369d71ddbff9b..68e049804e95a 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php @@ -59,6 +59,24 @@ public function testCreateAdminAccessToken() $this->assertEquals($accessToken, $token); } + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + * @expectedException \Magento\Framework\Exception\AuthenticationException + */ + public function testCreateAdminAccessTokenExpiredUser() + { + $adminUserNameFromFixture = 'adminUserExpired'; + $this->tokenService->createAdminAccessToken( + $adminUserNameFromFixture, + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + + $this->expectExceptionMessage( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ); + } + /** * @dataProvider validationDataProvider */ diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php index 6509e3af1050a..a979165c5bd59 100644 --- a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php @@ -8,6 +8,7 @@ /** * @magentoAppArea adminhtml * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AuthSessionTest extends \PHPUnit\Framework\TestCase { @@ -141,4 +142,46 @@ public function testProcessProlong() $this->assertGreaterThan(strtotime($oldUpdatedAt), strtotime($updatedAt)); } + + /** + * Test processing prolong with an expired user. + * + * @magentoDbIsolation enabled + */ + public function testProcessProlongWithExpiredUser() + { + $this->auth->login( + \Magento\TestFramework\Bootstrap::ADMIN_NAME, + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + + $expireDate = new \DateTime(); + $expireDate->modify('-10 days'); + /** @var \Magento\User\Model\User $user */ + $user = $this->objectManager->create(\Magento\User\Model\User::class); + $user->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $userExpirationFactory = + $this->objectManager->create(\Magento\Security\Api\Data\UserExpirationInterfaceFactory::class); + /** @var \Magento\Security\Api\Data\UserExpirationInterface $userExpiration */ + $userExpiration = $userExpirationFactory->create(); + $userExpiration->setId($user->getId()) + ->setExpiresAt($expireDate->format('Y-m-d H:i:s')) + ->save(); + + // need to trigger a prolong + $sessionId = $this->authSession->getSessionId(); + $prolongsDiff = 4 * log($this->securityConfig->getAdminSessionLifetime()) + 2; + $dateInPast = $this->dateTime->formatDate($this->authSession->getUpdatedAt() - $prolongsDiff); + $this->adminSessionsManager->getCurrentSession() + ->setData( + 'updated_at', + $dateInPast + ) + ->save(); + $this->adminSessionInfo->load($sessionId, 'session_id'); + $this->authSession->prolong(); + static::assertFalse($this->auth->isLoggedIn()); + $user->reload(); + static::assertFalse((bool)$user->getIsActive()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php new file mode 100644 index 0000000000000..e52f84c68d851 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Security\Model\ResourceModel\UserExpiration; + +/** + * Test UserExpiration collection filters. + */ +class CollectionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Security\Model\ResourceModel\UserExpiration\CollectionFactory + */ + protected $collectionModelFactory; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + protected $objectManager; + + /** + * Set up + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->collectionModelFactory = $this->objectManager + ->create(\Magento\Security\Model\ResourceModel\UserExpiration\CollectionFactory::class); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testAddExpiredActiveUsersFilter() + { + /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */ + $collectionModel = $this->collectionModelFactory->create(); + $collectionModel->addActiveExpiredUsersFilter(); + static::assertEquals(1, $collectionModel->getSize()); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testAddUserIdsFilter() + { + $adminUserNameFromFixture = 'adminUserExpired'; + $user = $this->objectManager->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + + /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */ + $collectionModel = $this->collectionModelFactory->create()->addUserIdsFilter([$user->getId()]); + static::assertEquals(1, $collectionModel->getSize()); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testAddExpiredRecordsForUserFilter() + { + $adminUserNameFromFixture = 'adminUserExpired'; + $user = $this->objectManager->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + + /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */ + $collectionModel = $this->collectionModelFactory->create()->addExpiredRecordsForUserFilter($user->getId()); + static::assertEquals(1, $collectionModel->getSize()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php new file mode 100644 index 0000000000000..adb7b7a120f1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Model; + +/** + * Tests for \Magento\Security\Model\UserExpirationManager + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UserExpirationManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\Backend\Model\Auth + */ + private $auth; + + /** + * @var \Magento\Backend\Model\Auth\Session + */ + private $authSession; + + /** + * @var \Magento\Security\Model\AdminSessionInfo + */ + private $adminSessionInfo; + + /** + * @var \Magento\Security\Model\UserExpirationManager + */ + private $userExpirationManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); + $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); + $this->adminSessionInfo = $this->objectManager->create(\Magento\Security\Model\AdminSessionInfo::class); + $this->auth->setAuthStorage($this->authSession); + $this->userExpirationManager = + $this->objectManager->create(\Magento\Security\Model\UserExpirationManager::class); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testUserIsExpired() + { + $adminUserNameFromFixture = 'adminUserExpired'; + $user = $this->loadUserByUsername($adminUserNameFromFixture); + static::assertTrue($this->userExpirationManager->isUserExpired($user->getId())); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + * @magentoAppIsolation enabled + */ + public function testDeactivateExpiredUsersWithExpiredUser() + { + $adminUsernameFromFixture = 'adminUserNotExpired'; + $this->loginUser($adminUsernameFromFixture); + $user = $this->loadUserByUsername($adminUsernameFromFixture); + $sessionId = $this->authSession->getSessionId(); + $this->expireUser($user); + $this->userExpirationManager->deactivateExpiredUsersById([$user->getId()]); + $this->adminSessionInfo->load($sessionId, 'session_id'); + $user->reload(); + $userExpirationModel = $this->loadExpiredUserModelByUser($user); + static::assertEquals(0, $user->getIsActive()); + static::assertNull($userExpirationModel->getId()); + static::assertEquals(AdminSessionInfo::LOGGED_OUT, (int)$this->adminSessionInfo->getStatus()); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + * @magentoAppIsolation enabled + */ + public function testDeactivateExpiredUsersWithNonExpiredUser() + { + $adminUsernameFromFixture = 'adminUserNotExpired'; + $this->loginUser($adminUsernameFromFixture); + $user = $this->loadUserByUsername($adminUsernameFromFixture); + $sessionId = $this->authSession->getSessionId(); + $this->userExpirationManager->deactivateExpiredUsersById([$user->getId()]); + $user->reload(); + $userExpirationModel = $this->loadExpiredUserModelByUser($user); + $this->adminSessionInfo->load($sessionId, 'session_id'); + static::assertEquals(1, $user->getIsActive()); + static::assertEquals($user->getId(), $userExpirationModel->getId()); + static::assertEquals(AdminSessionInfo::LOGGED_IN, (int)$this->adminSessionInfo->getStatus()); + } + + /** + * Test deactivating without inputting a user. + * + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testDeactivateExpiredUsers() + { + $notExpiredUser = $this->loadUserByUsername('adminUserNotExpired'); + $expiredUser = $this->loadUserByUsername('adminUserExpired'); + $this->userExpirationManager->deactivateExpiredUsers(); + $notExpiredUserExpirationModel = $this->loadExpiredUserModelByUser($notExpiredUser); + $expiredUserExpirationModel = $this->loadExpiredUserModelByUser($expiredUser); + + static::assertNotNull($notExpiredUserExpirationModel->getId()); + static::assertNull($expiredUserExpirationModel->getId()); + $notExpiredUser->reload(); + $expiredUser->reload(); + static::assertEquals($notExpiredUser->getIsActive(), 1); + static::assertEquals($expiredUser->getIsActive(), 0); + } + + /** + * Login the given user and return a user model. + * + * @param string $username + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function loginUser(string $username) + { + $this->auth->login( + $username, + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + } + + /** + * @param $username + * @return \Magento\User\Model\User + */ + private function loadUserByUsername(string $username): \Magento\User\Model\User + { + /** @var \Magento\User\Model\User $user */ + $user = $this->objectManager->create(\Magento\User\Model\User::class); + $user->loadByUsername($username); + return $user; + } + + /** + * Expire the given user and return the UserExpiration model. + * + * @param \Magento\User\Model\User $user + * @throws \Exception + */ + private function expireUser(\Magento\User\Model\User $user) + { + $expireDate = new \DateTime(); + $expireDate->modify('-10 days'); + /** @var \Magento\Security\Api\Data\UserExpirationInterface $userExpiration */ + $userExpiration = $this->objectManager->create(\Magento\Security\Api\Data\UserExpirationInterface::class); + $userExpiration->setId($user->getId()) + ->setExpiresAt($expireDate->format('Y-m-d H:i:s')) + ->save(); + } + + /** + * @param \Magento\User\Model\User $user + * @return \Magento\Security\Model\UserExpiration + */ + private function loadExpiredUserModelByUser(\Magento\User\Model\User $user): \Magento\Security\Model\UserExpiration + { + /** @var \Magento\Security\Model\UserExpiration $expiredUserModel */ + $expiredUserModel = $this->objectManager->create(\Magento\Security\Model\UserExpiration::class); + $expiredUserModel->load($user->getId()); + return $expiredUserModel; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Observer/AdminUserAuthenticateBeforeTest.php b/dev/tests/integration/testsuite/Magento/Security/Observer/AdminUserAuthenticateBeforeTest.php new file mode 100644 index 0000000000000..f25b836b80514 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Observer/AdminUserAuthenticateBeforeTest.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Observer; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for \Magento\Security\Observer\AdminUserAuthenticateBefore + */ +class AdminUserAuthenticateBeforeTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + * @expectedException \Magento\Framework\Exception\Plugin\AuthenticationException + * @expectedExceptionMessage The account sign-in was incorrect or your account is disabled temporarily. + * Please wait and try again later + */ + public function testWithExpiredUser() + { + $adminUserNameFromFixture = 'adminUserExpired'; + $password = \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD; + /** @var \Magento\User\Model\User $user */ + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->authenticate($adminUserNameFromFixture, $password); + static::assertFalse((bool)$user->getIsActive()); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testWithNonExpiredUser() + { + $adminUserNameFromFixture = 'adminUserNotExpired'; + $password = \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD; + /** @var \Magento\User\Model\User $user */ + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->authenticate($adminUserNameFromFixture, $password); + static::assertTrue((bool)$user->getIsActive()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php new file mode 100644 index 0000000000000..ee2ae79b62cf8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Observer; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for \Magento\Security\Observer\AfterAdminUserSave + */ +class AfterAdminUserSaveTest extends \PHPUnit\Framework\TestCase +{ + + /** + * Save a new UserExpiration record + * + * @magentoDataFixture Magento/User/_files/dummy_user.php + */ + public function testSaveNewUserExpiration() + { + $adminUserNameFromFixture = 'dummy_username'; + $testDate = $this->getFutureDateInStoreTime(); + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + $user->setExpiresAt($testDate); + $user->save(); + + $userExpirationFactory = + Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class); + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $userExpirationFactory->create(); + $userExpiration->load($user->getId()); + static::assertNotNull($userExpiration->getId()); + static::assertEquals($userExpiration->getExpiresAt(), $testDate); + } + + /** + * Save a new UserExpiration; used to validate that date conversion is working correctly. + * + * @magentoDataFixture Magento/User/_files/dummy_user.php + */ + public function testSaveNewUserExpirationInMinutes() + { + $adminUserNameFromFixture = 'dummy_username'; + $testDate = $this->getFutureDateInStoreTime('+2 minutes'); + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + $user->setExpiresAt($testDate); + $user->save(); + + $userExpirationFactory = + Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class); + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $userExpirationFactory->create(); + $userExpiration->load($user->getId()); + static::assertNotNull($userExpiration->getId()); + static::assertEquals($userExpiration->getExpiresAt(), $testDate); + } + + /** + * Remove the UserExpiration record + * + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testClearUserExpiration() + { + $adminUserNameFromFixture = 'adminUserExpired'; + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + $user->setExpiresAt(null); + $user->save(); + + $userExpirationFactory = + Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class); + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $userExpirationFactory->create(); + $userExpiration->load($user->getId()); + static::assertNull($userExpiration->getId()); + } + + /** + * Change the UserExpiration record + * + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testChangeUserExpiration() + { + $adminUserNameFromFixture = 'adminUserNotExpired'; + $testDate = $this->getFutureDateInStoreTime(); + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + + $userExpirationFactory = + Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class); + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $userExpirationFactory->create(); + $userExpiration->load($user->getId()); + $existingExpiration = $userExpiration->getExpiresAt(); + + $user->setExpiresAt($testDate); + $user->save(); + $userExpiration->load($user->getId()); + static::assertNotNull($userExpiration->getId()); + static::assertEquals($userExpiration->getExpiresAt(), $testDate); + static::assertNotEquals($existingExpiration, $userExpiration->getExpiresAt()); + } + + /** + * @param string $timeToAdd Amount of time to add + * @return string + * @throws \Exception + */ + private function getFutureDateInStoreTime($timeToAdd = '+20 days') + { + /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface $locale */ + $locale = Bootstrap::getObjectManager()->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); + $testDate = new \DateTime(); + $testDate->modify($timeToAdd); + $storeDate = $locale->date($testDate); + return $storeDate->format('Y-m-d H:i:s'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php new file mode 100644 index 0000000000000..ac45f120ca6d3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php @@ -0,0 +1,58 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** + * Create an admin user with expired and non-expired access date + */ + +/** @var $userModelNotExpired \Magento\User\Model\User */ +$userModelNotExpired = $objectManager->create(\Magento\User\Model\User::class); +$userModelNotExpired->setFirstName("John") + ->setLastName("Doe") + ->setUserName('adminUserNotExpired') + ->setPassword(\Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ->setEmail('adminUserNotExpired@example.com') + ->setRoleType('G') + ->setResourceId('Magento_Adminhtml::all') + ->setPrivileges("") + ->setAssertId(0) + ->setRoleId(1) + ->setPermission('allow') + ->setIsActive(1) + ->save(); +$futureDate = new \DateTime(); +$futureDate->modify('+10 days'); +$notExpiredRecord = $objectManager->create(\Magento\Security\Model\UserExpiration::class); +$notExpiredRecord + ->setId($userModelNotExpired->getId()) + ->setExpiresAt($futureDate->format('Y-m-d H:i:s')) + ->save(); + +/** @var $userModelExpired \Magento\User\Model\User */ +$pastDate = new \DateTime(); +$pastDate->modify('-10 days'); +$userModelExpired = $objectManager->create(\Magento\User\Model\User::class); +$userModelExpired->setFirstName("John") + ->setLastName("Doe") + ->setUserName('adminUserExpired') + ->setPassword(\Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ->setEmail('adminUserExpired@example.com') + ->setRoleType('G') + ->setResourceId('Magento_Adminhtml::all') + ->setPrivileges("") + ->setAssertId(0) + ->setRoleId(1) + ->setPermission('allow') + ->setIsActive(1) + ->save(); +$expiredRecord = $objectManager->create(\Magento\Security\Model\UserExpiration::class); +$expiredRecord + ->setId($userModelExpired->getId()) + ->setExpiresAt($pastDate->format('Y-m-d H:i:s')) + ->save();