diff --git a/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key.php b/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key.php index 8c02fcacecb6e..b2e8e0b98542f 100644 --- a/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key.php +++ b/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key.php @@ -4,11 +4,12 @@ * See COPYING.txt for license details. */ + +namespace Magento\EncryptionKey\Controller\Adminhtml\Crypt; + /** * Encryption key changer controller */ -namespace Magento\EncryptionKey\Controller\Adminhtml\Crypt; - abstract class Key extends \Magento\Backend\App\Action { /** diff --git a/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Save.php b/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Save.php index b294d98df9c1c..926d00e4f673b 100644 --- a/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Save.php +++ b/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Save.php @@ -64,10 +64,10 @@ public function execute() } $newKey = $this->change->changeEncryptionKey($key); - $this->messageManager->addSuccess(__('The encryption key has been changed.')); + $this->messageManager->addSuccessMessage(__('The encryption key has been changed.')); if (!$key) { - $this->messageManager->addNotice( + $this->messageManager->addNoticeMessage( __( 'This is your new encryption key: %1. ' . 'Be sure to write it down and take good care of it!', @@ -77,7 +77,7 @@ public function execute() } $this->cache->clean(); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_session->setFormData(['crypt_key' => $key]); } $this->_redirect('adminhtml/*/'); diff --git a/app/code/Magento/EncryptionKey/Model/Resource/Key/Change.php b/app/code/Magento/EncryptionKey/Model/Resource/Key/Change.php index 2ebb4e2cc3f8e..a211eb523b410 100644 --- a/app/code/Magento/EncryptionKey/Model/Resource/Key/Change.php +++ b/app/code/Magento/EncryptionKey/Model/Resource/Key/Change.php @@ -77,35 +77,6 @@ protected function _construct() $this->_init('core_config_data', 'config_id'); } - /** - * Re-encrypt all encrypted data in the database - * - * TODO: seems not used - * - * @param bool $safe Specifies whether wrapping re-encryption into the database transaction or not - * @return void - * @throws \Exception - */ - public function reEncryptDatabaseValues($safe = true) - { - // update database only - if ($safe) { - $this->beginTransaction(); - } - try { - $this->_reEncryptSystemConfigurationValues(); - $this->_reEncryptCreditCardNumbers(); - if ($safe) { - $this->commit(); - } - } catch (\Exception $e) { - if ($safe) { - $this->rollBack(); - } - throw $e; - } - } - /** * Change encryption key * diff --git a/app/code/Magento/EncryptionKey/Test/Unit/Controller/Adminhtml/Crypt/Key/SaveTest.php b/app/code/Magento/EncryptionKey/Test/Unit/Controller/Adminhtml/Crypt/Key/SaveTest.php new file mode 100644 index 0000000000000..ef30546eb3ba7 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Unit/Controller/Adminhtml/Crypt/Key/SaveTest.php @@ -0,0 +1,130 @@ +encryptMock = $this->getMockBuilder('Magento\Framework\Encryption\EncryptorInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->changeMock = $this->getMockBuilder('Magento\EncryptionKey\Model\Resource\Key\Change') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->cacheMock = $this->getMockBuilder('Magento\Framework\App\CacheInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->requestMock = $this->getMockBuilder('Magento\Framework\App\RequestInterface') + ->disableOriginalConstructor() + ->setMethods(['getPost']) + ->getMockForAbstractClass(); + $this->managerMock = $this->getMockBuilder('\Magento\Framework\Message\ManagerInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->responseMock = $this->getMockBuilder('Magento\Framework\App\ResponseInterface') + ->disableOriginalConstructor() + ->setMethods(['setRedirect']) + ->getMockForAbstractClass(); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->model = $helper->getObject( + 'Magento\EncryptionKey\Controller\Adminhtml\Crypt\Key\Save', + [ + 'encryptor' => $this->encryptMock, + 'change' => $this->changeMock, + 'cache' => $this->cacheMock, + 'request' => $this->requestMock, + 'messageManager' => $this->managerMock, + 'response' => $this->responseMock, + ] + ); + } + + public function testExecuteNonRandomAndWithCryptKey() + { + $expectedMessage = 'The encryption key has been changed.'; + $key = 1; + $newKey = 'RSASHA9000VERYSECURESUPERMANKEY'; + $this->requestMock + ->expects($this->at(0)) + ->method('getPost') + ->with($this->equalTo('generate_random')) + ->willReturn(0); + $this->requestMock + ->expects($this->at(1)) + ->method('getPost') + ->with($this->equalTo('crypt_key')) + ->willReturn($key); + $this->encryptMock->expects($this->once())->method('validateKey'); + $this->changeMock->expects($this->once())->method('changeEncryptionKey')->willReturn($newKey); + $this->managerMock->expects($this->once())->method('addSuccessMessage')->with($expectedMessage); + $this->cacheMock->expects($this->once())->method('clean'); + $this->responseMock->expects($this->once())->method('setRedirect'); + + $this->model->execute(); + } + + public function testExecuteNonRandomAndWithoutCryptKey() + { + $key = null; + $this->requestMock + ->expects($this->at(0)) + ->method('getPost') + ->with($this->equalTo('generate_random')) + ->willReturn(0); + $this->requestMock + ->expects($this->at(1)) + ->method('getPost') + ->with($this->equalTo('crypt_key')) + ->willReturn($key); + $this->managerMock->expects($this->once())->method('addErrorMessage'); + + $this->model->execute(); + } + + public function testExecuteRandom() + { + $newKey = 'RSASHA9000VERYSECURESUPERMANKEY'; + $this->requestMock + ->expects($this->at(0)) + ->method('getPost') + ->with($this->equalTo('generate_random')) + ->willReturn(1); + $this->changeMock->expects($this->once())->method('changeEncryptionKey')->willReturn($newKey); + $this->managerMock->expects($this->once())->method('addSuccessMessage'); + $this->managerMock->expects($this->once())->method('addNoticeMessage'); + $this->cacheMock->expects($this->once())->method('clean'); + $this->responseMock->expects($this->once())->method('setRedirect'); + + $this->model->execute(); + } +} diff --git a/app/code/Magento/EncryptionKey/Test/Unit/Model/Resource/Key/ChangeTest.php b/app/code/Magento/EncryptionKey/Test/Unit/Model/Resource/Key/ChangeTest.php new file mode 100644 index 0000000000000..c0793ee43c544 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Unit/Model/Resource/Key/ChangeTest.php @@ -0,0 +1,130 @@ +encryptMock = $this->getMockBuilder('Magento\Framework\Encryption\EncryptorInterface') + ->disableOriginalConstructor() + ->setMethods(['setNewKey', 'exportKeys']) + ->getMockForAbstractClass(); + $this->filesystemMock = $this->getMockBuilder('Magento\Framework\Filesystem') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->structureMock = $this->getMockBuilder('Magento\Config\Model\Config\Structure') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->writerMock = $this->getMockBuilder('Magento\Framework\App\DeploymentConfig\Writer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->adapterMock = $this->getMockBuilder('Magento\Framework\DB\Adapter\AdapterInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->resourceMock = $this->getMockBuilder('Magento\Framework\App\Resource') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->selectMock = $this->getMockBuilder('Magento\Framework\DB\Select') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->tansactionMock = $this->getMockBuilder('Magento\Framework\Model\Resource\Db\TransactionManagerInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->objRelationMock = $this->getMockBuilder('Magento\Framework\Model\Resource\Db\ObjectRelationProcessor') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->model = $helper->getObject( + 'Magento\EncryptionKey\Model\Resource\Key\Change', + [ + 'filesystem' => $this->filesystemMock, + 'structure' => $this->structureMock, + 'encryptor' => $this->encryptMock, + 'writer' => $this->writerMock, + 'adapterInterface' => $this->adapterMock, + 'resource' => $this->resourceMock, + 'transactionManager' => $this->tansactionMock, + 'relationProcessor' => $this->objRelationMock + ] + ); + } + + public function testChangeEncryptionKey() + { + $paths = ['path1', 'path2']; + $table = ['item1', 'item2']; + $values = [ + 'key1' => 'value1', + 'key2' => 'value2' + ]; + $key = 'key'; + + $this->writerMock->expects($this->once())->method('checkIfWritable')->willReturn(true); + $this->resourceMock->expects($this->atLeastOnce())->method('getConnection')->willReturn($this->adapterMock); + $this->adapterMock->expects($this->once())->method('beginTransaction'); + $this->structureMock->expects($this->once())->method('getFieldPathsByAttribute')->willReturn($paths); + $this->resourceMock->expects($this->atLeastOnce())->method('getTableName')->willReturn($table); + $this->adapterMock->expects($this->any())->method('select')->willReturn($this->selectMock); + $this->adapterMock->expects($this->any())->method('fetchPairs')->willReturn($values); + $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); + $this->selectMock->expects($this->atLeastOnce())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('update')->willReturnSelf(); + $this->writerMock->expects($this->once())->method('saveConfig'); + $this->adapterMock->expects($this->once())->method('getTransactionLevel')->willReturn(1); + + $this->assertEquals($key, $this->model->changeEncryptionKey($key)); + } + + public function testChangeEncryptionKeyThrowsException() + { + $key = 'key'; + $this->writerMock->expects($this->once())->method('checkIfWritable')->willReturn(false); + + try { + $this->model->changeEncryptionKey($key); + } catch (\Exception $e) { + return; + } + + $this->fail('An excpected exception was not signaled.'); + } +} diff --git a/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php b/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php new file mode 100644 index 0000000000000..adcb28325dc53 --- /dev/null +++ b/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php @@ -0,0 +1,83 @@ +backendConfig = $backendConfig; + } + + /** + * Check if latest password is expired + * + * @param array $latestPassword + * @return bool + */ + public function _isLatestPasswordExpired($latestPassword) + { + if (!isset($latestPassword['expires']) || $this->getAdminPasswordLifetime() == 0) { + return false; + } else { + return (int)$latestPassword['expires'] < time(); + } + } + + /** + * Get admin lock threshold from configuration + * @return int + */ + public function getAdminLockThreshold() + { + return 60 * (int)$this->backendConfig->getValue('admin/security/lockout_threshold'); + } + + /** + * Check whether password change is forced + * + * @return bool + */ + public function isPasswordChangeForced() + { + return (bool)(int)$this->backendConfig->getValue('admin/security/password_is_forced'); + } + + /** + * Get admin password lifetime + * + * @return int + */ + public function getAdminPasswordLifetime() + { + return 86400 * (int)$this->backendConfig->getValue('admin/security/password_lifetime'); + } + + /** + * Get admin maxiumum security failures from config + * + * @return int + */ + public function getMaxFailures() + { + return (int)$this->backendConfig->getValue('admin/security/lockout_failures'); + } +} diff --git a/app/code/Magento/User/Model/Backend/Observer/AuthObserver.php b/app/code/Magento/User/Model/Backend/Observer/AuthObserver.php new file mode 100644 index 0000000000000..f682202a22320 --- /dev/null +++ b/app/code/Magento/User/Model/Backend/Observer/AuthObserver.php @@ -0,0 +1,198 @@ +observerConfig = $observerConfig; + $this->userResource = $userResource; + $this->url = $url; + $this->authSession = $authSession; + $this->userFactory = $userFactory; + $this->encryptor = $encryptor; + $this->messageManager = $messageManager; + } + + /** + * Admin locking and password hashing upgrade logic implementation + * + * @param EventObserver $observer + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function adminAuthenticate($observer) + { + $password = $observer->getEvent()->getPassword(); + /** @var User $user */ + $user = $observer->getEvent()->getUser(); + $authResult = $observer->getEvent()->getResult(); + + if (!$authResult && $user->getId()) { + // update locking information regardless whether user locked or not + $this->_updateLockingInformation($user); + } + + // check whether user is locked + $lockExpires = $user->getLockExpires(); + if ($lockExpires) { + $lockExpires = new \DateTime($lockExpires); + if ($lockExpires > new \DateTime()) { + throw new UserLockedException( + __('You did not sign in correctly or your account is temporarily disabled.') + ); + } + } + + if (!$authResult) { + return; + } + + $this->userResource->unlock($user->getId()); + + $latestPassword = $this->userResource->getLatestPassword($user->getId()); + $this->_checkExpiredPassword($latestPassword); + + if (!$this->encryptor->validateHashVersion($user->getPassword(), true)) { + $user->setPassword($password) + ->setData('force_new_password', true) + ->save(); + } + } + + /** + * Update locking information for the user + * + * @param \Magento\User\Model\User $user + * @return void + */ + private function _updateLockingInformation($user) + { + $now = new \DateTime(); + $lockThreshold = $this->observerConfig->getAdminLockThreshold(); + $maxFailures = $this->observerConfig->getMaxFailures(); + if (!($lockThreshold && $maxFailures)) { + return; + } + $failuresNum = (int)$user->getFailuresNum() + 1; + /** @noinspection PhpAssignmentInConditionInspection */ + if ($firstFailureDate = $user->getFirstFailure()) { + $firstFailureDate = new \DateTime($firstFailureDate); + } + + $newFirstFailureDate = false; + $updateLockExpires = false; + $lockThreshInterval = new \DateInterval('PT' . $lockThreshold.'S'); + // set first failure date when this is first failure or last first failure expired + if (1 === $failuresNum || !$firstFailureDate || $now->diff($firstFailureDate) > $lockThreshInterval) { + $newFirstFailureDate = $now; + // otherwise lock user + } elseif ($failuresNum >= $maxFailures) { + $updateLockExpires = $now->add($lockThreshInterval); + } + $this->userResource->updateFailure($user, $updateLockExpires, $newFirstFailureDate); + } + + /** + * Check whether the latest password is expired + * Side-effect can be when passwords were changed with different lifetime configuration settings + * + * @param array $latestPassword + * @return void + */ + private function _checkExpiredPassword($latestPassword) + { + if ($latestPassword && $this->observerConfig->_isLatestPasswordExpired($latestPassword)) { + if ($this->observerConfig->isPasswordChangeForced()) { + $message = __('It\'s time to change your password.'); + } else { + $myAccountUrl = $this->url->getUrl('adminhtml/system_account/'); + $message = __('It\'s time to change your password.', $myAccountUrl); + } + $this->messageManager->addNoticeMessage($message); + $message = $this->messageManager->getMessages()->getLastAddedMessage(); + if ($message) { + $message->setIdentifier('magento_user_password_expired')->setIsSticky(true); + $this->authSession->setPciAdminUserIsPasswordExpired(true); + } + } + } +} diff --git a/app/code/Magento/User/Model/Backend/Observer.php b/app/code/Magento/User/Model/Backend/Observer/PasswordObserver.php similarity index 52% rename from app/code/Magento/User/Model/Backend/Observer.php rename to app/code/Magento/User/Model/Backend/Observer/PasswordObserver.php index 95e5259b53850..93c54fd5df9a7 100644 --- a/app/code/Magento/User/Model/Backend/Observer.php +++ b/app/code/Magento/User/Model/Backend/Observer/PasswordObserver.php @@ -3,33 +3,29 @@ * Copyright © 2015 Magento. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\User\Model\Backend; + +namespace Magento\User\Model\Backend\Observer; use Magento\Framework\Event\Observer as EventObserver; -use Magento\Framework\Exception\State\UserLockedException; -use Magento\User\Model\User; /** - * User backend observer model - * - * Implements hashes upgrading - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * User backend observer model for passwords */ -class Observer +class PasswordObserver { /** - * Authorization interface + * Backend configuration interface * - * @var \Magento\Framework\AuthorizationInterface + * @var \Magento\User\Model\Backend\Config\ObserverConfig */ - protected $authorization; + protected $observerConfig; /** - * Backend configuration interface + * Authorization interface * - * @var \Magento\Backend\App\ConfigInterface + * @var \Magento\Framework\AuthorizationInterface */ - protected $backendConfig; + protected $authorization; /** * Admin user resource model @@ -59,13 +55,6 @@ class Observer */ protected $authSession; - /** - * Factory class for user model - * - * @var \Magento\User\Model\UserFactory - */ - protected $userFactory; - /** * Encryption model * @@ -89,159 +78,37 @@ class Observer /** * @param \Magento\Framework\AuthorizationInterface $authorization - * @param \Magento\Backend\App\ConfigInterface $backendConfig + * @param \Magento\User\Model\Backend\Config\ObserverConfig $observerConfig * @param \Magento\User\Model\Resource\User $userResource * @param \Magento\Backend\Model\UrlInterface $url * @param \Magento\Backend\Model\Session $session * @param \Magento\Backend\Model\Auth\Session $authSession - * @param \Magento\User\Model\UserFactory $userFactory * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor * @param \Magento\Framework\App\ActionFlag $actionFlag * @param \Magento\Framework\Message\ManagerInterface $messageManager - * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\AuthorizationInterface $authorization, - \Magento\Backend\App\ConfigInterface $backendConfig, + \Magento\User\Model\Backend\Config\ObserverConfig $observerConfig, \Magento\User\Model\Resource\User $userResource, \Magento\Backend\Model\UrlInterface $url, \Magento\Backend\Model\Session $session, \Magento\Backend\Model\Auth\Session $authSession, - \Magento\User\Model\UserFactory $userFactory, \Magento\Framework\Encryption\EncryptorInterface $encryptor, \Magento\Framework\App\ActionFlag $actionFlag, \Magento\Framework\Message\ManagerInterface $messageManager ) { $this->authorization = $authorization; - $this->backendConfig = $backendConfig; + $this->observerConfig = $observerConfig; $this->userResource = $userResource; $this->url = $url; $this->session = $session; $this->authSession = $authSession; - $this->userFactory = $userFactory; $this->encryptor = $encryptor; $this->actionFlag = $actionFlag; $this->messageManager = $messageManager; } - /** - * Admin locking and password hashing upgrade logic implementation - * - * @param EventObserver $observer - * @return void - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function adminAuthenticate($observer) - { - $password = $observer->getEvent()->getPassword(); - /** @var User $user */ - $user = $observer->getEvent()->getUser(); - $authResult = $observer->getEvent()->getResult(); - - if (!$authResult && $user->getId()) { - // update locking information regardless whether user locked or not - $this->_updateLockingInformation($user); - } - - // check whether user is locked - $lockExpires = $user->getLockExpires(); - if ($lockExpires) { - $lockExpires = new \DateTime($lockExpires); - if ($lockExpires > new \DateTime()) { - throw new UserLockedException( - __('You did not sign in correctly or your account is temporarily disabled.') - ); - } - } - - if (!$authResult) { - return; - } - - $this->userResource->unlock($user->getId()); - - $latestPassword = $this->userResource->getLatestPassword($user->getId()); - $this->_checkExpiredPassword($latestPassword); - - if (!$this->encryptor->validateHashVersion($user->getPassword(), true)) { - $user->setPassword($password) - ->setData('force_new_password', true) - ->save(); - } - } - - /** - * Update locking information for the user - * - * @param \Magento\User\Model\User $user - * @return void - */ - private function _updateLockingInformation($user) - { - $now = new \DateTime(); - $lockThreshold = $this->getAdminLockThreshold(); - $maxFailures = (int)$this->backendConfig->getValue('admin/security/lockout_failures'); - if (!($lockThreshold && $maxFailures)) { - return; - } - $failuresNum = (int)$user->getFailuresNum() + 1; - if ($firstFailureDate = $user->getFirstFailure()) { - $firstFailureDate = new \DateTime($firstFailureDate); - } - - $updateFirstFailureDate = false; - $updateLockExpires = false; - $lockThresholdInterval = new \DateInterval('PT' . $lockThreshold.'S'); - // set first failure date when this is first failure or last first failure expired - if (1 === $failuresNum || !$firstFailureDate || $now->diff($firstFailureDate) > $lockThresholdInterval) { - $updateFirstFailureDate = $now; - // otherwise lock user - } elseif ($failuresNum >= $maxFailures) { - $updateLockExpires = $now->add($lockThresholdInterval); - } - $this->userResource->updateFailure($user, $updateLockExpires, $updateFirstFailureDate); - } - - /** - * Check whether the latest password is expired - * Side-effect can be when passwords were changed with different lifetime configuration settings - * - * @param array $latestPassword - * @return void - */ - private function _checkExpiredPassword($latestPassword) - { - if ($latestPassword && $this->_isLatestPasswordExpired($latestPassword)) { - if ($this->isPasswordChangeForced()) { - $message = __('It\'s time to change your password.'); - } else { - $myAccountUrl = $this->url->getUrl('adminhtml/system_account/'); - $message = __('It\'s time to change your password.', $myAccountUrl); - } - $this->messageManager->addNotice($message); - $message = $this->messageManager->getMessages()->getLastAddedMessage(); - if ($message) { - $message->setIdentifier('magento_user_password_expired')->setIsSticky(true); - $this->authSession->setPciAdminUserIsPasswordExpired(true); - } - } - } - - /** - * Check if latest password is expired - * - * @param array $latestPassword - * @return bool - */ - protected function _isLatestPasswordExpired($latestPassword) - { - if (!isset($latestPassword['expires']) || $this->getAdminPasswordLifetime() == 0) { - return false; - } else { - return (int)$latestPassword['expires'] < time(); - } - } - /** * Harden admin password change. * @@ -264,7 +131,7 @@ public function checkAdminPasswordChange($observer) } if ($password && !$user->getForceNewPassword() && $user->getId()) { - if ($this->encryptor->validateHash($password, $user->getOrigData('password'))) { + if ($this->encryptor->isValidHash($password, $user->getOrigData('password'))) { throw new \Magento\Framework\Exception\LocalizedException( __('Sorry, but this password has already been used. Please create another.') ); @@ -295,7 +162,7 @@ public function trackAdminNewPassword($observer) $user = $observer->getEvent()->getObject(); if ($user->getId()) { $password = $user->getNewPassword(); - $passwordLifetime = $this->getAdminPasswordLifetime(); + $passwordLifetime = $this->observerConfig->getAdminPasswordLifetime(); if ($passwordLifetime && $password && !$user->getForceNewPassword()) { $resource = $this->userResource; $passwordHash = $this->encryptor->getHash($password, false); @@ -306,25 +173,6 @@ public function trackAdminNewPassword($observer) } } - /** - * Get admin lock threshold from configuration - * @return int - */ - public function getAdminLockThreshold() - { - return 60 * (int)$this->backendConfig->getValue('admin/security/lockout_threshold'); - } - - /** - * Get admin password lifetime - * - * @return int - */ - public function getAdminPasswordLifetime() - { - return 86400 * (int)$this->backendConfig->getValue('admin/security/password_lifetime'); - } - /** * Force admin to change password * @@ -333,11 +181,10 @@ public function getAdminPasswordLifetime() */ public function forceAdminPasswordChange($observer) { - if (!$this->isPasswordChangeForced()) { + if (!$this->observerConfig->isPasswordChangeForced()) { return; } - $session = $this->authSession; - if (!$session->isLoggedIn()) { + if (!$this->authSession->isLoggedIn()) { return; } $actionList = [ @@ -345,9 +192,11 @@ public function forceAdminPasswordChange($observer) 'adminhtml_system_account_save', 'adminhtml_auth_logout', ]; + /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getEvent()->getControllerAction(); /** @var \Magento\Framework\App\RequestInterface $request */ $request = $observer->getEvent()->getRequest(); + if ($this->authSession->getPciAdminUserIsPasswordExpired()) { if (!in_array($request->getFullActionName(), $actionList)) { if ($this->authorization->isAllowed('Magento_Backend::myaccount')) { @@ -361,7 +210,7 @@ public function forceAdminPasswordChange($observer) */ $this->authSession->clearStorage(); $this->session->clearStorage(); - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('Your password has expired; please contact your administrator.') ); $controller->getRequest()->setDispatched(false); @@ -369,14 +218,4 @@ public function forceAdminPasswordChange($observer) } } } - - /** - * Check whether password change is forced - * - * @return bool - */ - public function isPasswordChangeForced() - { - return (bool)(int)$this->backendConfig->getValue('admin/security/password_is_forced'); - } } diff --git a/app/code/Magento/User/Test/Unit/Model/Backend/Observer/AuthObserverTest.php b/app/code/Magento/User/Test/Unit/Model/Backend/Observer/AuthObserverTest.php new file mode 100644 index 0000000000000..97be558ef833b --- /dev/null +++ b/app/code/Magento/User/Test/Unit/Model/Backend/Observer/AuthObserverTest.php @@ -0,0 +1,267 @@ +configInterfaceMock = $this->getMockBuilder('Magento\Backend\App\ConfigInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->userMock = $this->getMockBuilder('Magento\User\Model\Resource\User') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->urlInterfaceMock = $this->getMockBuilder('Magento\Backend\Model\UrlInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->authSessionMock = $this->getMockBuilder('Magento\Backend\Model\Auth\Session') + ->disableOriginalConstructor() + ->setMethods( + [ + 'setPciAdminUserIsPasswordExpired', + 'unsPciAdminUserIsPasswordExpired', + 'getPciAdminUserIsPasswordExpired', + 'isLoggedIn', + 'clearStorage' + ] + )->getMock(); + + $this->userFactoryMock = $this->getMockBuilder('Magento\User\Model\UserFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->encryptorMock = $this->getMockBuilder('\Magento\Framework\Encryption\EncryptorInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->managerInterfaceMock = $this->getMockBuilder('Magento\Framework\Message\ManagerInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->messageInterfaceMock = $this->getMockBuilder('Magento\Framework\Message\MessageInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->eventManagerMock = $this->getMockBuilder('Magento\Framework\Event\ManagerInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMockForAbstractClass(); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->observerConfig = $helper->getObject( + 'Magento\User\Model\Backend\Config\ObserverConfig', + [ + 'backendConfig' => $this->configInterfaceMock + ] + ); + + $this->model = $helper->getObject( + 'Magento\User\Model\Backend\Observer\AuthObserver', + [ + 'observerConfig' => $this->observerConfig, + 'userResource' => $this->userMock, + 'url' => $this->urlInterfaceMock, + 'authSession' => $this->authSessionMock, + 'userFactory' => $this->userFactoryMock, + 'encryptor' => $this->encryptorMock, + 'messageManager' => $this->managerInterfaceMock, + 'messageInterface' => $this->messageInterfaceMock, + 'eventManager' => $this->eventManagerMock + ] + ); + } + + public function testAdminAuthenticate() + { + $password = "myP@sw0rd"; + $uid = 123; + $authResult = true; + $lockExpires = false; + $userPassword = [ + 'expires' => 1, + ]; + + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getPassword', 'getUser', 'getResult']) + ->getMock(); + + /** @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject $userMock */ + $userMock = $this->getMockBuilder('Magento\User\Model\User') + ->disableOriginalConstructor() + ->setMethods(['getId', 'getLockExpires', 'getPassword', 'save']) + ->getMock(); + + $eventObserverMock->expects($this->atLeastOnce())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getPassword')->willReturn($password); + $eventMock->expects($this->once())->method('getUser')->willReturn($userMock); + $eventMock->expects($this->once())->method('getResult')->willReturn($authResult); + $userMock->expects($this->atLeastOnce())->method('getId')->willReturn($uid); + $userMock->expects($this->once())->method('getLockExpires')->willReturn($lockExpires); + $this->userMock->expects($this->once())->method('unlock'); + $this->userMock->expects($this->once())->method('getLatestPassword')->willReturn($userPassword); + $this->configInterfaceMock + ->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(1); + + /** @var \Magento\Framework\Message\Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ + $collectionMock = $this->getMockBuilder('Magento\Framework\Message\Collection') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->managerInterfaceMock->expects($this->once())->method('getMessages')->willReturn($collectionMock); + $collectionMock + ->expects($this->once()) + ->method('getLastAddedMessage') + ->willReturn($this->messageInterfaceMock); + $this->messageInterfaceMock->expects($this->once())->method('setIdentifier')->willReturnSelf(); + $this->authSessionMock->expects($this->once())->method('setPciAdminUserIsPasswordExpired'); + $this->encryptorMock->expects($this->once())->method('validateHashVersion')->willReturn(false); + + $this->model->adminAuthenticate($eventObserverMock); + } + + public function testAdminAuthenticateThrowsException() + { + $password = "myP@sw0rd"; + $authResult = true; + $lockExpires = '3015-07-08 11:14:15.638276'; + + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getPassword', 'getUser', 'getResult']) + ->getMock(); + + /** @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject $userMock */ + $userMock = $this->getMockBuilder('Magento\User\Model\User') + ->disableOriginalConstructor() + ->setMethods(['getId', 'getLockExpires', 'getPassword']) + ->getMock(); + + $eventObserverMock->expects($this->atLeastOnce())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getPassword')->willReturn($password); + $eventMock->expects($this->once())->method('getUser')->willReturn($userMock); + $eventMock->expects($this->once())->method('getResult')->willReturn($authResult); + $userMock->expects($this->once())->method('getLockExpires')->willReturn($lockExpires); + + try { + $this->model->adminAuthenticate($eventObserverMock); + } catch (UserLockedException $expected) { + return; + } + $this->fail('An expected exception has not been raised.'); + } + + public function testAdminAuthenticateUpdateLockingInfo() + { + $password = "myP@sw0rd"; + $uid = 123; + $authResult = false; + $firstFailure = '1965-07-08 11:14:15.638276'; + $numOfFailures = 5; + + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getPassword', 'getUser', 'getResult']) + ->getMock(); + + /** @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject $userMock */ + $userMock = $this->getMockBuilder('Magento\User\Model\User') + ->disableOriginalConstructor() + ->setMethods(['getId', 'getFailuresNum', 'getFirstFailure']) + ->getMock(); + + $eventObserverMock->expects($this->atLeastOnce())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getPassword')->willReturn($password); + $eventMock->expects($this->once())->method('getUser')->willReturn($userMock); + $eventMock->expects($this->once())->method('getResult')->willReturn($authResult); + $userMock->expects($this->once())->method('getId')->willReturn($uid); + $this->configInterfaceMock + ->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(1); + $userMock->expects($this->once())->method('getFailuresNum')->willReturn($numOfFailures); + $userMock->expects($this->once())->method('getFirstFailure')->willReturn($firstFailure); + $this->userMock->expects($this->once())->method('updateFailure'); + + $this->model->adminAuthenticate($eventObserverMock); + } +} diff --git a/app/code/Magento/User/Test/Unit/Model/Backend/Observer/PasswordObserverTest.php b/app/code/Magento/User/Test/Unit/Model/Backend/Observer/PasswordObserverTest.php new file mode 100644 index 0000000000000..331d74ac086d3 --- /dev/null +++ b/app/code/Magento/User/Test/Unit/Model/Backend/Observer/PasswordObserverTest.php @@ -0,0 +1,308 @@ +authMock = $this->getMockBuilder('Magento\Framework\AuthorizationInterface') + ->disableOriginalConstructor() + ->setMethods(['isAllowed']) + ->getMock(); + + $this->configInterfaceMock = $this->getMockBuilder('Magento\Backend\App\ConfigInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->userMock = $this->getMockBuilder('Magento\User\Model\Resource\User') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->urlInterfaceMock = $this->getMockBuilder('Magento\Backend\Model\UrlInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->sessionMock = $this->getMockBuilder('Magento\Backend\Model\Session') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->authSessionMock = $this->getMockBuilder('Magento\Backend\Model\Auth\Session') + ->disableOriginalConstructor() + ->setMethods( + [ + 'setPciAdminUserIsPasswordExpired', + 'unsPciAdminUserIsPasswordExpired', + 'getPciAdminUserIsPasswordExpired', + 'isLoggedIn', + 'clearStorage' + ] + )->getMock(); + + $this->encryptorMock = $this->getMockBuilder('\Magento\Framework\Encryption\EncryptorInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->actionFlagMock = $this->getMockBuilder('Magento\Framework\App\ActionFlag') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->managerInterfaceMock = $this->getMockBuilder('Magento\Framework\Message\ManagerInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->messageInterfaceMock = $this->getMockBuilder('Magento\Framework\Message\MessageInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->eventManagerMock = $this->getMockBuilder('Magento\Framework\Event\ManagerInterface') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMockForAbstractClass(); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->observerConfig = $helper->getObject( + 'Magento\User\Model\Backend\Config\ObserverConfig', + [ + 'backendConfig' => $this->configInterfaceMock + ] + ); + + $this->model = $helper->getObject( + 'Magento\User\Model\Backend\Observer\PasswordObserver', + [ + 'observerConfig' => $this->observerConfig, + 'authorization' => $this->authMock, + 'userResource' => $this->userMock, + 'url' => $this->urlInterfaceMock, + 'session' => $this->sessionMock, + 'authSession' => $this->authSessionMock, + 'actionFlag' => $this->actionFlagMock, + 'encryptor' => $this->encryptorMock, + 'messageManager' => $this->managerInterfaceMock, + 'messageInterface' => $this->messageInterfaceMock, + 'eventManager' => $this->eventManagerMock + ] + ); + } + + public function testCheckAdminPasswordChange() + { + $newPW = "mYn3wpassw0rd"; + $uid = 123; + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getObject']) + ->getMock(); + + /** @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject $userMock */ + $userMock = $this->getMockBuilder('Magento\User\Model\User') + ->disableOriginalConstructor() + ->setMethods(['getId', 'getNewPassword', 'getForceNewPassword']) + ->getMock(); + + $eventObserverMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getObject')->willReturn($userMock); + $userMock->expects($this->atLeastOnce())->method('getNewPassword')->willReturn($newPW); + $userMock->expects($this->once())->method('getForceNewPassword')->willReturn(false); + $userMock->expects($this->once())->method('getId')->willReturn($uid); + $this->encryptorMock->expects($this->once())->method('isValidHash')->willReturn(false); + $this->encryptorMock->expects($this->once())->method('getHash')->willReturn(md5($newPW)); + $this->userMock->method('getOldPasswords')->willReturn([md5('pw1'), md5('pw2')]); + + $this->model->checkAdminPasswordChange($eventObserverMock); + } + + public function testCheckAdminPasswordChangeThrowsLocalizedExp() + { + $newPW = "mYn3wpassw0rd"; + $uid = 123; + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getObject']) + ->getMock(); + + /** @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject $userMock */ + $userMock = $this->getMockBuilder('Magento\User\Model\User') + ->disableOriginalConstructor() + ->setMethods(['getId', 'getNewPassword', 'getForceNewPassword']) + ->getMock(); + + $eventObserverMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getObject')->willReturn($userMock); + $userMock->expects($this->atLeastOnce())->method('getNewPassword')->willReturn($newPW); + $userMock->expects($this->once())->method('getForceNewPassword')->willReturn(false); + $userMock->expects($this->once())->method('getId')->willReturn($uid); + $this->encryptorMock->expects($this->once())->method('isValidHash')->willReturn(true); + $this->userMock->method('getOldPasswords')->willReturn([md5('pw1'), md5('pw2')]); + + try { + $this->model->checkAdminPasswordChange($eventObserverMock); + } catch (\Magento\Framework\Exception\LocalizedException $expected) { + return; + } + $this->fail('An expected exception has not been raised.'); + } + + public function testTrackAdminPassword() + { + $newPW = "mYn3wpassw0rd"; + $oldPW = "notsecure"; + $uid = 123; + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getObject']) + ->getMock(); + + /** @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject $userMock */ + $userMock = $this->getMockBuilder('Magento\User\Model\User') + ->disableOriginalConstructor() + ->setMethods(['getId', 'getNewPassword', 'getForceNewPassword']) + ->getMock(); + + $eventObserverMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getObject')->willReturn($userMock); + $userMock->expects($this->once())->method('getId')->willReturn($uid); + $userMock->expects($this->once())->method('getNewPassword')->willReturn($newPW); + $this->configInterfaceMock + ->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(1); + $userMock->expects($this->once())->method('getForceNewPassword')->willReturn(false); + $this->encryptorMock->expects($this->once())->method('getHash')->willReturn(md5($oldPW)); + + /** @var \Magento\Framework\Message\Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ + $collectionMock = $this->getMockBuilder('Magento\Framework\Message\Collection') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $this->managerInterfaceMock + ->expects($this->once()) + ->method('getMessages') + ->willReturn($collectionMock); + $this->authSessionMock->expects($this->once())->method('unsPciAdminUserIsPasswordExpired')->willReturn(null); + + $this->model->trackAdminNewPassword($eventObserverMock); + } + + public function testForceAdminPasswordChange() + { + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $eventObserverMock */ + $eventObserverMock = $this->getMockBuilder('Magento\Framework\Event\Observer') + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + /** @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ + $eventMock = $this->getMockBuilder('Magento\Framework\Event') + ->disableOriginalConstructor() + ->setMethods(['getControllerAction', 'getRequest']) + ->getMock(); + + $this->configInterfaceMock + ->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn(1); + $this->authSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(true); + $eventObserverMock->expects($this->atLeastOnce())->method('getEvent')->willReturn($eventMock); + /** @var \Magento\Framework\App\Action\Action $controllerMock */ + $controllerMock = $this->getMockBuilder('Magento\Framework\App\Action\AbstractAction') + ->disableOriginalConstructor() + ->setMethods(['getRedirect', 'getRequest']) + ->getMockForAbstractClass(); + /** @var \Magento\Framework\App\RequestInterface $requestMock */ + $requestMock = $this->getMockBuilder('Magento\Framework\App\RequestInterface') + ->disableOriginalConstructor() + ->setMethods(['getFullActionName', 'setDispatched']) + ->getMockForAbstractClass(); + $eventMock->expects($this->once())->method('getControllerAction')->willReturn($controllerMock); + $eventMock->expects($this->once())->method('getRequest')->willReturn($requestMock); + $this->authSessionMock->expects($this->once())->method('getPciAdminUserIsPasswordExpired')->willReturn(true); + $requestMock->expects($this->once())->method('getFullActionName')->willReturn('not_in_array'); + + $this->authSessionMock->expects($this->once())->method('clearStorage'); + $this->sessionMock->expects($this->once())->method('clearStorage'); + $this->managerInterfaceMock->expects($this->once())->method('addErrorMessage'); + $controllerMock->expects($this->once())->method('getRequest')->willReturn($requestMock); + $requestMock->expects($this->once())->method('setDispatched')->willReturn(false); + + $this->model->forceAdminPasswordChange($eventObserverMock); + } +} diff --git a/app/code/Magento/User/etc/adminhtml/di.xml b/app/code/Magento/User/etc/adminhtml/di.xml old mode 100644 new mode 100755 index ad550cbf5058d..4278e618075fa --- a/app/code/Magento/User/etc/adminhtml/di.xml +++ b/app/code/Magento/User/etc/adminhtml/di.xml @@ -7,7 +7,7 @@ --> - + Magento\Framework\Authorization diff --git a/app/code/Magento/User/etc/adminhtml/events.xml b/app/code/Magento/User/etc/adminhtml/events.xml old mode 100644 new mode 100755 index 01f8142a549d0..3bdfe2f33f7e6 --- a/app/code/Magento/User/etc/adminhtml/events.xml +++ b/app/code/Magento/User/etc/adminhtml/events.xml @@ -7,15 +7,15 @@ --> - + - + - + - + diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/User/Test/Repository/ConfigData.xml new file mode 100644 index 0000000000000..8101677711e3e --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Repository/ConfigData.xml @@ -0,0 +1,19 @@ + + + + + + + admin + 1 + 6 + 6 + + + + diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.php new file mode 100644 index 0000000000000..d9653f0c2c7ce --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.php @@ -0,0 +1,72 @@ +persist(); + $customAdmin->persist(); + /** @var User $incorrectUser */ + $incorrectUser = $fixtureFactory->createByCode( + 'user', + ['data' => ['username' => $customAdmin->getUsername(), 'password' => $incorrectPassword]] + ); + + // Steps and assertions + for ($i = 0; $i < $attempts; $i++) { + $assertUserFailedLoginMessage->processAssert($adminAuth, $incorrectUser); + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml new file mode 100644 index 0000000000000..5fda7b00a447b --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml @@ -0,0 +1,23 @@ + + + + + + default_lockout_failures + + + 6 + + + custom_admin_with_default_role + honey boo boo + 7 + + + + diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml index 692af412394d2..aeac825861854 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml @@ -7,6 +7,16 @@ --> + + System > Locked Users + Locked Users + + + + System > Manage Encryption Key + Encryption Key + + System > All Users Users diff --git a/dev/tests/integration/testsuite/Magento/Backend/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Backend/Helper/DataTest.php index 0ebf3de8ec0b7..eeb5ae06c6339 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Helper/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Helper/DataTest.php @@ -7,6 +7,7 @@ /** * @magentoAppArea adminhtml + * @magentoAppIsolation enabled */ class DataTest extends \PHPUnit_Framework_TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/AuthTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/AuthTest.php index 3f0e20d8a744b..b8d78918bb5e7 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/AuthTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/AuthTest.php @@ -11,6 +11,7 @@ * Test class for \Magento\Backend\Model\Auth. * * @magentoAppArea adminhtml + * @magentoAppIsolation enabled */ class AuthTest extends \PHPUnit_Framework_TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index bef1e03ea68ee..731836d648132 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -254,17 +254,17 @@ public function cssDirectiveDataProvider() 'CSS from theme' => [ TemplateTypesInterface::TYPE_HTML, 'file="css/email-1.css"', - 'color: #111;' + 'color: #111' ], 'CSS from parent theme' => [ TemplateTypesInterface::TYPE_HTML, 'file="css/email-2.css"', - 'color: #222;' + 'color: #222' ], 'CSS from grandparent theme' => [ TemplateTypesInterface::TYPE_HTML, 'file="css/email-3.css"', - 'color: #333;' + 'color: #333' ], 'Missing file parameter' => [ TemplateTypesInterface::TYPE_HTML, @@ -316,10 +316,8 @@ public function testInlinecssDirective( $this->model->setPlainTemplateMode($plainTemplateMode); $this->model->setIsChildTemplate($isChildTemplateMode); - if ($productionMode) { - $this->objectManager->get('Magento\Framework\App\State') - ->setMode(State::MODE_PRODUCTION); - } + $appMode = $productionMode ? State::MODE_PRODUCTION : State::MODE_DEVELOPER; + $this->objectManager->get('Magento\Framework\App\State')->setMode($appMode); $this->assertContains($expectedOutput, $this->model->filter($templateText)); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Api/ExtensionAttribute/JoinProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Api/ExtensionAttribute/JoinProcessorTest.php index b81c11211dbbe..e1c319834a5c9 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Api/ExtensionAttribute/JoinProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Api/ExtensionAttribute/JoinProcessorTest.php @@ -11,11 +11,16 @@ use Magento\Framework\Api\ExtensionAttribute\JoinDataInterfaceFactory; use Magento\Framework\Reflection\TypeProcessor; use Magento\Framework\App\Resource as AppResource; +use Magento\Framework\Api\ExtensionAttributesFactory; +/** + * Class to test the JoinProcessor functionality + */ class JoinProcessorTest extends \PHPUnit_Framework_TestCase { - - /** @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessor */ + /** + * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessor + */ private $joinProcessor; /** @@ -38,6 +43,16 @@ class JoinProcessorTest extends \PHPUnit_Framework_TestCase */ private $appResource; + /** + * @var ExtensionAttributesFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $extensionAttributesFactory; + + /** + * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorHelper + */ + private $joinProcessorHelper; + protected function setUp() { $this->config = $this->getMockBuilder('Magento\Framework\Api\ExtensionAttribute\Config') @@ -47,11 +62,10 @@ protected function setUp() ->getMockBuilder('Magento\Framework\Api\ExtensionAttribute\JoinDataInterfaceFactory') ->disableOriginalConstructor() ->getMock(); - $this->extensionAttributeJoinDataFactory = $this - ->getMockBuilder('Magento\Framework\Api\ExtensionAttribute\JoinDataInterfaceFactory') + $this->typeProcessor = $this->getMockBuilder('Magento\Framework\Reflection\TypeProcessor') ->disableOriginalConstructor() ->getMock(); - $this->typeProcessor = $this->getMockBuilder('Magento\Framework\Reflection\TypeProcessor') + $this->extensionAttributesFactory = $this->getMockBuilder('Magento\Framework\Api\ExtensionAttributesFactory') ->disableOriginalConstructor() ->getMock(); @@ -60,13 +74,20 @@ protected function setUp() $this->appResource = $objectManager->get('Magento\Framework\App\Resource'); + $this->joinProcessorHelper = $objectManager->create( + 'Magento\Framework\Api\ExtensionAttribute\JoinProcessorHelper', + [ + 'config' => $this->config, + 'joinDataInterfaceFactory' => $this->extensionAttributeJoinDataFactory + ] + ); + $this->joinProcessor = $objectManager->create( 'Magento\Framework\Api\ExtensionAttribute\JoinProcessor', [ 'objectManager' => $objectManager, - 'config' => $this->config, - 'extensionAttributeJoinDataFactory' => $this->extensionAttributeJoinDataFactory, - 'typeProcessor' => $this->typeProcessor + 'typeProcessor' => $this->typeProcessor, + 'joinProcessorHelper' => $this->joinProcessorHelper ] ); } @@ -81,8 +102,8 @@ public function testProcess() ->will($this->returnValue($this->getConfig())); $collection = $this->getMockBuilder('Magento\Framework\Data\Collection\AbstractDb') - ->setMethods(['joinExtensionAttribute']) ->disableOriginalConstructor() + ->setMethods(['joinExtensionAttribute']) ->getMockForAbstractClass(); $extensionAttributeJoinData = new JoinData(); @@ -112,6 +133,11 @@ public function testProcess() ); } + /** + * Will return the data that is expected from the config object + * + * @return array + */ private function getConfig() { return [ @@ -194,10 +220,17 @@ public function testProcessSqlSelectVerification() 'Magento\Framework\Api\ExtensionAttribute\Config', ['reader' => $configReader] ); + + /** @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorHelper $extensionAttributesProcessorHelper */ + $extensionAttributesProcessorHelper = $objectManager->create( + 'Magento\Framework\Api\ExtensionAttribute\JoinProcessorHelper', + ['config' => $config] + ); + /** @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessor $extensionAttributesProcessor */ $extensionAttributesProcessor = $objectManager->create( 'Magento\Framework\Api\ExtensionAttribute\JoinProcessor', - ['config' => $config] + ['joinProcessorHelper' => $extensionAttributesProcessorHelper] ); /** @var \Magento\Catalog\Model\Resource\Product\Collection $collection */ $collection = $objectManager->create('Magento\Catalog\Model\Resource\Product\Collection'); diff --git a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessor.php b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessor.php index dbe83d1d343a2..3f7ebf4ab4ae4 100644 --- a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessor.php +++ b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessor.php @@ -7,18 +7,14 @@ namespace Magento\Framework\Api\ExtensionAttribute; use Magento\Framework\Api\ExtensionAttribute\Config; -use Magento\Framework\Api\ExtensionAttribute\Config\Converter; +use Magento\Framework\Api\ExtensionAttribute\Config\Converter as Converter; use Magento\Framework\Data\Collection\AbstractDb as DbCollection; -use Magento\Framework\Api\ExtensionAttribute\JoinDataInterface; -use Magento\Framework\Api\ExtensionAttribute\JoinDataInterfaceFactory; use Magento\Framework\Reflection\TypeProcessor; use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\ExtensionAttributesFactory; -use Magento\Framework\Api\SimpleDataObjectConverter; /** * Join processor allows to join extension attributes during collections loading. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class JoinProcessor implements \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface { @@ -29,47 +25,33 @@ class JoinProcessor implements \Magento\Framework\Api\ExtensionAttribute\JoinPro */ protected $objectManager; - /** - * @var Config - */ - private $config; - - /** - * @var JoinDataInterfaceFactory - */ - private $extensionAttributeJoinDataFactory; - - /** - * @var TypeProcessor - */ + /** @var TypeProcessor */ private $typeProcessor; - /** - * @var ExtensionAttributesFactory - */ + /** @var ExtensionAttributesFactory */ private $extensionAttributesFactory; + /** @var JoinProcessorHelper */ + private $joinProcessorHelper; + /** * Initialize dependencies. * * @param \Magento\Framework\ObjectManagerInterface $objectManager - * @param Config $config - * @param JoinDataInterfaceFactory $extensionAttributeJoinDataFactory * @param TypeProcessor $typeProcessor * @param ExtensionAttributesFactory $extensionAttributesFactory + * @param JoinProcessorHelper $joinProcessorHelper */ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, - Config $config, - JoinDataInterfaceFactory $extensionAttributeJoinDataFactory, TypeProcessor $typeProcessor, - ExtensionAttributesFactory $extensionAttributesFactory + ExtensionAttributesFactory $extensionAttributesFactory, + JoinProcessorHelper $joinProcessorHelper ) { $this->objectManager = $objectManager; - $this->config = $config; - $this->extensionAttributeJoinDataFactory = $extensionAttributeJoinDataFactory; $this->typeProcessor = $typeProcessor; $this->extensionAttributesFactory = $extensionAttributesFactory; + $this->joinProcessorHelper = $joinProcessorHelper; } /** @@ -79,50 +61,22 @@ public function process(DbCollection $collection, $extensibleEntityClass = null) { $extensibleEntityClass = $extensibleEntityClass ?: $collection->getItemObjectClass(); $joinDirectives = $this->getJoinDirectivesForType($extensibleEntityClass); + foreach ($joinDirectives as $attributeCode => $directive) { /** @var JoinDataInterface $joinData */ - $joinData = $this->extensionAttributeJoinDataFactory->create(); + $joinData = $this->joinProcessorHelper->getJoinDataInterface(); $joinData->setAttributeCode($attributeCode) ->setReferenceTable($directive[Converter::JOIN_REFERENCE_TABLE]) ->setReferenceTableAlias($this->getReferenceTableAlias($attributeCode)) ->setReferenceField($directive[Converter::JOIN_REFERENCE_FIELD]) ->setJoinField($directive[Converter::JOIN_ON_FIELD]); $joinData->setSelectFields( - $this->getSelectFieldsMap($attributeCode, $directive[Converter::JOIN_FIELDS]) + $this->joinProcessorHelper->getSelectFieldsMap($attributeCode, $directive[Converter::JOIN_FIELDS]) ); $collection->joinExtensionAttribute($joinData, $this); } } - /** - * Generate a list of select fields with mapping of client facing attribute names to field names used in SQL select. - * - * @param string $attributeCode - * @param array $selectFields - * @return array - */ - private function getSelectFieldsMap($attributeCode, $selectFields) - { - $referenceTableAlias = $this->getReferenceTableAlias($attributeCode); - $useFieldInAlias = (count($selectFields) > 1); - $selectFieldsAliases = []; - foreach ($selectFields as $selectField) { - $internalFieldName = $selectField[Converter::JOIN_FIELD_COLUMN] - ? $selectField[Converter::JOIN_FIELD_COLUMN] - : $selectField[Converter::JOIN_FIELD]; - $setterName = 'set' - . ucfirst(SimpleDataObjectConverter::snakeCaseToCamelCase($selectField[Converter::JOIN_FIELD])); - $selectFieldsAliases[] = [ - JoinDataInterface::SELECT_FIELD_EXTERNAL_ALIAS => $attributeCode - . ($useFieldInAlias ? '.' . $selectField[Converter::JOIN_FIELD] : ''), - JoinDataInterface::SELECT_FIELD_INTERNAL_ALIAS => $referenceTableAlias . '_' . $internalFieldName, - JoinDataInterface::SELECT_FIELD_WITH_DB_PREFIX => $referenceTableAlias . '.' . $internalFieldName, - JoinDataInterface::SELECT_FIELD_SETTER => $setterName - ]; - } - return $selectFieldsAliases; - } - /** * Generate reference table alias. * @@ -180,7 +134,9 @@ private function populateAttributeCodeWithDirective( $extensibleEntityClass ) { $attributeType = $directive[Converter::DATA_TYPE]; - $selectFields = $this->getSelectFieldsMap($attributeCode, $directive[Converter::JOIN_FIELDS]); + $selectFields = $this->joinProcessorHelper + ->getSelectFieldsMap($attributeCode, $directive[Converter::JOIN_FIELDS]); + foreach ($selectFields as $selectField) { $internalAlias = $selectField[JoinDataInterface::SELECT_FIELD_INTERNAL_ALIAS]; if (isset($data[$internalAlias])) { @@ -223,7 +179,7 @@ private function getJoinDirectivesForType($extensibleEntityClass) $extensibleInterfaceName = $this->extensionAttributesFactory ->getExtensibleInterfaceName($extensibleEntityClass); $extensibleInterfaceName = ltrim($extensibleInterfaceName, '\\'); - $config = $this->config->get(); + $config = $this->joinProcessorHelper->getConfigData(); if (!isset($config[$extensibleInterfaceName])) { return []; } diff --git a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorHelper.php b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorHelper.php new file mode 100644 index 0000000000000..0ac6f092e18ed --- /dev/null +++ b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorHelper.php @@ -0,0 +1,98 @@ +config = $config; + $this->joinDataInterfaceFactory = $joinDataInterfaceFactory; + } + + /** + * Generate a list of select fields with mapping of client facing attribute names to field names used in SQL select. + * + * @param string $attributeCode + * @param array $selectFields + * @return array + */ + public function getSelectFieldsMap($attributeCode, $selectFields) + { + $referenceTableAlias = $this->getReferenceTableAlias($attributeCode); + $useFieldInAlias = (count($selectFields) > 1); + $selectFieldsAliases = []; + + foreach ($selectFields as $selectField) { + $internalFieldName = $selectField[Converter::JOIN_FIELD_COLUMN] + ? $selectField[Converter::JOIN_FIELD_COLUMN] + : $selectField[Converter::JOIN_FIELD]; + $setterName = 'set' + . ucfirst(SimpleDataObjectConverter::snakeCaseToCamelCase($selectField[Converter::JOIN_FIELD])); + $selectFieldsAliases[] = [ + JoinDataInterface::SELECT_FIELD_EXTERNAL_ALIAS => $attributeCode + . ($useFieldInAlias ? '.' . $selectField[Converter::JOIN_FIELD] : ''), + JoinDataInterface::SELECT_FIELD_INTERNAL_ALIAS => $referenceTableAlias . '_' . $internalFieldName, + JoinDataInterface::SELECT_FIELD_WITH_DB_PREFIX => $referenceTableAlias . '.' . $internalFieldName, + JoinDataInterface::SELECT_FIELD_SETTER => $setterName + ]; + } + return $selectFieldsAliases; + } + + /** + * Generate reference table alias. + * + * @param string $attributeCode + * @return string + */ + public function getReferenceTableAlias($attributeCode) + { + return 'extension_attribute_' . $attributeCode; + } + + /** + * Returns config data values + * + * @return array|mixed|null + */ + public function getConfigData() + { + return $this->config->get(); + } + + /** + * JoinDataInterface getter + * + * @return JoinDataInterface + */ + public function getJoinDataInterface() + { + return $this->joinDataInterfaceFactory->create(); + } +} diff --git a/lib/internal/Magento/Framework/View/Asset/Source.php b/lib/internal/Magento/Framework/View/Asset/Source.php index f3518c2fc3022..d47a76a4fe58c 100644 --- a/lib/internal/Magento/Framework/View/Asset/Source.php +++ b/lib/internal/Magento/Framework/View/Asset/Source.php @@ -122,7 +122,6 @@ public function getContent(LocalInterface $asset) private function preProcess(LocalInterface $asset) { $sourceFile = $this->findSourceFile($asset); - $dirCode = DirectoryList::ROOT; $path = $this->rootDir->getRelativePath($sourceFile); $chain = $this->createChain($asset, $path);