Skip to content

Commit

Permalink
Create models for userexpiration, add tests and events (#22833: Short…
Browse files Browse the repository at this point in the history
…-term admin accounts)
  • Loading branch information
lfolco committed Jun 30, 2019
1 parent 13bfde2 commit e51bf0a
Show file tree
Hide file tree
Showing 30 changed files with 1,742 additions and 41 deletions.
75 changes: 38 additions & 37 deletions app/code/Magento/Security/Model/AdminSessionsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,28 +75,39 @@ 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
* @param AdminSessionInfoFactory $adminSessionInfoFactory
* @param CollectionFactory $adminSessionInfoCollectionFactory
* @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime
* @param RemoteAddress $remoteAddress
* @param UserExpirationManager|null $userExpirationManager
*/
public function __construct(
ConfigInterface $securityConfig,
\Magento\Backend\Model\Auth\Session $authSession,
\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;
$this->adminSessionInfoFactory = $adminSessionInfoFactory;
$this->adminSessionInfoCollectionFactory = $adminSessionInfoCollectionFactory;
$this->dateTime = $dateTime;
$this->remoteAddress = $remoteAddress;
$this->userExpirationManager = $userExpirationManager ?:
\Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Security\Model\UserExpirationManager::class);
}

/**
Expand All @@ -127,10 +133,6 @@ public function processLogin()
}
}

if ($this->authSession->getUser()->getExpiresAt()) {
$this->revokeExpiredAdminUser();
}

return $this;
}

Expand All @@ -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(
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
}
40 changes: 40 additions & 0 deletions app/code/Magento/Security/Model/Plugin/UserValidationRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Security\Model\Plugin;

/**
* \Magento\User\Model\UserValidationRules decorator
*
* @package Magento\Security\Model\Plugin
*/
class UserValidationRules
{
/**@var \Magento\Security\Model\UserExpiration\Validator */
private $validator;

/**
* UserValidationRules constructor.
*
* @param \Magento\Security\Model\UserExpiration\Validator $validator
*/
public function __construct(\Magento\Security\Model\UserExpiration\Validator $validator)
{
$this->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');
}
}
49 changes: 49 additions & 0 deletions app/code/Magento/Security/Model/ResourceModel/UserExpiration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Security\Model\ResourceModel;

/**
* Admin User Expiration resource model
*/
class UserExpiration extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{

/**
* Flag that notifies whether Primary key of table is auto-incremented
*
* @var bool
*/
protected $_isPkAutoIncrement = false;

/**
* @return void
*/
protected function _construct()
{
$this->_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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Security\Model\ResourceModel\UserExpiration;

/**
* Admin user expiration collection
*/
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
/**
* @var string
*/
protected $_idFieldName = 'user_id';

/**
* @return void
*/
protected function _construct()
{
$this->_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);
}
}
62 changes: 62 additions & 0 deletions app/code/Magento/Security/Model/UserExpiration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Security\Model;

/**
* Admin User Expiration model.
* @method string getExpiresAt()
* @method \Magento\Security\Model\UserExpiration setExpiresAt(string $value)
*/
class UserExpiration extends \Magento\Framework\Model\AbstractModel
{

/**
* @var UserExpiration\Validator
*/
private $validator;

/**
* UserExpiration constructor.
*
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
* @param UserExpiration\Validator $validator
* @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource
* @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
* @param array $data
*/
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Security\Model\UserExpiration\Validator $validator,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = []
) {
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
$this->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;
// }
}
Loading

0 comments on commit e51bf0a

Please sign in to comment.