diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml
index 93ee464a17efa..40f80ce52c2de 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml
@@ -35,8 +35,9 @@
-
+
+
diff --git a/app/code/Magento/Security/Api/Data/UserExpirationInterface.php b/app/code/Magento/Security/Api/Data/UserExpirationInterface.php
new file mode 100644
index 0000000000000..4f602c8b58a87
--- /dev/null
+++ b/app/code/Magento/Security/Api/Data/UserExpirationInterface.php
@@ -0,0 +1,67 @@
+localeDate = $localeDate;
+ $this->userExpirationResource = $userExpirationResource;
+ $this->userExpirationFactory = $userExpirationFactory;
+ }
+
+ /**
+ * Add the `expires_at` field to the admin user edit form.
+ *
+ * @param \Magento\User\Block\User\Edit\Tab\Main $subject
+ * @param \Closure $proceed
+ * @return mixed
+ */
+ public function aroundGetFormHtml(
+ \Magento\User\Block\User\Edit\Tab\Main $subject,
+ \Closure $proceed
+ ) {
+ /** @var \Magento\Framework\Data\Form $form */
+ $form = $subject->getForm();
+ if (is_object($form)) {
+ $dateFormat = $this->localeDate->getDateFormat(
+ \IntlDateFormatter::MEDIUM
+ );
+ $timeFormat = $this->localeDate->getTimeFormat(
+ \IntlDateFormatter::MEDIUM
+ );
+ $fieldset = $form->getElement('base_fieldset');
+ $userIdField = $fieldset->getElements()->searchById('user_id');
+ $userExpirationValue = null;
+ if ($userIdField) {
+ $userId = $userIdField->getValue();
+ $userExpirationValue = $this->loadUserExpirationByUserId($userId);
+ }
+ $fieldset->addField(
+ 'expires_at',
+ 'date',
+ [
+ 'name' => 'expires_at',
+ 'label' => __('Expiration Date'),
+ 'title' => __('Expiration Date'),
+ 'date_format' => $dateFormat,
+ 'time_format' => $timeFormat,
+ 'class' => 'validate-date',
+ 'value' => $userExpirationValue,
+ ]
+ );
+
+ $subject->setForm($form);
+ }
+
+ return $proceed();
+ }
+
+ /**
+ * Loads a user expiration record by user ID.
+ *
+ * @param string $userId
+ * @return string
+ */
+ private function loadUserExpirationByUserId($userId)
+ {
+ /** @var \Magento\Security\Model\UserExpiration $userExpiration */
+ $userExpiration = $this->userExpirationFactory->create();
+ $this->userExpirationResource->load($userExpiration, $userId);
+ return $userExpiration->getExpiresAt();
+ }
+}
diff --git a/app/code/Magento/Security/Model/Plugin/AuthSession.php b/app/code/Magento/Security/Model/Plugin/AuthSession.php
index 01203caaa31cd..6dc5e796d8950 100644
--- a/app/code/Magento/Security/Model/Plugin/AuthSession.php
+++ b/app/code/Magento/Security/Model/Plugin/AuthSession.php
@@ -7,6 +7,7 @@
use Magento\Backend\Model\Auth\Session;
use Magento\Security\Model\AdminSessionsManager;
+use Magento\Security\Model\UserExpirationManager;
/**
* Magento\Backend\Model\Auth\Session decorator
@@ -33,22 +34,32 @@ class AuthSession
*/
protected $securityCookie;
+ /**
+ * @var UserExpirationManager
+ */
+ private $userExpirationManager;
+
/**
* @param \Magento\Framework\App\RequestInterface $request
* @param \Magento\Framework\Message\ManagerInterface $messageManager
* @param AdminSessionsManager $sessionsManager
* @param \Magento\Security\Model\SecurityCookie $securityCookie
+ * @param UserExpirationManager|null $userExpirationManager
*/
public function __construct(
\Magento\Framework\App\RequestInterface $request,
\Magento\Framework\Message\ManagerInterface $messageManager,
AdminSessionsManager $sessionsManager,
- \Magento\Security\Model\SecurityCookie $securityCookie
+ \Magento\Security\Model\SecurityCookie $securityCookie,
+ \Magento\Security\Model\UserExpirationManager $userExpirationManager = null
) {
$this->request = $request;
$this->messageManager = $messageManager;
$this->sessionsManager = $sessionsManager;
$this->securityCookie = $securityCookie;
+ $this->userExpirationManager = $userExpirationManager ?:
+ \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Security\Model\UserExpirationManager::class);
}
/**
@@ -64,6 +75,11 @@ public function aroundProlong(Session $session, \Closure $proceed)
$session->destroy();
$this->addUserLogoutNotification();
return null;
+ } elseif ($this->userExpirationManager->isUserExpired($session->getUser()->getId())) {
+ $this->userExpirationManager->deactivateExpiredUsersById([$session->getUser()->getId()]);
+ $session->destroy();
+ $this->addUserLogoutNotification();
+ return null;
}
$result = $proceed();
$this->sessionsManager->processProlong();
diff --git a/app/code/Magento/Security/Model/Plugin/UserValidationRules.php b/app/code/Magento/Security/Model/Plugin/UserValidationRules.php
new file mode 100644
index 0000000000000..7fddbb21200f4
--- /dev/null
+++ b/app/code/Magento/Security/Model/Plugin/UserValidationRules.php
@@ -0,0 +1,42 @@
+validator = $validator;
+ }
+
+ /**
+ * Add the Expires At validator to user validation rules.
+ *
+ * @param \Magento\User\Model\UserValidationRules $userValidationRules
+ * @param \Magento\Framework\Validator\DataObject $result
+ * @return \Magento\Framework\Validator\DataObject
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterAddUserInfoRules(\Magento\User\Model\UserValidationRules $userValidationRules, $result)
+ {
+ return $result->addRule($this->validator, 'expires_at');
+ }
+}
diff --git a/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php
new file mode 100644
index 0000000000000..71a331a178006
--- /dev/null
+++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php
@@ -0,0 +1,88 @@
+timezone = $timezone;
+ }
+
+ /**
+ * Define main table
+ *
+ * @return void
+ */
+ protected function _construct()
+ {
+ $this->_init('admin_user_expiration', 'user_id');
+ }
+
+ /**
+ * Convert to UTC time.
+ *
+ * @param \Magento\Framework\Model\AbstractModel $userExpiration
+ * @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $userExpiration)
+ {
+ /** @var $userExpiration \Magento\Security\Model\UserExpiration */
+ $expiresAt = $userExpiration->getExpiresAt();
+ $utcValue = $this->timezone->convertConfigTimeToUtc($expiresAt);
+ $userExpiration->setExpiresAt($utcValue);
+
+ return $this;
+ }
+
+ /**
+ * Convert to store time.
+ *
+ * @param \Magento\Framework\Model\AbstractModel $userExpiration
+ * @return $this|\Magento\Framework\Model\ResourceModel\Db\AbstractDb
+ * @throws \Exception
+ */
+ protected function _afterLoad(\Magento\Framework\Model\AbstractModel $userExpiration)
+ {
+ /** @var $userExpiration \Magento\Security\Model\UserExpiration */
+ if ($userExpiration->getExpiresAt()) {
+ $storeValue = $this->timezone->date($userExpiration->getExpiresAt());
+ $userExpiration->setExpiresAt($storeValue->format('Y-m-d H:i:s'));
+ }
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php b/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php
new file mode 100644
index 0000000000000..2f2971bc90225
--- /dev/null
+++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php
@@ -0,0 +1,75 @@
+_init(
+ \Magento\Security\Model\UserExpiration::class,
+ \Magento\Security\Model\ResourceModel\UserExpiration::class
+ );
+ }
+
+ /**
+ * Filter for expired, active users.
+ *
+ * @return $this
+ */
+ public function addActiveExpiredUsersFilter(): Collection
+ {
+ $currentTime = new \DateTime();
+ $currentTime->format('Y-m-d H:i:s');
+ $this->getSelect()->joinLeft(
+ ['user' => $this->getTable('admin_user')],
+ 'main_table.user_id = user.user_id',
+ ['is_active']
+ );
+ $this->addFieldToFilter('expires_at', ['lt' => $currentTime])
+ ->addFieldToFilter('user.is_active', 1);
+
+ return $this;
+ }
+
+ /**
+ * Filter collection by user id.
+ *
+ * @param int[] $userIds
+ * @return Collection
+ */
+ public function addUserIdsFilter(array $userIds = []): Collection
+ {
+ return $this->addFieldToFilter('main_table.user_id', ['in' => $userIds]);
+ }
+
+ /**
+ * Get any expired records for the given user.
+ *
+ * @param string $userId
+ * @return Collection
+ */
+ public function addExpiredRecordsForUserFilter(string $userId): Collection
+ {
+ return $this->addActiveExpiredUsersFilter()
+ ->addFieldToFilter('main_table.user_id', (int)$userId);
+ }
+}
diff --git a/app/code/Magento/Security/Model/UserExpiration.php b/app/code/Magento/Security/Model/UserExpiration.php
new file mode 100644
index 0000000000000..e6c711b7ac049
--- /dev/null
+++ b/app/code/Magento/Security/Model/UserExpiration.php
@@ -0,0 +1,87 @@
+_init(\Magento\Security\Model\ResourceModel\UserExpiration::class);
+ }
+
+ /**
+ * `expires_at` getter.
+ *
+ * @return string
+ */
+ public function getExpiresAt()
+ {
+ return $this->getData(self::EXPIRES_AT);
+ }
+
+ /**
+ * `expires_at` setter.
+ *
+ * @param string $expiresAt
+ * @return $this
+ */
+ public function setExpiresAt($expiresAt)
+ {
+ return $this->setData(self::EXPIRES_AT, $expiresAt);
+ }
+
+ /**
+ * `user_id` getter.
+ *
+ * @return string
+ */
+ public function getUserId()
+ {
+ return $this->getData(self::USER_ID);
+ }
+
+ /**
+ * `user_id` setter.
+ *
+ * @param string $userId
+ * @return $this
+ */
+ public function setUserId($userId)
+ {
+ return $this->setData(self::USER_ID, $userId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getExtensionAttributes()
+ {
+ return $this->_getExtensionAttributes();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setExtensionAttributes(UserExpirationExtensionInterface $extensionAttributes)
+ {
+ return $this->_setExtensionAttributes($extensionAttributes);
+ }
+}
diff --git a/app/code/Magento/Security/Model/UserExpiration/Validator.php b/app/code/Magento/Security/Model/UserExpiration/Validator.php
new file mode 100644
index 0000000000000..62dbd7852ff33
--- /dev/null
+++ b/app/code/Magento/Security/Model/UserExpiration/Validator.php
@@ -0,0 +1,69 @@
+timezone = $timezone;
+ $this->dateTime = $dateTime;
+ }
+
+ /**
+ * Ensure that the given date is later than the current date.
+ *
+ * @param string $value
+ * @return bool
+ * @throws \Exception
+ */
+ public function isValid($value)
+ {
+ $this->_clearMessages();
+ $messages = [];
+ $expiresAt = $value;
+ $label = 'Expiration date';
+ if (\Zend_Validate::is($expiresAt, 'NotEmpty')) {
+ if (strtotime($expiresAt)) {
+ $currentTime = $this->dateTime->gmtTimestamp();
+ $utcExpiresAt = $this->timezone->convertConfigTimeToUtc($expiresAt);
+ $expiresAt = $this->timezone->date($utcExpiresAt)->getTimestamp();
+ if ($expiresAt < $currentTime) {
+ $messages['expires_at'] = __('"%1" must be later than the current date.', $label);
+ }
+ } else {
+ $messages['expires_at'] = __('"%1" is not a valid date.', $label);
+ }
+ }
+ $this->_addMessages($messages);
+
+ return empty($messages);
+ }
+}
diff --git a/app/code/Magento/Security/Model/UserExpirationManager.php b/app/code/Magento/Security/Model/UserExpirationManager.php
new file mode 100644
index 0000000000000..fe6b87de5a8ec
--- /dev/null
+++ b/app/code/Magento/Security/Model/UserExpirationManager.php
@@ -0,0 +1,153 @@
+dateTime = $dateTime;
+ $this->securityConfig = $securityConfig;
+ $this->adminSessionInfoCollectionFactory = $adminSessionInfoCollectionFactory;
+ $this->authSession = $authSession;
+ $this->userExpirationCollectionFactory = $userExpirationCollectionFactory;
+ $this->userCollectionFactory = $userCollectionFactory;
+ }
+
+ /**
+ * Deactivate expired user accounts and invalidate their sessions.
+ */
+ public function deactivateExpiredUsers(): void
+ {
+ /** @var ExpiredUsersCollection $expiredRecords */
+ $expiredRecords = $this->userExpirationCollectionFactory->create()->addActiveExpiredUsersFilter();
+ $this->processExpiredUsers($expiredRecords);
+ }
+
+ /**
+ * Deactivate specific expired users.
+ *
+ * @param array $userIds
+ */
+ public function deactivateExpiredUsersById(array $userIds): void
+ {
+ $expiredRecords = $this->userExpirationCollectionFactory->create()
+ ->addActiveExpiredUsersFilter()
+ ->addUserIdsFilter($userIds);
+ $this->processExpiredUsers($expiredRecords);
+ }
+
+ /**
+ * Deactivate expired user accounts and invalidate their sessions.
+ *
+ * @param ExpiredUsersCollection $expiredRecords
+ */
+ private function processExpiredUsers(ExpiredUsersCollection $expiredRecords): void
+ {
+ if ($expiredRecords->getSize() > 0) {
+ // get all active sessions for the users and set them to logged out
+ /** @var \Magento\Security\Model\ResourceModel\AdminSessionInfo\Collection $currentSessions */
+ $currentSessions = $this->adminSessionInfoCollectionFactory->create()
+ ->addFieldToFilter('user_id', ['in' => $expiredRecords->getAllIds()])
+ ->filterExpiredSessions($this->securityConfig->getAdminSessionLifetime());
+ /** @var \Magento\Security\Model\AdminSessionInfo $currentSession */
+ $currentSessions->setDataToAll('status', \Magento\Security\Model\AdminSessionInfo::LOGGED_OUT)
+ ->save();
+ }
+
+ // delete expired records
+ $expiredRecordIds = $expiredRecords->getAllIds();
+
+ // set user is_active to 0
+ $users = $this->userCollectionFactory->create()
+ ->addFieldToFilter('main_table.user_id', ['in' => $expiredRecordIds]);
+ $users->setDataToAll('is_active', 0)->save();
+ $expiredRecords->walk('delete');
+ }
+
+ /**
+ * Check if the given user is expired.
+ *
+ * @param string $userId
+ * @return bool
+ */
+ public function isUserExpired(string $userId): bool
+ {
+ $isExpired = false;
+ /** @var \Magento\Security\Api\Data\UserExpirationInterface $expiredRecord */
+ $expiredRecord = $this->userExpirationCollectionFactory->create()
+ ->addExpiredRecordsForUserFilter($userId)
+ ->getFirstItem();
+ if ($expiredRecord && $expiredRecord->getId()) {
+ $expiresAt = $this->dateTime->timestamp($expiredRecord->getExpiresAt());
+ $isExpired = $expiresAt < $this->dateTime->gmtTimestamp();
+ }
+
+ return $isExpired;
+ }
+}
diff --git a/app/code/Magento/Security/Observer/AdminUserAuthenticateBefore.php b/app/code/Magento/Security/Observer/AdminUserAuthenticateBefore.php
new file mode 100644
index 0000000000000..2d0f7bc0f0ac0
--- /dev/null
+++ b/app/code/Magento/Security/Observer/AdminUserAuthenticateBefore.php
@@ -0,0 +1,69 @@
+userExpirationManager = $userExpirationManager;
+ $this->userFactory = $userFactory;
+ }
+
+ /**
+ * Check for expired user when logging in.
+ *
+ * @param Observer $observer
+ * @return void
+ * @throws AuthenticationException
+ */
+ public function execute(Observer $observer)
+ {
+ $username = $observer->getEvent()->getUsername();
+ $user = $this->userFactory->create();
+ /** @var \Magento\User\Model\User $user */
+ $user->loadByUsername($username);
+
+ if ($user->getId() && $this->userExpirationManager->isUserExpired($user->getId())) {
+ $this->userExpirationManager->deactivateExpiredUsersById([$user->getId()]);
+ throw new AuthenticationException(
+ __(
+ 'The account sign-in was incorrect or your account is disabled temporarily. '
+ . 'Please wait and try again later.'
+ )
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/Security/Observer/AfterAdminUserSave.php b/app/code/Magento/Security/Observer/AfterAdminUserSave.php
new file mode 100644
index 0000000000000..d11c1bfdcdf17
--- /dev/null
+++ b/app/code/Magento/Security/Observer/AfterAdminUserSave.php
@@ -0,0 +1,76 @@
+userExpirationFactory = $userExpirationFactory;
+ $this->userExpirationResource = $userExpirationResource;
+ }
+
+ /**
+ * Save user expiration.
+ *
+ * @param Observer $observer
+ * @return void
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ */
+ public function execute(Observer $observer)
+ {
+ /* @var $user \Magento\User\Model\User */
+ $user = $observer->getEvent()->getObject();
+ if ($user->getId()) {
+ $expiresAt = $user->getExpiresAt();
+ /** @var \Magento\Security\Model\UserExpiration $userExpiration */
+ $userExpiration = $this->userExpirationFactory->create();
+ $this->userExpirationResource->load($userExpiration, $user->getId());
+
+ if (empty($expiresAt)) {
+ // delete it if the admin user clears the field
+ if ($userExpiration->getId()) {
+ $this->userExpirationResource->delete($userExpiration);
+ }
+ } else {
+ if (!$userExpiration->getId()) {
+ $userExpiration->setId($user->getId());
+ }
+ $userExpiration->setExpiresAt($expiresAt);
+ $this->userExpirationResource->save($userExpiration);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminFillInUserWithExpirationActionGroup.xml b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminFillInUserWithExpirationActionGroup.xml
new file mode 100644
index 0000000000000..d1d5427506e40
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminFillInUserWithExpirationActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Goes to the Admin Users grid page. Clicks on Create User. Fills in the provided User details with a expiration date.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserInvalidExpirationActionGroup.xml b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserInvalidExpirationActionGroup.xml
new file mode 100644
index 0000000000000..02280ed809124
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserInvalidExpirationActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Error message for saving an admin user with an invalid expiration date.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserSuccessActionGroup.xml b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserSuccessActionGroup.xml
new file mode 100644
index 0000000000000..7aed1e07112cf
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/ActionGroup/AdminSaveUserSuccessActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Success message for saving an admin user successfully.
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/Security/Test/Mftf/Section/AdminEditUserSection.xml
new file mode 100644
index 0000000000000..d7acf2466c09e
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Section/AdminEditUserSection.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Section/AdminNewUserFormSection.xml b/app/code/Magento/Security/Test/Mftf/Section/AdminNewUserFormSection.xml
new file mode 100644
index 0000000000000..1d1aba4da07dd
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Section/AdminNewUserFormSection.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml
new file mode 100644
index 0000000000000..02a8170b308d3
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml
new file mode 100644
index 0000000000000..dc971f2044760
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml
new file mode 100644
index 0000000000000..bdea845c81a56
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml
new file mode 100644
index 0000000000000..12bba27f21269
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml
new file mode 100644
index 0000000000000..c4603a88c56c4
--- /dev/null
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php
index 0f7f590b71de4..9870de8fc2cb4 100644
--- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php
+++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php
@@ -37,6 +37,12 @@ class AuthSessionTest extends \PHPUnit\Framework\TestCase
/** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */
protected $objectManager;
+ /**@var \Magento\Security\Model\UserExpirationManager */
+ protected $userExpirationManagerMock;
+
+ /**@var \Magento\User\Model\User */
+ protected $userMock;
+
/**
* Init mocks for tests
* @return void
@@ -61,20 +67,31 @@ public function setUp()
$this->securityCookieMock = $this->createPartialMock(SecurityCookie::class, ['setLogoutReasonCookie']);
- $this->authSessionMock = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['destroy']);
+ $this->authSessionMock = $this->createPartialMock(
+ \Magento\Backend\Model\Auth\Session::class,
+ ['destroy', 'getUser']
+ );
$this->currentSessionMock = $this->createPartialMock(
\Magento\Security\Model\AdminSessionInfo::class,
['isLoggedInStatus', 'getStatus', 'isActive']
);
+ $this->userExpirationManagerMock = $this->createPartialMock(
+ \Magento\Security\Model\UserExpirationManager::class,
+ ['isUserExpired', 'deactivateExpiredUsersById']
+ );
+
+ $this->userMock = $this->createMock(\Magento\User\Model\User::class);
+
$this->model = $this->objectManager->getObject(
\Magento\Security\Model\Plugin\AuthSession::class,
[
'request' => $this->requestMock,
'messageManager' => $this->messageManagerMock,
'sessionsManager' => $this->adminSessionsManagerMock,
- 'securityCookie' => $this->securityCookieMock
+ 'securityCookie' => $this->securityCookieMock,
+ 'userExpirationManager' => $this->userExpirationManagerMock,
]
);
@@ -154,6 +171,59 @@ public function testAroundProlongSessionIsNotActiveAndIsAjaxRequest()
$this->model->aroundProlong($this->authSessionMock, $proceed);
}
+ /**
+ * @return void
+ */
+ public function testAroundProlongSessionIsActiveUserIsExpired()
+ {
+ $result = 'result';
+ $errorMessage = 'Error Message';
+
+ $proceed = function () use ($result) {
+ return $result;
+ };
+
+ $adminUserId = '12345';
+ $this->currentSessionMock->expects($this->once())
+ ->method('isLoggedInStatus')
+ ->willReturn(true);
+
+ $this->authSessionMock->expects($this->exactly(2))
+ ->method('getUser')
+ ->willReturn($this->userMock);
+
+ $this->userMock->expects($this->exactly(2))
+ ->method('getId')
+ ->willReturn($adminUserId);
+
+ $this->requestMock->expects($this->once())
+ ->method('getParam')
+ ->with('isAjax')
+ ->willReturn(false);
+
+ $this->userExpirationManagerMock->expects($this->once())
+ ->method('isUserExpired')
+ ->with($adminUserId)
+ ->willReturn(true);
+
+ $this->userExpirationManagerMock->expects($this->once())
+ ->method('deactivateExpiredUsersById')
+ ->with([$adminUserId]);
+
+ $this->authSessionMock->expects($this->once())
+ ->method('destroy');
+
+ $this->adminSessionsManagerMock->expects($this->once())
+ ->method('getLogoutReasonMessage')
+ ->willReturn($errorMessage);
+
+ $this->messageManagerMock->expects($this->once())
+ ->method('addErrorMessage')
+ ->with($errorMessage);
+
+ $this->model->aroundProlong($this->authSessionMock, $proceed);
+ }
+
/**
* @return void
*/
@@ -164,10 +234,24 @@ public function testAroundProlongSessionIsActive()
return $result;
};
+ $adminUserId = '12345';
$this->currentSessionMock->expects($this->any())
->method('isLoggedInStatus')
->willReturn(true);
+ $this->authSessionMock->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->userMock);
+
+ $this->userMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($adminUserId);
+
+ $this->userExpirationManagerMock->expects($this->once())
+ ->method('isUserExpired')
+ ->with($adminUserId)
+ ->willReturn(false);
+
$this->adminSessionsManagerMock->expects($this->any())
->method('processProlong');
diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php
new file mode 100644
index 0000000000000..00b3356c2e11d
--- /dev/null
+++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/UserValidationRulesTest.php
@@ -0,0 +1,61 @@
+createMock(\Magento\Security\Model\UserExpiration\Validator::class);
+ $this->userValidationRules = $this->createMock(\Magento\User\Model\UserValidationRules::class);
+ $this->rules = $objectManager->getObject(\Magento\User\Model\UserValidationRules::class);
+ $this->validator = $this->createMock(\Magento\Framework\Validator\DataObject::class);
+ $this->plugin =
+ $objectManager->getObject(
+ \Magento\Security\Model\Plugin\UserValidationRules::class,
+ ['validator' => $userExpirationValidator]
+ );
+ }
+
+ public function testAfterAddUserInfoRules()
+ {
+ $this->validator->expects(static::exactly(5))->method('addRule')->willReturn($this->validator);
+ static::assertSame($this->validator, $this->rules->addUserInfoRules($this->validator));
+ static::assertSame($this->validator, $this->callAfterAddUserInfoRulesPlugin($this->validator));
+ }
+
+ protected function callAfterAddUserInfoRulesPlugin($validator)
+ {
+ return $this->plugin->afterAddUserInfoRules($this->userValidationRules, $validator);
+ }
+}
diff --git a/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php b/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php
new file mode 100644
index 0000000000000..28541231cb123
--- /dev/null
+++ b/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php
@@ -0,0 +1,99 @@
+dateTimeMock =
+ $this->createPartialMock(\Magento\Framework\Stdlib\DateTime\DateTime::class, ['gmtTimestamp']);
+ $this->timezoneMock =
+ $this->createPartialMock(
+ \Magento\Framework\Stdlib\DateTime\Timezone::class,
+ ['date', 'convertConfigTimeToUtc']
+ );
+ $this->validator = $objectManager->getObject(
+ \Magento\Security\Model\UserExpiration\Validator::class,
+ ['dateTime' => $this->dateTimeMock, 'timezone' => $this->timezoneMock]
+ );
+ }
+
+ public function testWithInvalidDate()
+ {
+ $expireDate = 'invalid_date';
+
+ static::assertFalse($this->validator->isValid($expireDate));
+ static::assertContains(
+ '"Expiration date" is not a valid date.',
+ $this->validator->getMessages()
+ );
+ }
+
+ public function testWithPastDate()
+ {
+ /** @var \DateTime|\PHPUnit_Framework_MockObject_MockObject $dateObject */
+ $dateObject = $this->createMock(\DateTime::class);
+ $this->timezoneMock->expects(static::once())
+ ->method('date')
+ ->will(static::returnValue($dateObject));
+
+ $currentDate = new \DateTime();
+ $currentDate = $currentDate->getTimestamp();
+ $expireDate = new \DateTime();
+ $expireDate->modify('-10 days');
+
+ $this->dateTimeMock->expects(static::once())->method('gmtTimestamp')->willReturn($currentDate);
+ $this->timezoneMock->expects(static::once())->method('date')->willReturn($expireDate);
+ $dateObject->expects(static::once())->method('getTimestamp')->willReturn($expireDate->getTimestamp());
+ static::assertFalse($this->validator->isValid($expireDate->format('Y-m-d H:i:s')));
+ static::assertContains(
+ '"Expiration date" must be later than the current date.',
+ $this->validator->getMessages()
+ );
+ }
+
+ public function testWithFutureDate()
+ {
+ /** @var \DateTime|\PHPUnit_Framework_MockObject_MockObject $dateObject */
+ $dateObject = $this->createMock(\DateTime::class);
+ $this->timezoneMock->expects(static::once())
+ ->method('date')
+ ->will(static::returnValue($dateObject));
+ $currentDate = new \DateTime();
+ $currentDate = $currentDate->getTimestamp();
+ $expireDate = new \DateTime();
+ $expireDate->modify('+10 days');
+
+ $this->dateTimeMock->expects(static::once())->method('gmtTimestamp')->willReturn($currentDate);
+ $this->timezoneMock->expects(static::once())->method('date')->willReturn($expireDate);
+ $dateObject->expects(static::once())->method('getTimestamp')->willReturn($expireDate->getTimestamp());
+ static::assertTrue($this->validator->isValid($expireDate->format('Y-m-d H:i:s')));
+ static::assertEquals([], $this->validator->getMessages());
+ }
+}
diff --git a/app/code/Magento/Security/Test/Unit/Observer/AdminUserAuthenticateBeforeTest.php b/app/code/Magento/Security/Test/Unit/Observer/AdminUserAuthenticateBeforeTest.php
new file mode 100644
index 0000000000000..3f5717abe2a2c
--- /dev/null
+++ b/app/code/Magento/Security/Test/Unit/Observer/AdminUserAuthenticateBeforeTest.php
@@ -0,0 +1,133 @@
+objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->userExpirationManagerMock = $this->createPartialMock(
+ \Magento\Security\Model\UserExpirationManager::class,
+ ['isUserExpired', 'deactivateExpiredUsersById']
+ );
+ $this->userFactoryMock = $this->createPartialMock(\Magento\User\Model\UserFactory::class, ['create']);
+ $this->userMock = $this->createPartialMock(\Magento\User\Model\User::class, ['loadByUsername', 'getId']);
+ $this->observer = $this->objectManager->getObject(
+ \Magento\Security\Observer\AdminUserAuthenticateBefore::class,
+ [
+ 'userExpirationManager' => $this->userExpirationManagerMock,
+ 'userFactory' => $this->userFactoryMock,
+ ]
+ );
+ $this->eventObserverMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']);
+ $this->eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getUsername']);
+ $this->userExpirationMock = $this->createPartialMock(
+ \Magento\Security\Api\Data\UserExpirationInterface::class,
+ [
+ 'getUserId',
+ 'getExpiresAt',
+ 'setUserId',
+ 'setExpiresAt',
+ 'getExtensionAttributes',
+ 'setExtensionAttributes'
+ ]
+ );
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\Plugin\AuthenticationException
+ * @expectedExceptionMessage The account sign-in was incorrect or your account is disabled temporarily.
+ * Please wait and try again later
+ */
+ public function testWithExpiredUser()
+ {
+ $adminUserId = '123';
+ $username = 'testuser';
+ $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock);
+ $this->eventMock->expects(static::once())->method('getUsername')->willReturn($username);
+ $this->userFactoryMock->expects(static::once())->method('create')->willReturn($this->userMock);
+ $this->userMock->expects(static::once())->method('loadByUsername')->willReturnSelf();
+
+ $this->userExpirationManagerMock->expects(static::once())
+ ->method('isUserExpired')
+ ->with($adminUserId)
+ ->willReturn(true);
+ $this->userMock->expects(static::exactly(3))->method('getId')->willReturn($adminUserId);
+ $this->userExpirationManagerMock->expects(static::once())
+ ->method('deactivateExpiredUsersById')
+ ->with([$adminUserId])
+ ->willReturn(null);
+ $this->observer->execute($this->eventObserverMock);
+ }
+
+ public function testWithNonExpiredUser()
+ {
+ $adminUserId = '123';
+ $username = 'testuser';
+ $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock);
+ $this->eventMock->expects(static::once())->method('getUsername')->willReturn($username);
+ $this->userFactoryMock->expects(static::once())->method('create')->willReturn($this->userMock);
+ $this->userMock->expects(static::once())->method('loadByUsername')->willReturnSelf();
+ $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($adminUserId);
+ $this->userExpirationManagerMock->expects(static::once())
+ ->method('isUserExpired')
+ ->with($adminUserId)
+ ->willReturn(false);
+ $this->observer->execute($this->eventObserverMock);
+ }
+}
diff --git a/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php
new file mode 100644
index 0000000000000..85505632e1eb6
--- /dev/null
+++ b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php
@@ -0,0 +1,157 @@
+objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->userExpirationFactoryMock = $this->createMock(\Magento\Security\Model\UserExpirationFactory::class);
+ $this->userExpirationResourceMock = $this->createPartialMock(
+ \Magento\Security\Model\ResourceModel\UserExpiration::class,
+ ['load', 'save', 'delete']
+ );
+ $this->observer = $this->objectManager->getObject(
+ \Magento\Security\Observer\AfterAdminUserSave::class,
+ [
+ 'userExpirationFactory' => $this->userExpirationFactoryMock,
+ 'userExpirationResource' => $this->userExpirationResourceMock,
+ ]
+ );
+ $this->eventObserverMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']);
+ $this->eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getObject']);
+ $this->userMock = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', 'getExpiresAt']);
+ $this->userExpirationMock = $this->createPartialMock(
+ \Magento\Security\Model\UserExpiration::class,
+ ['getId', 'getExpiresAt', 'setId', 'setExpiresAt']
+ );
+ }
+
+ public function testSaveNewUserExpiration()
+ {
+ $userId = '123';
+ $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock);
+ $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock);
+ $this->userMock->expects(static::exactly(3))->method('getId')->willReturn($userId);
+ $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn($this->getExpiresDateTime());
+ $this->userExpirationFactoryMock->expects(static::once())->method('create')
+ ->willReturn($this->userExpirationMock);
+ $this->userExpirationResourceMock->expects(static::once())->method('load')
+ ->willReturn($this->userExpirationMock);
+
+ $this->userExpirationMock->expects(static::once())->method('getId')->willReturn(null);
+ $this->userExpirationMock->expects(static::once())->method('setId')->willReturn($this->userExpirationMock);
+ $this->userExpirationMock->expects(static::once())->method('setExpiresAt')
+ ->willReturn($this->userExpirationMock);
+ $this->userExpirationResourceMock->expects(static::once())->method('save')
+ ->willReturn($this->userExpirationResourceMock);
+ $this->observer->execute($this->eventObserverMock);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function testClearUserExpiration()
+ {
+ $userId = '123';
+ $this->userExpirationMock->setId($userId);
+
+ $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock);
+ $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock);
+ $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($userId);
+ $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn(null);
+ $this->userExpirationFactoryMock->expects(static::once())->method('create')
+ ->willReturn($this->userExpirationMock);
+ $this->userExpirationResourceMock->expects(static::once())->method('load')
+ ->willReturn($this->userExpirationMock);
+
+ $this->userExpirationMock->expects(static::once())->method('getId')->willReturn($userId);
+ $this->userExpirationResourceMock->expects(static::once())->method('delete')
+ ->willReturn($this->userExpirationResourceMock);
+ $this->observer->execute($this->eventObserverMock);
+ }
+
+ public function testChangeUserExpiration()
+ {
+ $userId = '123';
+ $this->userExpirationMock->setId($userId);
+
+ $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock);
+ $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock);
+ $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($userId);
+ $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn($this->getExpiresDateTime());
+ $this->userExpirationFactoryMock->expects(static::once())->method('create')
+ ->willReturn($this->userExpirationMock);
+ $this->userExpirationResourceMock->expects(static::once())->method('load')
+ ->willReturn($this->userExpirationMock);
+
+ $this->userExpirationMock->expects(static::once())->method('getId')->willReturn($userId);
+ $this->userExpirationMock->expects(static::once())->method('setExpiresAt')
+ ->willReturn($this->userExpirationMock);
+ $this->userExpirationResourceMock->expects(static::once())->method('save')
+ ->willReturn($this->userExpirationResourceMock);
+ $this->observer->execute($this->eventObserverMock);
+ }
+
+ /**
+ * @return string
+ * @throws \Exception
+ */
+ private function getExpiresDateTime()
+ {
+ $testDate = new \DateTime();
+ $testDate->modify('+10 days');
+ return $testDate->format('Y-m-d H:i:s');
+ }
+}
diff --git a/app/code/Magento/Security/etc/adminhtml/di.xml b/app/code/Magento/Security/etc/adminhtml/di.xml
index 79477e9443097..388a1eac742a5 100644
--- a/app/code/Magento/Security/etc/adminhtml/di.xml
+++ b/app/code/Magento/Security/etc/adminhtml/di.xml
@@ -15,6 +15,9 @@
+
+
+
Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST
@@ -28,4 +31,7 @@
+
+
+
diff --git a/app/code/Magento/Security/etc/crontab.xml b/app/code/Magento/Security/etc/crontab.xml
index a30a43730e6fa..7ee046ee44850 100644
--- a/app/code/Magento/Security/etc/crontab.xml
+++ b/app/code/Magento/Security/etc/crontab.xml
@@ -13,5 +13,8 @@
0 0 * * *
+
+ 0 * * * *
+
diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml
index 5052f5642cb53..34bb497954a7e 100644
--- a/app/code/Magento/Security/etc/db_schema.xml
+++ b/app/code/Magento/Security/etc/db_schema.xml
@@ -55,4 +55,15 @@
+
diff --git a/app/code/Magento/Security/etc/db_schema_whitelist.json b/app/code/Magento/Security/etc/db_schema_whitelist.json
index c387b7591c7a5..1f8183e123956 100644
--- a/app/code/Magento/Security/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Security/etc/db_schema_whitelist.json
@@ -33,5 +33,15 @@
"constraint": {
"PRIMARY": true
}
+ },
+ "admin_user_expiration": {
+ "column": {
+ "user_id": true,
+ "expires_at": true
+ },
+ "constraint": {
+ "PRIMARY": true,
+ "ADMIN_USER_EXPIRATION_USER_ID_ADMIN_USER_USER_ID": true
+ }
}
}
\ No newline at end of file
diff --git a/app/code/Magento/Security/etc/di.xml b/app/code/Magento/Security/etc/di.xml
index 4fe88f219cf74..3b07bb84b1161 100644
--- a/app/code/Magento/Security/etc/di.xml
+++ b/app/code/Magento/Security/etc/di.xml
@@ -18,4 +18,5 @@
+
diff --git a/app/code/Magento/Security/etc/events.xml b/app/code/Magento/Security/etc/events.xml
new file mode 100644
index 0000000000000..f85cfc9387c48
--- /dev/null
+++ b/app/code/Magento/Security/etc/events.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Security/i18n/en_US.csv b/app/code/Magento/Security/i18n/en_US.csv
index 0cf998b21a1c8..3d5bb6e8b59d4 100644
--- a/app/code/Magento/Security/i18n/en_US.csv
+++ b/app/code/Magento/Security/i18n/en_US.csv
@@ -26,3 +26,6 @@ None,None
"Limit the number of password reset request per hour. Use 0 to disable.","Limit the number of password reset request per hour. Use 0 to disable."
"Min Time Between Password Reset Requests","Min Time Between Password Reset Requests"
"Delay in minutes between password reset requests. Use 0 to disable.","Delay in minutes between password reset requests. Use 0 to disable."
+"""%1"" must be later than the current date.","""%1"" must be later than the current date."
+"User Expiration","User Expiration"
+"""%1"" is not a valid date.","""%1"" is not a valid date."
diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml
index eb3ef37056b2f..15cb4e5319904 100644
--- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml
+++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml
@@ -13,7 +13,7 @@
Fills in the provided User details on the New User creation page.
-
+
diff --git a/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php b/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php
index 369d71ddbff9b..68e049804e95a 100644
--- a/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php
+++ b/dev/tests/integration/testsuite/Magento/Integration/Model/AdminTokenServiceTest.php
@@ -59,6 +59,24 @@ public function testCreateAdminAccessToken()
$this->assertEquals($accessToken, $token);
}
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ * @expectedException \Magento\Framework\Exception\AuthenticationException
+ */
+ public function testCreateAdminAccessTokenExpiredUser()
+ {
+ $adminUserNameFromFixture = 'adminUserExpired';
+ $this->tokenService->createAdminAccessToken(
+ $adminUserNameFromFixture,
+ \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD
+ );
+
+ $this->expectExceptionMessage(
+ 'The account sign-in was incorrect or your account is disabled temporarily. '
+ . 'Please wait and try again later.'
+ );
+ }
+
/**
* @dataProvider validationDataProvider
*/
diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php
index 6509e3af1050a..a979165c5bd59 100644
--- a/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php
+++ b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AuthSessionTest.php
@@ -8,6 +8,7 @@
/**
* @magentoAppArea adminhtml
* @magentoAppIsolation enabled
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class AuthSessionTest extends \PHPUnit\Framework\TestCase
{
@@ -141,4 +142,46 @@ public function testProcessProlong()
$this->assertGreaterThan(strtotime($oldUpdatedAt), strtotime($updatedAt));
}
+
+ /**
+ * Test processing prolong with an expired user.
+ *
+ * @magentoDbIsolation enabled
+ */
+ public function testProcessProlongWithExpiredUser()
+ {
+ $this->auth->login(
+ \Magento\TestFramework\Bootstrap::ADMIN_NAME,
+ \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD
+ );
+
+ $expireDate = new \DateTime();
+ $expireDate->modify('-10 days');
+ /** @var \Magento\User\Model\User $user */
+ $user = $this->objectManager->create(\Magento\User\Model\User::class);
+ $user->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME);
+ $userExpirationFactory =
+ $this->objectManager->create(\Magento\Security\Api\Data\UserExpirationInterfaceFactory::class);
+ /** @var \Magento\Security\Api\Data\UserExpirationInterface $userExpiration */
+ $userExpiration = $userExpirationFactory->create();
+ $userExpiration->setId($user->getId())
+ ->setExpiresAt($expireDate->format('Y-m-d H:i:s'))
+ ->save();
+
+ // need to trigger a prolong
+ $sessionId = $this->authSession->getSessionId();
+ $prolongsDiff = 4 * log($this->securityConfig->getAdminSessionLifetime()) + 2;
+ $dateInPast = $this->dateTime->formatDate($this->authSession->getUpdatedAt() - $prolongsDiff);
+ $this->adminSessionsManager->getCurrentSession()
+ ->setData(
+ 'updated_at',
+ $dateInPast
+ )
+ ->save();
+ $this->adminSessionInfo->load($sessionId, 'session_id');
+ $this->authSession->prolong();
+ static::assertFalse($this->auth->isLoggedIn());
+ $user->reload();
+ static::assertFalse((bool)$user->getIsActive());
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php
new file mode 100644
index 0000000000000..e52f84c68d851
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpiration/CollectionTest.php
@@ -0,0 +1,74 @@
+objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+ $this->collectionModelFactory = $this->objectManager
+ ->create(\Magento\Security\Model\ResourceModel\UserExpiration\CollectionFactory::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testAddExpiredActiveUsersFilter()
+ {
+ /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */
+ $collectionModel = $this->collectionModelFactory->create();
+ $collectionModel->addActiveExpiredUsersFilter();
+ static::assertEquals(1, $collectionModel->getSize());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testAddUserIdsFilter()
+ {
+ $adminUserNameFromFixture = 'adminUserExpired';
+ $user = $this->objectManager->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($adminUserNameFromFixture);
+
+ /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */
+ $collectionModel = $this->collectionModelFactory->create()->addUserIdsFilter([$user->getId()]);
+ static::assertEquals(1, $collectionModel->getSize());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testAddExpiredRecordsForUserFilter()
+ {
+ $adminUserNameFromFixture = 'adminUserExpired';
+ $user = $this->objectManager->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($adminUserNameFromFixture);
+
+ /** @var \Magento\Security\Model\ResourceModel\UserExpiration\Collection $collectionModel */
+ $collectionModel = $this->collectionModelFactory->create()->addExpiredRecordsForUserFilter($user->getId());
+ static::assertEquals(1, $collectionModel->getSize());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php
new file mode 100644
index 0000000000000..adb7b7a120f1f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpirationManagerTest.php
@@ -0,0 +1,177 @@
+objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+ $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class);
+ $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class);
+ $this->adminSessionInfo = $this->objectManager->create(\Magento\Security\Model\AdminSessionInfo::class);
+ $this->auth->setAuthStorage($this->authSession);
+ $this->userExpirationManager =
+ $this->objectManager->create(\Magento\Security\Model\UserExpirationManager::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testUserIsExpired()
+ {
+ $adminUserNameFromFixture = 'adminUserExpired';
+ $user = $this->loadUserByUsername($adminUserNameFromFixture);
+ static::assertTrue($this->userExpirationManager->isUserExpired($user->getId()));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ * @magentoAppIsolation enabled
+ */
+ public function testDeactivateExpiredUsersWithExpiredUser()
+ {
+ $adminUsernameFromFixture = 'adminUserNotExpired';
+ $this->loginUser($adminUsernameFromFixture);
+ $user = $this->loadUserByUsername($adminUsernameFromFixture);
+ $sessionId = $this->authSession->getSessionId();
+ $this->expireUser($user);
+ $this->userExpirationManager->deactivateExpiredUsersById([$user->getId()]);
+ $this->adminSessionInfo->load($sessionId, 'session_id');
+ $user->reload();
+ $userExpirationModel = $this->loadExpiredUserModelByUser($user);
+ static::assertEquals(0, $user->getIsActive());
+ static::assertNull($userExpirationModel->getId());
+ static::assertEquals(AdminSessionInfo::LOGGED_OUT, (int)$this->adminSessionInfo->getStatus());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ * @magentoAppIsolation enabled
+ */
+ public function testDeactivateExpiredUsersWithNonExpiredUser()
+ {
+ $adminUsernameFromFixture = 'adminUserNotExpired';
+ $this->loginUser($adminUsernameFromFixture);
+ $user = $this->loadUserByUsername($adminUsernameFromFixture);
+ $sessionId = $this->authSession->getSessionId();
+ $this->userExpirationManager->deactivateExpiredUsersById([$user->getId()]);
+ $user->reload();
+ $userExpirationModel = $this->loadExpiredUserModelByUser($user);
+ $this->adminSessionInfo->load($sessionId, 'session_id');
+ static::assertEquals(1, $user->getIsActive());
+ static::assertEquals($user->getId(), $userExpirationModel->getId());
+ static::assertEquals(AdminSessionInfo::LOGGED_IN, (int)$this->adminSessionInfo->getStatus());
+ }
+
+ /**
+ * Test deactivating without inputting a user.
+ *
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testDeactivateExpiredUsers()
+ {
+ $notExpiredUser = $this->loadUserByUsername('adminUserNotExpired');
+ $expiredUser = $this->loadUserByUsername('adminUserExpired');
+ $this->userExpirationManager->deactivateExpiredUsers();
+ $notExpiredUserExpirationModel = $this->loadExpiredUserModelByUser($notExpiredUser);
+ $expiredUserExpirationModel = $this->loadExpiredUserModelByUser($expiredUser);
+
+ static::assertNotNull($notExpiredUserExpirationModel->getId());
+ static::assertNull($expiredUserExpirationModel->getId());
+ $notExpiredUser->reload();
+ $expiredUser->reload();
+ static::assertEquals($notExpiredUser->getIsActive(), 1);
+ static::assertEquals($expiredUser->getIsActive(), 0);
+ }
+
+ /**
+ * Login the given user and return a user model.
+ *
+ * @param string $username
+ * @throws \Magento\Framework\Exception\AuthenticationException
+ */
+ private function loginUser(string $username)
+ {
+ $this->auth->login(
+ $username,
+ \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD
+ );
+ }
+
+ /**
+ * @param $username
+ * @return \Magento\User\Model\User
+ */
+ private function loadUserByUsername(string $username): \Magento\User\Model\User
+ {
+ /** @var \Magento\User\Model\User $user */
+ $user = $this->objectManager->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($username);
+ return $user;
+ }
+
+ /**
+ * Expire the given user and return the UserExpiration model.
+ *
+ * @param \Magento\User\Model\User $user
+ * @throws \Exception
+ */
+ private function expireUser(\Magento\User\Model\User $user)
+ {
+ $expireDate = new \DateTime();
+ $expireDate->modify('-10 days');
+ /** @var \Magento\Security\Api\Data\UserExpirationInterface $userExpiration */
+ $userExpiration = $this->objectManager->create(\Magento\Security\Api\Data\UserExpirationInterface::class);
+ $userExpiration->setId($user->getId())
+ ->setExpiresAt($expireDate->format('Y-m-d H:i:s'))
+ ->save();
+ }
+
+ /**
+ * @param \Magento\User\Model\User $user
+ * @return \Magento\Security\Model\UserExpiration
+ */
+ private function loadExpiredUserModelByUser(\Magento\User\Model\User $user): \Magento\Security\Model\UserExpiration
+ {
+ /** @var \Magento\Security\Model\UserExpiration $expiredUserModel */
+ $expiredUserModel = $this->objectManager->create(\Magento\Security\Model\UserExpiration::class);
+ $expiredUserModel->load($user->getId());
+ return $expiredUserModel;
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Security/Observer/AdminUserAuthenticateBeforeTest.php b/dev/tests/integration/testsuite/Magento/Security/Observer/AdminUserAuthenticateBeforeTest.php
new file mode 100644
index 0000000000000..f25b836b80514
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Security/Observer/AdminUserAuthenticateBeforeTest.php
@@ -0,0 +1,46 @@
+create(\Magento\User\Model\User::class);
+ $user->authenticate($adminUserNameFromFixture, $password);
+ static::assertFalse((bool)$user->getIsActive());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testWithNonExpiredUser()
+ {
+ $adminUserNameFromFixture = 'adminUserNotExpired';
+ $password = \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD;
+ /** @var \Magento\User\Model\User $user */
+ $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class);
+ $user->authenticate($adminUserNameFromFixture, $password);
+ static::assertTrue((bool)$user->getIsActive());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php
new file mode 100644
index 0000000000000..ee2ae79b62cf8
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Security/Observer/AfterAdminUserSaveTest.php
@@ -0,0 +1,126 @@
+getFutureDateInStoreTime();
+ $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($adminUserNameFromFixture);
+ $user->setExpiresAt($testDate);
+ $user->save();
+
+ $userExpirationFactory =
+ Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class);
+ /** @var \Magento\Security\Model\UserExpiration $userExpiration */
+ $userExpiration = $userExpirationFactory->create();
+ $userExpiration->load($user->getId());
+ static::assertNotNull($userExpiration->getId());
+ static::assertEquals($userExpiration->getExpiresAt(), $testDate);
+ }
+
+ /**
+ * Save a new UserExpiration; used to validate that date conversion is working correctly.
+ *
+ * @magentoDataFixture Magento/User/_files/dummy_user.php
+ */
+ public function testSaveNewUserExpirationInMinutes()
+ {
+ $adminUserNameFromFixture = 'dummy_username';
+ $testDate = $this->getFutureDateInStoreTime('+2 minutes');
+ $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($adminUserNameFromFixture);
+ $user->setExpiresAt($testDate);
+ $user->save();
+
+ $userExpirationFactory =
+ Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class);
+ /** @var \Magento\Security\Model\UserExpiration $userExpiration */
+ $userExpiration = $userExpirationFactory->create();
+ $userExpiration->load($user->getId());
+ static::assertNotNull($userExpiration->getId());
+ static::assertEquals($userExpiration->getExpiresAt(), $testDate);
+ }
+
+ /**
+ * Remove the UserExpiration record
+ *
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testClearUserExpiration()
+ {
+ $adminUserNameFromFixture = 'adminUserExpired';
+ $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($adminUserNameFromFixture);
+ $user->setExpiresAt(null);
+ $user->save();
+
+ $userExpirationFactory =
+ Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class);
+ /** @var \Magento\Security\Model\UserExpiration $userExpiration */
+ $userExpiration = $userExpirationFactory->create();
+ $userExpiration->load($user->getId());
+ static::assertNull($userExpiration->getId());
+ }
+
+ /**
+ * Change the UserExpiration record
+ *
+ * @magentoDataFixture Magento/Security/_files/expired_users.php
+ */
+ public function testChangeUserExpiration()
+ {
+ $adminUserNameFromFixture = 'adminUserNotExpired';
+ $testDate = $this->getFutureDateInStoreTime();
+ $user = Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class);
+ $user->loadByUsername($adminUserNameFromFixture);
+
+ $userExpirationFactory =
+ Bootstrap::getObjectManager()->create(\Magento\Security\Model\UserExpirationFactory::class);
+ /** @var \Magento\Security\Model\UserExpiration $userExpiration */
+ $userExpiration = $userExpirationFactory->create();
+ $userExpiration->load($user->getId());
+ $existingExpiration = $userExpiration->getExpiresAt();
+
+ $user->setExpiresAt($testDate);
+ $user->save();
+ $userExpiration->load($user->getId());
+ static::assertNotNull($userExpiration->getId());
+ static::assertEquals($userExpiration->getExpiresAt(), $testDate);
+ static::assertNotEquals($existingExpiration, $userExpiration->getExpiresAt());
+ }
+
+ /**
+ * @param string $timeToAdd Amount of time to add
+ * @return string
+ * @throws \Exception
+ */
+ private function getFutureDateInStoreTime($timeToAdd = '+20 days')
+ {
+ /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface $locale */
+ $locale = Bootstrap::getObjectManager()->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
+ $testDate = new \DateTime();
+ $testDate->modify($timeToAdd);
+ $storeDate = $locale->date($testDate);
+ return $storeDate->format('Y-m-d H:i:s');
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php
new file mode 100644
index 0000000000000..ac45f120ca6d3
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users.php
@@ -0,0 +1,58 @@
+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();