From e51bf0a909ab69ff397dac26a8edcf34e88b3008 Mon Sep 17 00:00:00 2001 From: lfolco Date: Sun, 30 Jun 2019 12:55:13 -0400 Subject: [PATCH] Create models for userexpiration, add tests and events (magento/magento2#22833: Short-term admin accounts) --- .../Security/Model/AdminSessionsManager.php | 75 ++++---- .../Model/Plugin/UserValidationRules.php | 40 ++++ .../Model/ResourceModel/UserExpiration.php | 49 +++++ .../UserExpiration/Collection.php | 75 ++++++++ .../Magento/Security/Model/UserExpiration.php | 62 ++++++ .../Model/UserExpiration/Validator.php | 45 +++++ .../Security/Model/UserExpirationManager.php | 179 ++++++++++++++++++ .../Security/Observer/AfterAdminUserLoad.php | 54 ++++++ .../Security/Observer/AfterAdminUserSave.php | 65 +++++++ .../Observer/BeforeAdminUserAuthenticate.php | 61 ++++++ .../Unit/Model/AdminSessionsManagerTest.php | 2 +- .../Model/Plugin/UserValidationRulesTest.php | 63 ++++++ .../Model/UserExpiration/ValidatorTest.php | 49 +++++ .../Unit/Observer/AfterAdminUserLoadTest.php | 120 ++++++++++++ .../Unit/Observer/AfterAdminUserSaveTest.php | 159 ++++++++++++++++ .../BeforeAdminUserAuthenticateTest.php | 119 ++++++++++++ .../Magento/Security/etc/adminhtml/di.xml | 3 + .../Magento/Security/etc/adminhtml/events.xml | 14 ++ app/code/Magento/Security/etc/crontab.xml | 3 + app/code/Magento/Security/etc/db_schema.xml | 11 ++ .../Security/etc/db_schema_whitelist.json | 10 + app/code/Magento/Security/i18n/en_US.csv | 1 + .../Model/AdminTokenServiceTest.php | 19 ++ .../Model/AdminSessionsManagerTest.php | 27 ++- .../UserExpiration/CollectionTest.php | 62 ++++++ .../Model/UserExpirationManagerTest.php | 148 +++++++++++++++ .../Observer/AfterAdminUserLoadTest.php | 49 +++++ .../Observer/AfterAdminUserSaveTest.php | 101 ++++++++++ .../BeforeAdminUserAuthenticateTest.php | 47 +++++ .../Magento/Security/_files/expired_users.php | 71 +++++++ 30 files changed, 1742 insertions(+), 41 deletions(-) create mode 100644 app/code/Magento/Security/Model/Plugin/UserValidationRules.php create mode 100644 app/code/Magento/Security/Model/ResourceModel/UserExpiration.php create mode 100644 app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php create mode 100644 app/code/Magento/Security/Model/UserExpiration.php create mode 100644 app/code/Magento/Security/Model/UserExpiration/Validator.php create mode 100644 app/code/Magento/Security/Model/UserExpirationManager.php create mode 100644 app/code/Magento/Security/Observer/AfterAdminUserLoad.php create mode 100644 app/code/Magento/Security/Observer/AfterAdminUserSave.php create mode 100644 app/code/Magento/Security/Observer/BeforeAdminUserAuthenticate.php create mode 100644 app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php create mode 100644 app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php create mode 100644 app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserLoadTest.php create mode 100644 app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php create mode 100644 app/code/Magento/Security/Test/Unit/Observer/BeforeAdminUserAuthenticateTest.php create mode 100644 app/code/Magento/Security/etc/adminhtml/events.xml create mode 100644 dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserLoadTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Security/Observer/BeforeAdminUserAuthenticateTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php diff --git a/app/code/Magento/Security/Model/AdminSessionsManager.php b/app/code/Magento/Security/Model/AdminSessionsManager.php index 2a9024c54ee95..0c1cca617cbc8 100644 --- a/app/code/Magento/Security/Model/AdminSessionsManager.php +++ b/app/code/Magento/Security/Model/AdminSessionsManager.php @@ -27,11 +27,6 @@ class AdminSessionsManager */ const LOGOUT_REASON_USER_LOCKED = 10; - /** - * User has been logged out due to an expired user account - */ - const LOGOUT_REASON_USER_EXPIRED = 11; - /** * @var ConfigInterface * @since 100.1.0 @@ -80,6 +75,12 @@ class AdminSessionsManager */ private $maxIntervalBetweenConsecutiveProlongs = 60; + /** + * TODO: make sure we need this here + * @var UserExpirationManager + */ + private $userExpirationManager; + /** * @param ConfigInterface $securityConfig * @param \Magento\Backend\Model\Auth\Session $authSession @@ -87,6 +88,7 @@ class AdminSessionsManager * @param CollectionFactory $adminSessionInfoCollectionFactory * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime * @param RemoteAddress $remoteAddress + * @param UserExpirationManager|null $userExpirationManager */ public function __construct( ConfigInterface $securityConfig, @@ -94,7 +96,8 @@ public function __construct( \Magento\Security\Model\AdminSessionInfoFactory $adminSessionInfoFactory, \Magento\Security\Model\ResourceModel\AdminSessionInfo\CollectionFactory $adminSessionInfoCollectionFactory, \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - RemoteAddress $remoteAddress + RemoteAddress $remoteAddress, + \Magento\Security\Model\UserExpirationManager $userExpirationManager = null ) { $this->securityConfig = $securityConfig; $this->authSession = $authSession; @@ -102,6 +105,9 @@ public function __construct( $this->adminSessionInfoCollectionFactory = $adminSessionInfoCollectionFactory; $this->dateTime = $dateTime; $this->remoteAddress = $remoteAddress; + $this->userExpirationManager = $userExpirationManager ?: + \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Security\Model\UserExpirationManager::class); } /** @@ -127,10 +133,6 @@ public function processLogin() } } - if ($this->authSession->getUser()->getExpiresAt()) { - $this->revokeExpiredAdminUser(); - } - return $this; } @@ -142,7 +144,33 @@ public function processLogin() */ public function processProlong() { + // TODO: is this the right place for this? Or should I use a plugin? This method is called in a plugin + // also, don't want to hit the database every single time. We could put it within the lastProlongIsOldEnough + // in order to reduece database loads, but what if the user is expired already? How granular do we want to get? + // if their session is expired, then they will get logged out anyways, and we can handle deactivating them + // upon login or via the cron + + // already (\Magento\Security\Model\Plugin\AuthSession::aroundProlong, which plugs into + // \Magento\Backend\Model\Auth\Session::prolong, which is called from + // \Magento\Backend\App\Action\Plugin\Authentication::aroundDispatch, which is a plugin to + // \Magento\Backend\App\AbstractAction::dispatch) + + // \Magento\Backend\App\AbstractAction::dispatch is called, which kicks off the around plugin + // \Magento\Backend\App\Action\Plugin\Authentication::aroundDispatch, which calls + // \Magento\Backend\Model\Auth\Session::prolong, which kicks off the around plugin + // \Magento\Security\Model\Plugin\AuthSession::aroundProlong, which calls + // this method. + + // this method will prolong the session only if it's old enough, otherwise it's not called. +// if ($this->userExpirationManager->userIsExpired($this->authSession->getUser())) { +// $this->userExpirationManager->deactivateExpiredUsers([$this->authSession->getUser()->getId()]); +// } + if ($this->lastProlongIsOldEnough()) { + // TODO: throw exception? + if ($this->userExpirationManager->userIsExpired($this->authSession->getUser())) { + $this->userExpirationManager->deactivateExpiredUsers([$this->authSession->getUser()->getId()]); + } $this->getCurrentSession()->setData( 'updated_at', date( @@ -153,11 +181,6 @@ public function processProlong() $this->getCurrentSession()->save(); } - // todo: don't necessarily have a user here - if ($this->authSession->getUser()->getExpiresAt()) { - $this->revokeExpiredAdminUser(); - } - return $this; } @@ -223,11 +246,6 @@ public function getLogoutReasonMessageByStatus($statusCode) 'Your account is temporarily disabled. Please try again later.' ); break; - case self::LOGOUT_REASON_USER_EXPIRED: - $reasonMessage = __( - 'Your account has expired.' - ); - break; default: $reasonMessage = __('Your current session has been expired.'); break; @@ -373,21 +391,4 @@ private function getIntervalBetweenConsecutiveProlongs() ) ); } - - /** - * Check if the current user is expired and, if so, revoke their admin token. - */ - private function revokeExpiredAdminUser() - { - $expiresAt = $this->dateTime->gmtTimestamp($this->authSession->getUser()->getExpiresAt()); - if ($expiresAt < $this->dateTime->gmtTimestamp()) { - $currentSessions = $this->getSessionsForCurrentUser(); - $currentSessions->setDataToAll('status', self::LOGOUT_REASON_USER_EXPIRED) - ->save(); - $this->authSession->getUser() - ->setIsActive(0) - ->setExpiresAt(null) - ->save(); - } - } } 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..75b826dfb6b65 --- /dev/null +++ b/app/code/Magento/Security/Model/Plugin/UserValidationRules.php @@ -0,0 +1,40 @@ +validator = $validator; + } + + /** + * @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..5afca619c3f7f --- /dev/null +++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php @@ -0,0 +1,49 @@ +_init('admin_user_expiration', 'user_id'); + } + + /** + * Perform actions before object save + * + * @param \Magento\Framework\Model\AbstractModel $object + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function _beforeSave(\Magento\Framework\Model\AbstractModel $object) + { + /** @var $object \Magento\Security\Model\UserExpiration */ + if ($object->getExpiresAt() instanceof \DateTimeInterface) { + + // TODO: use this? need to check if we're ever passing in a \DateTimeInterface or if it's always a string + $object->setExpiresAt($object->getExpiresAt()->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..08c72f7d9fd2f --- /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. + * + * @param string $now + * @return $this + */ + public function addActiveExpiredUsersFilter($now = null): Collection + { + if ($now === null) { + $now = new \DateTime(); + $now->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' => $now]) + ->addFieldToFilter('user.is_active', 1); + + return $this; + } + + /** + * Filter collection by user id. + * @param array $userIds + * @return Collection + */ + public function addUserIdsFilter($userIds = []): Collection + { + return $this->addFieldToFilter('main_table.user_id', ['in' => $userIds]); + } + + /** + * Get any expired records for the given user. + * + * @param $userId + * @return Collection + */ + public function addExpiredRecordsForUserFilter($userId): Collection + { + return $this->addActiveExpiredUsersFilter() + ->addFieldToFilter('main_table.user_id', $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..eaf2259375f0a --- /dev/null +++ b/app/code/Magento/Security/Model/UserExpiration.php @@ -0,0 +1,62 @@ +validator = $validator; + } + + /** + * Resource initialization + * + * @return void + */ + protected function _construct() + { + $this->_init(\Magento\Security\Model\ResourceModel\UserExpiration::class); + } + + /** + * TODO: remove and use a plugin on UserValidationRules + */ +// protected function _getValidationRulesBeforeSave() +// { +// return $this->validator; +// } +} 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..4b82f2ab43f07 --- /dev/null +++ b/app/code/Magento/Security/Model/UserExpiration/Validator.php @@ -0,0 +1,45 @@ +_clearMessages(); + $messages = []; + $expiresAt = $value; + $label = 'Expiration date'; + if (\Zend_Validate::is($expiresAt, 'NotEmpty')) { + $currentTime = new \DateTime(); + $expiresAt = new \DateTime($expiresAt); + + if ($expiresAt < $currentTime) { + $messages['expires_at'] = __('"%1" must be later than the current 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..bd10b55e1b36d --- /dev/null +++ b/app/code/Magento/Security/Model/UserExpirationManager.php @@ -0,0 +1,179 @@ +dateTime = $dateTime; + $this->securityConfig = $securityConfig; + $this->adminSessionInfoCollectionFactory = $adminSessionInfoCollectionFactory; + $this->authSession = $authSession; + $this->userExpirationCollectionFactory = $userExpirationCollectionFactory; + $this->userCollectionFactory = $userCollectionFactory; + } + + /** + * Revoke admin tokens for expired users. + * TODO: any better way than looping? + * TODO: remove + * @param \Magento\User\Model\User $user + * @throws \Exception + */ + public function deactivateExpiredUser(\Magento\User\Model\User $user): void + { + $currentSessions = $this->getSessionsForUser($user); + $currentSessions->setDataToAll('status', \Magento\Security\Model\AdminSessionInfo::LOGGED_OUT) + ->save(); + $user + ->setIsActive(0) + ->save(); + // TODO: remove expires_at record from new table + } + + /** + * Deactivate expired user accounts and invalidate their sessions. + * + * @param array|null $userIds + */ + public function deactivateExpiredUsers(?array $userIds = null): void + { + /** @var ExpiredUsersCollection $expiredRecords */ + $expiredRecords = $this->userExpirationCollectionFactory->create()->addActiveExpiredUsersFilter(); + if ($userIds != null) { + $expiredRecords->addUserIdsFilter($userIds); + } + + 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()); + $currentSessions->setDataToAll('status', \Magento\Security\Model\AdminSessionInfo::LOGGED_OUT) + ->save(); + } + + // delete expired records + $expiredRecordIds = $expiredRecords->getAllIds(); + $expiredRecords->walk('delete'); + + // set user is_active to 0 + $users = $this->userCollectionFactory->create() + ->addFieldToFilter('main_table.user_id', ['in' => $expiredRecordIds]); + $users->setDataToAll('is_active', 0)->save(); + } + + /** + * Get sessions for the given user. + * TODO: remove + * @param \Magento\User\Model\User $user + * @return ResourceModel\AdminSessionInfo\Collection + */ + private function getSessionsForUser(\Magento\User\Model\User $user) + { + $collection = $this->adminSessionInfoCollectionFactory->create(); + $collection + ->filterByUser($user->getId(), \Magento\Security\Model\AdminSessionInfo::LOGGED_IN) + ->filterExpiredSessions($this->securityConfig->getAdminSessionLifetime()) + ->loadData(); + + return $collection; + } + + /** + * Check if the given user is expired. + * // TODO: check users expired an hour ago (timezone stuff) + * @param \Magento\User\Model\User $user + * @return bool + */ + public function userIsExpired(\Magento\User\Model\User $user): bool + { + $isExpired = false; + $expiredRecord = $this->userExpirationCollectionFactory->create() + ->addExpiredRecordsForUserFilter($user->getId()) + ->getFirstItem(); // expiresAt: 1561824907, current timestamp: 1561824932 + if ($expiredRecord && $expiredRecord->getId()) { + //$expiresAt = $this->dateTime->gmtTimestamp($expiredRecord->getExpiredAt()); + $expiresAt = $this->dateTime->timestamp($expiredRecord->getExpiresAt()); + $isExpired = $expiresAt < $this->dateTime->gmtTimestamp(); + } + + return $isExpired; + } + + /** + * Check if the current user is expired and, if so, revoke their admin token. + */ + // private function revokeExpiredAdminUser() + // { + // $expiresAt = $this->dateTime->gmtTimestamp($this->authSession->getUser()->getExpiresAt()); + // if ($expiresAt < $this->dateTime->gmtTimestamp()) { + // $currentSessions = $this->getSessionsForCurrentUser(); + // $currentSessions->setDataToAll('status', \Magento\Security\Model\AdminSessionInfo::LOGGED_OUT) + // ->save(); + // $this->authSession->getUser() + // ->setIsActive(0) + // ->setExpiresAt(null) + // ->save(); + // } + // } +} diff --git a/app/code/Magento/Security/Observer/AfterAdminUserLoad.php b/app/code/Magento/Security/Observer/AfterAdminUserLoad.php new file mode 100644 index 0000000000000..4bc6804276c5c --- /dev/null +++ b/app/code/Magento/Security/Observer/AfterAdminUserLoad.php @@ -0,0 +1,54 @@ +userExpirationFactory = $userExpirationFactory; + $this->userExpirationResource = $userExpirationResource; + } + + /** + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + /* @var $user \Magento\User\Model\User */ + $user = $observer->getEvent()->getObject(); + if ($user->getId()) { + /** @var \Magento\Security\Model\UserExpiration $userExpiration */ + $userExpiration = $this->userExpirationFactory->create(); + $this->userExpirationResource->load($userExpiration, $user->getId()); + if ($userExpiration->getExpiresAt()) { + $user->setExpiresAt($userExpiration->getExpiresAt()); + } + } + } +} diff --git a/app/code/Magento/Security/Observer/AfterAdminUserSave.php b/app/code/Magento/Security/Observer/AfterAdminUserSave.php new file mode 100644 index 0000000000000..37fb6af3ccaa6 --- /dev/null +++ b/app/code/Magento/Security/Observer/AfterAdminUserSave.php @@ -0,0 +1,65 @@ +userExpirationFactory = $userExpirationFactory; + $this->userExpirationResource = $userExpirationResource; + } + + /** + * @param Observer $observer + * @return void + */ + 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/Observer/BeforeAdminUserAuthenticate.php b/app/code/Magento/Security/Observer/BeforeAdminUserAuthenticate.php new file mode 100644 index 0000000000000..1c888ec511646 --- /dev/null +++ b/app/code/Magento/Security/Observer/BeforeAdminUserAuthenticate.php @@ -0,0 +1,61 @@ +userExpirationManager = $userExpirationManager; + $this->user = $user; + } + + /** + * @param Observer $observer + * @return void + * @throws AuthenticationException + */ + public function execute(Observer $observer) + { + $username = $observer->getEvent()->getUsername(); + /** @var \Magento\User\Model\User $user */ + $user = $this->user->loadByUsername($username); + + if ($this->userExpirationManager->userIsExpired($user)) { + $this->userExpirationManager->deactivateExpiredUsers([$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/Test/Unit/Model/AdminSessionsManagerTest.php b/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php index d7495cd9ce173..a062cad9d61da 100644 --- a/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php @@ -50,7 +50,7 @@ class AdminSessionsManagerTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ protected $objectManager; - /* + /** * @var RemoteAddress */ protected $remoteAddressMock; 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..4498e514e45d3 --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php @@ -0,0 +1,63 @@ +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..18e1eb3438bae --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php @@ -0,0 +1,49 @@ +validator = $objectManager->getObject(\Magento\Security\Model\UserExpiration\Validator::class); + } + + public function testWithPastDate() + { + $testDate = new \DateTime(); + $testDate->modify('-10 days'); + static::assertFalse($this->validator->isValid($testDate->format('Y-m-d H:i:s'))); + static::assertContains( + '"Expiration date" must be later than the current date.', + $this->validator->getMessages() + ); + } + + public function testWithFutureDate() + { + $testDate = new \DateTime(); + $testDate->modify('+10 days'); + static::assertTrue($this->validator->isValid($testDate->format('Y-m-d H:i:s'))); + static::assertEquals([], $this->validator->getMessages()); + } +} diff --git a/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserLoadTest.php b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserLoadTest.php new file mode 100644 index 0000000000000..dfcbcad5780b8 --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserLoadTest.php @@ -0,0 +1,120 @@ +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'] + ); + $this->observer = $this->objectManager->getObject( + \Magento\Security\Observer\AfterAdminUserLoad::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', 'setExpiresAt']); + $this->userExpirationMock = $this->createPartialMock( + \Magento\Security\Model\UserExpiration::class, + ['getExpiresAt'] + ); + } + + public function testWithExpiredUser() + { + $userId = '123'; + $testDate = new \DateTime(); + $testDate->modify('+10 days'); + $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->userExpirationFactoryMock->expects(static::once()) + ->method('create') + ->willReturn($this->userExpirationMock); + $this->userExpirationResourceMock->expects(static::once()) + ->method('load') + ->willReturn($this->userExpirationMock); + $this->userExpirationMock->expects(static::exactly(2)) + ->method('getExpiresAt') + ->willReturn($testDate->format('Y-m-d H:i:s')); + $this->userMock->expects(static::once()) + ->method('setExpiresAt') + ->willReturn($this->userMock); + $this->observer->execute($this->eventObserverMock); + } + + public function testWithNonExpiredUser() + { + $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(2))->method('getId')->willReturn($userId); + $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('getExpiresAt') + ->willReturn(null); + $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..439ec3f88548b --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php @@ -0,0 +1,159 @@ +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/Test/Unit/Observer/BeforeAdminUserAuthenticateTest.php b/app/code/Magento/Security/Test/Unit/Observer/BeforeAdminUserAuthenticateTest.php new file mode 100644 index 0000000000000..9df2a133ff2a9 --- /dev/null +++ b/app/code/Magento/Security/Test/Unit/Observer/BeforeAdminUserAuthenticateTest.php @@ -0,0 +1,119 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->userExpirationManagerMock = $this->createPartialMock( + \Magento\Security\Model\UserExpirationManager::class, + ['userIsExpired', 'deactivateExpiredUsers'] + ); + $this->userMock = $this->createPartialMock(\Magento\User\Model\User::class, ['loadByUsername', 'getId']); + $this->observer = $this->objectManager->getObject( + \Magento\Security\Observer\BeforeAdminUserAuthenticate::class, + [ + 'userExpirationManager' => $this->userExpirationManagerMock, + 'user' => $this->userMock, + ] + ); + $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\Model\UserExpiration::class, + ['getId', 'getExpiresAt', 'setId', 'setExpiresAt'] + ); + } + + /** + * @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->userMock->expects(static::once())->method('loadByUsername')->willReturn($this->userMock); + + $this->userExpirationManagerMock->expects(static::once()) + ->method('userIsExpired') + ->with($this->userMock) + ->willReturn(true); + $this->userMock->expects(static::once())->method('getId')->willReturn($adminUserId); + $this->userExpirationManagerMock->expects(static::once()) + ->method('deactivateExpiredUsers') + ->with([$adminUserId]) + ->willReturn(null); + $this->observer->execute($this->eventObserverMock); + } + + public function testWithNonExpiredUser() + { + $username = 'testuser'; + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getUsername')->willReturn($username); + $this->userMock->expects(static::once())->method('loadByUsername')->willReturn($this->userMock); + + $this->userExpirationManagerMock->expects(static::once()) + ->method('userIsExpired') + ->with($this->userMock) + ->willReturn(false); + $this->observer->execute($this->eventObserverMock); + } +} diff --git a/app/code/Magento/Security/etc/adminhtml/di.xml b/app/code/Magento/Security/etc/adminhtml/di.xml index 79477e9443097..8cf5b610ef3c5 100644 --- a/app/code/Magento/Security/etc/adminhtml/di.xml +++ b/app/code/Magento/Security/etc/adminhtml/di.xml @@ -15,6 +15,9 @@ + + + Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST diff --git a/app/code/Magento/Security/etc/adminhtml/events.xml b/app/code/Magento/Security/etc/adminhtml/events.xml new file mode 100644 index 0000000000000..1bd8e9807c05f --- /dev/null +++ b/app/code/Magento/Security/etc/adminhtml/events.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/Security/etc/crontab.xml b/app/code/Magento/Security/etc/crontab.xml index a30a43730e6fa..203cf5c2a2389 100644 --- a/app/code/Magento/Security/etc/crontab.xml +++ b/app/code/Magento/Security/etc/crontab.xml @@ -13,5 +13,8 @@ 0 0 * * * + + 0 * * * * + diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml index ce7143582ce69..ec0f1d72c604e 100644 --- a/app/code/Magento/Security/etc/db_schema.xml +++ b/app/code/Magento/Security/etc/db_schema.xml @@ -55,4 +55,15 @@ + + + + + + + +
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/i18n/en_US.csv b/app/code/Magento/Security/i18n/en_US.csv index 0cf998b21a1c8..4329e0747d08e 100644 --- a/app/code/Magento/Security/i18n/en_US.csv +++ b/app/code/Magento/Security/i18n/en_US.csv @@ -26,3 +26,4 @@ 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." diff --git a/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php b/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php index 369d71ddbff9b..21a9f6d942dc3 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php @@ -59,6 +59,25 @@ public function testCreateAdminAccessToken() $this->assertEquals($accessToken, $token); } + /** + * TODO: fix failing test + * @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/AdminSessionsManagerTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/AdminSessionsManagerTest.php index a201dbfdf1b03..74cefb9d41a59 100644 --- a/dev/tests/integration/testsuite/Magento/Security/Model/AdminSessionsManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Security/Model/AdminSessionsManagerTest.php @@ -3,8 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Security\Model; +/** + * Class AdminSessionsManagerTest + * TODO: test AdminSessionsManager::processLogin + * TODO: test AdminSessionsManager::processProlong + * + * @package Magento\Security\Model + */ class AdminSessionsManagerTest extends \PHPUnit\Framework\TestCase { /** @@ -61,8 +69,8 @@ protected function setUp() protected function tearDown() { $this->auth = null; - $this->authSession = null; - $this->adminSessionInfo = null; + $this->authSession = null; + $this->adminSessionInfo = null; $this->adminSessionsManager = null; $this->objectManager = null; parent::tearDown(); @@ -125,10 +133,23 @@ public function testTerminateOtherSessionsProcessLogin() $session->load('669e2e3d752e8', 'session_id'); $this->assertEquals( AdminSessionInfo::LOGGED_OUT_BY_LOGIN, - (int) $session->getStatus() + (int)$session->getStatus() ); } + /** + * Test that expired users cannot login. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedException \Magento\Framework\Exception\AuthenticationException + * @magentoDataFixture Magento/Security/_files/expired_users.php + * @magentoDbIsolation enabled + */ + public function testExpiredUserProcessLogin() + { + static::markTestSkipped('to implement'); + } + /** * Test if current session is retrieved * 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..63f1c377f99cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php @@ -0,0 +1,62 @@ +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 testExpiredActiveUsersFilter() + { + /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */ + $collectionModel = $this->collectionModelFactory->create(); + $collectionModel->addActiveExpiredUsersFilter(); + static::assertGreaterThanOrEqual(1, $collectionModel->getSize()); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testGetExpiredRecordsForUser() + { + $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::assertGreaterThanOrEqual(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..1840748444842 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php @@ -0,0 +1,148 @@ +getUserFromUserName($adminUserNameFromFixture); + /** @var \Magento\Security\Model\UserExpirationManager $userExpirationManager */ + $userExpirationManager = Bootstrap::getObjectManager() + ->create(\Magento\Security\Model\UserExpirationManager::class); + static::assertTrue($userExpirationManager->userIsExpired($user)); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testDeactivateExpiredUsersWithExpiredUser() + { + $adminUserNameFromFixture = 'adminUserExpired'; + + list($user, $token, $expiredUserModel) = $this->setupCronTests($adminUserNameFromFixture); + + static::assertEquals(0, $user->getIsActive()); + static::assertEquals(null, $token->getId()); + static::assertNull($expiredUserModel->getId()); + } + + /** + * @magentoDataFixture Magento/Security/_files/expired_users.php + */ + public function testDeactivateExpiredUsersWithNonExpiredUser() + { + $adminUserNameFromFixture = 'adminUserNotExpired'; + // log them in + $adminToken = $this->createToken($adminUserNameFromFixture); + + list($user, $token, $expiredUserModel) = $this->setupCronTests($adminUserNameFromFixture); + + static::assertEquals(1, $user->getIsActive()); + static::assertNotNull($token->getId()); + static::assertEquals($expiredUserModel->getUserId(), $user->getId()); + } + + /** + * @param string $adminUserNameFromFixture + * @return array + */ + private function setupCronTests(string $adminUserNameFromFixture): array + { + // TODO: set the user expired after calling this + // TODO: use this to test the observer with exception: + // Magento\Framework\Exception\Plugin\AuthenticationException : The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later. + //$adminToken = $this->createToken($adminUserNameFromFixture); // TODO: this logs the user in, which kicks off the deactivate call + + /** @var \Magento\Security\Model\UserExpirationManager $job */ + $userExpirationManager = Bootstrap::getObjectManager() + ->create(\Magento\Security\Model\UserExpirationManager::class); + $userExpirationManager->deactivateExpiredUsers(); + + /** @var \Magento\User\Model\User $user */ + $user = $this->getUserFromUserName($adminUserNameFromFixture); + + // this is for the API only + $oauthToken = $this->getOauthTokenByUser($user); + $expiredUserModel = $this->getExpiredUserModelByUser($user); + + return [$user, $oauthToken, $expiredUserModel]; + } + + /** + * TODO: this calls user->login and throws an AuthenticationException + * + * @param string $adminUserNameFromFixture + * @return string + * @throws \Magento\Framework\Exception\AuthenticationException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function createToken(string $adminUserNameFromFixture): string + { + /** @var \Magento\Integration\Api\AdminTokenServiceInterface $tokenService */ + $tokenService = Bootstrap::getObjectManager()->get(\Magento\Integration\Api\AdminTokenServiceInterface::class); + $token = $tokenService->createAdminAccessToken( + $adminUserNameFromFixture, + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + + return $token; + } + + /** + * @param string $adminUserNameFromFixture + * @return \Magento\User\Model\User + */ + private function getUserFromUserName(string $adminUserNameFromFixture): \Magento\User\Model\User + { + /** @var \Magento\User\Model\User $user */ + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + return $user; + } + + /** + * @param \Magento\User\Model\User $user + * @return \Magento\Integration\Model\Oauth\Token + */ + private function getOauthTokenByUser(\Magento\User\Model\User $user): \Magento\Integration\Model\Oauth\Token + { + /** @var \Magento\Integration\Model\Oauth\Token $tokenModel */ + $tokenModel = Bootstrap::getObjectManager()->get(\Magento\Integration\Model\Oauth\Token::class); + $oauthToken = $tokenModel->loadByAdminId($user->getId()); + return $oauthToken; + } + + /** + * @param \Magento\User\Model\User $user + * @return UserExpiration + */ + private function getExpiredUserModelByUser(\Magento\User\Model\User $user): \Magento\Security\Model\UserExpiration + { + /** @var \Magento\Security\Model\UserExpiration $expiredUserModel */ + $expiredUserModel = Bootstrap::getObjectManager()->get(\Magento\Security\Model\UserExpiration::class); + $expiredUserModel->load($user->getId()); + return $expiredUserModel; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserLoadTest.php b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserLoadTest.php new file mode 100644 index 0000000000000..0fcd90913e562 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserLoadTest.php @@ -0,0 +1,49 @@ +create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + $userId = $user->getId(); + $loadedUser = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $loadedUser->load($userId); + static::assertNotNull($loadedUser->getExpiresAt()); + } + + /** + * @magentoDataFixture Magento/User/_files/dummy_user.php + */ + public function testWithNonExpiredUser() + { + $adminUserNameFromFixture = 'dummy_username'; + $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $user->loadByUsername($adminUserNameFromFixture); + $userId = $user->getId(); + $loadedUser = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $loadedUser->load($userId); + static::assertNull($loadedUser->getExpiresAt()); + } + +} 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..6032b651b4dc3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php @@ -0,0 +1,101 @@ +getExpiresDateTime(); + $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->getExpiresDateTime(); + $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()); + } + + /** + * @return string + * @throws \Exception + */ + private function getExpiresDateTime() + { + $testDate = new \DateTime(); + $testDate->modify('+20 days'); + return $testDate->format('Y-m-d H:i:s'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Observer/BeforeAdminUserAuthenticateTest.php b/dev/tests/integration/testsuite/Magento/Security/Observer/BeforeAdminUserAuthenticateTest.php new file mode 100644 index 0000000000000..db7a83877d1b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Observer/BeforeAdminUserAuthenticateTest.php @@ -0,0 +1,47 @@ +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/_files/expired_users.php b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php new file mode 100644 index 0000000000000..5a3971cd9d0bb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php @@ -0,0 +1,71 @@ +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(); + +// TODO: remove +// need to bypass model validation to set expired date +//$pastDate = new \DateTime(); +//$pastDate->modify('-10 days'); +//$resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class); +///** @var \Magento\Framework\DB\Adapter\AdapterInterface $conn */ +//$conn = $resource->getConnection(\Magento\Framework\App\ResourceConnection::DEFAULT_CONNECTION); +//$conn->update( +// $resource->getTableName('admin_user_expiration'), +// ['expires_at' => $pastDate->format('Y-m-d H:i:s')], +// ['user_id = ?' => (int)$userModelExpired->getId()] +//);