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()]
+//);