User.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\User\Model\ResourceModel;
  7. use Magento\Authorization\Model\Acl\Role\Group as RoleGroup;
  8. use Magento\Authorization\Model\Acl\Role\User as RoleUser;
  9. use Magento\Authorization\Model\UserContextInterface;
  10. use Magento\Framework\Acl\Data\CacheInterface;
  11. use Magento\Framework\App\ObjectManager;
  12. use Magento\User\Model\Backend\Config\ObserverConfig;
  13. use Magento\User\Model\User as ModelUser;
  14. /**
  15. * ACL user resource
  16. *
  17. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  18. * @api
  19. * @since 100.0.2
  20. */
  21. class User extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
  22. {
  23. /**
  24. * Role model
  25. *
  26. * @var \Magento\Authorization\Model\RoleFactory
  27. */
  28. protected $_roleFactory;
  29. /**
  30. * @var \Magento\Framework\Stdlib\DateTime
  31. */
  32. protected $dateTime;
  33. /**
  34. * @var CacheInterface
  35. */
  36. private $aclDataCache;
  37. /**
  38. * @var ObserverConfig|null
  39. */
  40. private $observerConfig;
  41. /**
  42. * Construct
  43. *
  44. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
  45. * @param \Magento\Authorization\Model\RoleFactory $roleFactory
  46. * @param \Magento\Framework\Stdlib\DateTime $dateTime
  47. * @param string $connectionName
  48. * @param CacheInterface $aclDataCache
  49. * @param ObserverConfig|null $observerConfig
  50. */
  51. public function __construct(
  52. \Magento\Framework\Model\ResourceModel\Db\Context $context,
  53. \Magento\Authorization\Model\RoleFactory $roleFactory,
  54. \Magento\Framework\Stdlib\DateTime $dateTime,
  55. $connectionName = null,
  56. CacheInterface $aclDataCache = null,
  57. ObserverConfig $observerConfig = null
  58. ) {
  59. parent::__construct($context, $connectionName);
  60. $this->_roleFactory = $roleFactory;
  61. $this->dateTime = $dateTime;
  62. $this->aclDataCache = $aclDataCache ?: ObjectManager::getInstance()->get(CacheInterface::class);
  63. $this->observerConfig = $observerConfig ?: ObjectManager::getInstance()->get(ObserverConfig::class);
  64. }
  65. /**
  66. * Define main table
  67. *
  68. * @return void
  69. */
  70. protected function _construct()
  71. {
  72. $this->_init('admin_user', 'user_id');
  73. }
  74. /**
  75. * Initialize unique fields
  76. *
  77. * @return $this
  78. */
  79. protected function _initUniqueFields()
  80. {
  81. $this->_uniqueFields = [
  82. ['field' => 'email', 'title' => __('Email')],
  83. ['field' => 'username', 'title' => __('User Name')],
  84. ];
  85. return $this;
  86. }
  87. /**
  88. * Authenticate user by $username and $password
  89. *
  90. * @param ModelUser $user
  91. * @return $this
  92. */
  93. public function recordLogin(ModelUser $user)
  94. {
  95. $connection = $this->getConnection();
  96. $data = [
  97. 'logdate' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT),
  98. 'lognum' => $user->getLognum() + 1,
  99. ];
  100. $condition = ['user_id = ?' => (int)$user->getUserId()];
  101. $connection->update($this->getMainTable(), $data, $condition);
  102. return $this;
  103. }
  104. /**
  105. * Load data by specified username
  106. *
  107. * @param string $username
  108. * @return array
  109. */
  110. public function loadByUsername($username)
  111. {
  112. $connection = $this->getConnection();
  113. $select = $connection->select()->from($this->getMainTable())->where('username=:username');
  114. $binds = ['username' => $username];
  115. return $connection->fetchRow($select, $binds);
  116. }
  117. /**
  118. * Check if user is assigned to any role
  119. *
  120. * @param int|ModelUser $user
  121. * @return null|array
  122. */
  123. public function hasAssigned2Role($user)
  124. {
  125. if (is_numeric($user)) {
  126. $userId = $user;
  127. } elseif ($user instanceof \Magento\Framework\Model\AbstractModel) {
  128. $userId = $user->getUserId();
  129. } else {
  130. return null;
  131. }
  132. if ($userId > 0) {
  133. $connection = $this->getConnection();
  134. $select = $connection->select();
  135. $select->from($this->getTable('authorization_role'))
  136. ->where('parent_id > :parent_id')
  137. ->where('user_id = :user_id')
  138. ->where('user_type = :user_type');
  139. $binds = ['parent_id' => 0, 'user_id' => $userId,
  140. 'user_type' => UserContextInterface::USER_TYPE_ADMIN
  141. ];
  142. return $connection->fetchAll($select, $binds);
  143. } else {
  144. return null;
  145. }
  146. }
  147. /**
  148. * Unserialize user extra data after user save
  149. *
  150. * @param \Magento\Framework\Model\AbstractModel $user
  151. * @return $this
  152. */
  153. protected function _afterSave(\Magento\Framework\Model\AbstractModel $user)
  154. {
  155. $user->setExtra($this->getSerializer()->unserialize($user->getExtra()));
  156. if ($user->hasRoleId()) {
  157. $this->_clearUserRoles($user);
  158. $this->_createUserRole($user->getRoleId(), $user);
  159. }
  160. return $this;
  161. }
  162. /**
  163. * Clear all user-specific roles of provided user
  164. *
  165. * @param ModelUser $user
  166. * @return void
  167. */
  168. public function _clearUserRoles(ModelUser $user)
  169. {
  170. $conditions = ['user_id = ?' => (int)$user->getId(), 'user_type = ?' => UserContextInterface::USER_TYPE_ADMIN];
  171. $this->getConnection()->delete($this->getTable('authorization_role'), $conditions);
  172. }
  173. /**
  174. * Create role for provided user of provided type
  175. *
  176. * @param int $parentId
  177. * @param ModelUser $user
  178. * @return void
  179. */
  180. protected function _createUserRole($parentId, ModelUser $user)
  181. {
  182. if ($parentId > 0) {
  183. /** @var \Magento\Authorization\Model\Role $parentRole */
  184. $parentRole = $this->_roleFactory->create()->load($parentId);
  185. } else {
  186. $role = new \Magento\Framework\DataObject();
  187. $role->setTreeLevel(0);
  188. }
  189. if ($parentRole->getId()) {
  190. $data = new \Magento\Framework\DataObject(
  191. [
  192. 'parent_id' => $parentRole->getId(),
  193. 'tree_level' => $parentRole->getTreeLevel() + 1,
  194. 'sort_order' => 0,
  195. 'role_type' => RoleUser::ROLE_TYPE,
  196. 'user_id' => $user->getId(),
  197. 'user_type' => UserContextInterface::USER_TYPE_ADMIN,
  198. 'role_name' => $user->getFirstName(),
  199. ]
  200. );
  201. $insertData = $this->_prepareDataForTable($data, $this->getTable('authorization_role'));
  202. $this->getConnection()->insert($this->getTable('authorization_role'), $insertData);
  203. $this->aclDataCache->clean();
  204. }
  205. }
  206. /**
  207. * Unserialize user extra data after user load
  208. *
  209. * @param \Magento\Framework\Model\AbstractModel $user
  210. * @return $this
  211. */
  212. protected function _afterLoad(\Magento\Framework\Model\AbstractModel $user)
  213. {
  214. if (is_string($user->getExtra())) {
  215. $user->setExtra($this->getSerializer()->unserialize($user->getExtra()));
  216. }
  217. return parent::_afterLoad($user);
  218. }
  219. /**
  220. * Delete user role record with user
  221. *
  222. * @param \Magento\Framework\Model\AbstractModel $user
  223. * @return bool
  224. * @throws \Magento\Framework\Exception\LocalizedException
  225. */
  226. public function delete(\Magento\Framework\Model\AbstractModel $user)
  227. {
  228. $this->_beforeDelete($user);
  229. $connection = $this->getConnection();
  230. $uid = $user->getId();
  231. $connection->beginTransaction();
  232. try {
  233. $connection->delete($this->getMainTable(), ['user_id = ?' => $uid]);
  234. $connection->delete(
  235. $this->getTable('authorization_role'),
  236. ['user_id = ?' => $uid, 'user_type = ?' => UserContextInterface::USER_TYPE_ADMIN]
  237. );
  238. } catch (\Magento\Framework\Exception\LocalizedException $e) {
  239. throw $e;
  240. } catch (\Exception $e) {
  241. $connection->rollBack();
  242. return false;
  243. }
  244. $connection->commit();
  245. $this->_afterDelete($user);
  246. return true;
  247. }
  248. /**
  249. * Get user roles
  250. *
  251. * @param \Magento\Framework\Model\AbstractModel $user
  252. * @return array
  253. */
  254. public function getRoles(\Magento\Framework\Model\AbstractModel $user)
  255. {
  256. if (!$user->getId()) {
  257. return [];
  258. }
  259. $table = $this->getTable('authorization_role');
  260. $connection = $this->getConnection();
  261. $select = $connection->select()->from(
  262. $table,
  263. []
  264. )->joinLeft(
  265. ['ar' => $table],
  266. "(ar.role_id = {$table}.parent_id and ar.role_type = '" . RoleGroup::ROLE_TYPE . "')",
  267. ['role_id']
  268. )->where(
  269. "{$table}.user_id = :user_id"
  270. )->where(
  271. "{$table}.user_type = :user_type"
  272. );
  273. $binds = ['user_id' => (int)$user->getId(),
  274. 'user_type' => UserContextInterface::USER_TYPE_ADMIN
  275. ];
  276. $roles = $connection->fetchCol($select, $binds);
  277. if ($roles) {
  278. return $roles;
  279. }
  280. return [];
  281. }
  282. /**
  283. * Delete user role
  284. *
  285. * @param \Magento\Framework\Model\AbstractModel $user
  286. * @return $this
  287. */
  288. public function deleteFromRole(\Magento\Framework\Model\AbstractModel $user)
  289. {
  290. if ($user->getUserId() <= 0) {
  291. return $this;
  292. }
  293. if ($user->getRoleId() <= 0) {
  294. return $this;
  295. }
  296. $dbh = $this->getConnection();
  297. $condition = [
  298. 'user_id = ?' => (int)$user->getId(),
  299. 'parent_id = ?' => (int)$user->getRoleId(),
  300. 'user_type = ?' => UserContextInterface::USER_TYPE_ADMIN
  301. ];
  302. $dbh->delete($this->getTable('authorization_role'), $condition);
  303. return $this;
  304. }
  305. /**
  306. * Check if role user exists
  307. *
  308. * @param \Magento\Framework\Model\AbstractModel $user
  309. * @return array
  310. */
  311. public function roleUserExists(\Magento\Framework\Model\AbstractModel $user)
  312. {
  313. if ($user->getUserId() > 0) {
  314. $roleTable = $this->getTable('authorization_role');
  315. $dbh = $this->getConnection();
  316. $binds = [
  317. 'parent_id' => $user->getRoleId(),
  318. 'user_id' => $user->getUserId(),
  319. 'user_type' => UserContextInterface::USER_TYPE_ADMIN
  320. ];
  321. $select = $dbh->select()->from($roleTable)
  322. ->where('parent_id = :parent_id')
  323. ->where('user_type = :user_type')
  324. ->where('user_id = :user_id');
  325. return $dbh->fetchCol($select, $binds);
  326. } else {
  327. return [];
  328. }
  329. }
  330. /**
  331. * Check if user exists
  332. *
  333. * @param \Magento\Framework\Model\AbstractModel $user
  334. * @return array
  335. */
  336. public function userExists(\Magento\Framework\Model\AbstractModel $user)
  337. {
  338. $connection = $this->getConnection();
  339. $select = $connection->select();
  340. $binds = [
  341. 'username' => $user->getUsername(),
  342. 'email' => $user->getEmail(),
  343. 'user_id' => (int)$user->getId(),
  344. ];
  345. $select->from(
  346. $this->getMainTable()
  347. )->where(
  348. '(username = :username OR email = :email)'
  349. )->where(
  350. 'user_id <> :user_id'
  351. );
  352. return $connection->fetchRow($select, $binds);
  353. }
  354. /**
  355. * Whether a user's identity is confirmed
  356. *
  357. * @param \Magento\Framework\Model\AbstractModel $user
  358. * @return bool
  359. */
  360. public function isUserUnique(\Magento\Framework\Model\AbstractModel $user)
  361. {
  362. return !$this->userExists($user);
  363. }
  364. /**
  365. * Save user extra data
  366. *
  367. * @param \Magento\Framework\Model\AbstractModel $object
  368. * @param string $data
  369. * @return $this
  370. */
  371. public function saveExtra($object, $data)
  372. {
  373. if ($object->getId()) {
  374. $this->getConnection()->update(
  375. $this->getMainTable(),
  376. ['extra' => $data],
  377. ['user_id = ?' => (int)$object->getId()]
  378. );
  379. }
  380. return $this;
  381. }
  382. /**
  383. * Retrieve the total user count bypassing any filters applied to collections
  384. *
  385. * @return int
  386. */
  387. public function countAll()
  388. {
  389. $connection = $this->getConnection();
  390. $select = $connection->select();
  391. $select->from($this->getMainTable(), 'COUNT(*)');
  392. $result = (int)$connection->fetchOne($select);
  393. return $result;
  394. }
  395. /**
  396. * Add validation rules to be applied before saving an entity
  397. *
  398. * @return \Zend_Validate_Interface $validator
  399. */
  400. public function getValidationRulesBeforeSave()
  401. {
  402. $userIdentity = new \Zend_Validate_Callback([$this, 'isUserUnique']);
  403. $userIdentity->setMessage(
  404. __('A user with the same user name or email already exists.'),
  405. \Zend_Validate_Callback::INVALID_VALUE
  406. );
  407. return $userIdentity;
  408. }
  409. /**
  410. * Update role users ACL
  411. *
  412. * @param \Magento\Authorization\Model\Role $role
  413. * @return bool
  414. */
  415. public function updateRoleUsersAcl(\Magento\Authorization\Model\Role $role)
  416. {
  417. $connection = $this->getConnection();
  418. $users = $role->getRoleUsers();
  419. $rowsCount = 0;
  420. if (sizeof($users) > 0) {
  421. $bind = ['reload_acl_flag' => 1];
  422. $where = ['user_id IN(?)' => $users];
  423. $rowsCount = $connection->update($this->getTable('admin_user'), $bind, $where);
  424. }
  425. return $rowsCount > 0;
  426. }
  427. /**
  428. * Unlock specified user record(s)
  429. *
  430. * @param int|int[] $userIds
  431. * @return int number of affected rows
  432. */
  433. public function unlock($userIds)
  434. {
  435. if (!is_array($userIds)) {
  436. $userIds = [$userIds];
  437. }
  438. return $this->getConnection()->update(
  439. $this->getMainTable(),
  440. ['failures_num' => 0, 'first_failure' => null, 'lock_expires' => null],
  441. $this->getIdFieldName() . ' IN (' . $this->getConnection()->quote($userIds) . ')'
  442. );
  443. }
  444. /**
  445. * Lock specified user record(s)
  446. *
  447. * @param int|int[] $userIds
  448. * @param int $exceptId
  449. * @param int $lifetime
  450. * @return int number of affected rows
  451. */
  452. public function lock($userIds, $exceptId, $lifetime)
  453. {
  454. if (!is_array($userIds)) {
  455. $userIds = [$userIds];
  456. }
  457. $exceptId = (int)$exceptId;
  458. return $this->getConnection()->update(
  459. $this->getMainTable(),
  460. ['lock_expires' => $this->dateTime->formatDate(time() + $lifetime)],
  461. "{$this->getIdFieldName()} IN (" . $this->getConnection()->quote(
  462. $userIds
  463. ) . ")\n AND {$this->getIdFieldName()} <> {$exceptId}"
  464. );
  465. }
  466. /**
  467. * Increment failures count along with updating lock expire and first failure dates
  468. *
  469. * @param ModelUser $user
  470. * @param int|bool $setLockExpires
  471. * @param int|bool $setFirstFailure
  472. * @return void
  473. */
  474. public function updateFailure($user, $setLockExpires = false, $setFirstFailure = false)
  475. {
  476. $update = ['failures_num' => new \Zend_Db_Expr('failures_num + 1')];
  477. if (false !== $setFirstFailure) {
  478. $update['first_failure'] = $this->dateTime->formatDate($setFirstFailure);
  479. $update['failures_num'] = 1;
  480. }
  481. if (false !== $setLockExpires) {
  482. $update['lock_expires'] = $this->dateTime->formatDate($setLockExpires);
  483. }
  484. $this->getConnection()->update(
  485. $this->getMainTable(),
  486. $update,
  487. $this->getConnection()->quoteInto("{$this->getIdFieldName()} = ?", $user->getId())
  488. );
  489. }
  490. /**
  491. * Purge and get remaining old password hashes
  492. *
  493. * @param ModelUser $user
  494. * @param int $retainLimit
  495. * @return array
  496. */
  497. public function getOldPasswords($user, $retainLimit = 4)
  498. {
  499. $userId = (int)$user->getId();
  500. $table = $this->getTable('admin_passwords');
  501. // purge expired passwords, except those which should be retained
  502. $retainPasswordIds = $this->getConnection()->fetchCol(
  503. $this->getConnection()
  504. ->select()
  505. ->from($table, 'password_id')
  506. ->where('user_id = :user_id')
  507. ->order('password_id ' . \Magento\Framework\DB\Select::SQL_DESC)
  508. ->limit($retainLimit),
  509. [':user_id' => $userId]
  510. );
  511. $where = [
  512. 'user_id = ?' => $userId,
  513. 'last_updated <= ?' => time() - $this->observerConfig->getAdminPasswordLifetime()
  514. ];
  515. if ($retainPasswordIds) {
  516. $where['password_id NOT IN (?)'] = $retainPasswordIds;
  517. }
  518. $this->getConnection()->delete($table, $where);
  519. // get all remaining passwords
  520. return $this->getConnection()->fetchCol(
  521. $this->getConnection()
  522. ->select()
  523. ->from($table, 'password_hash')
  524. ->where('user_id = :user_id'),
  525. [':user_id' => $userId]
  526. );
  527. }
  528. /**
  529. * Remember a password hash for further usage
  530. *
  531. * @param ModelUser $user
  532. * @param string $passwordHash
  533. * @param int $lifetime deprecated, password expiration date doesn't save anymore,
  534. * it is calculated in runtime based on password created date and lifetime config value
  535. * @return void
  536. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  537. *
  538. * @see \Magento\User\Model\Backend\Config\ObserverConfig::_isLatestPasswordExpired()
  539. */
  540. public function trackPassword($user, $passwordHash, $lifetime = 0)
  541. {
  542. $this->getConnection()->insert(
  543. $this->getTable('admin_passwords'),
  544. [
  545. 'user_id' => $user->getId(),
  546. 'password_hash' => $passwordHash,
  547. 'last_updated' => time()
  548. ]
  549. );
  550. }
  551. /**
  552. * Get latest password for specified user id
  553. * Possible false positive when password was changed several times with different lifetime configuration
  554. *
  555. * @param int $userId
  556. * @return array
  557. */
  558. public function getLatestPassword($userId)
  559. {
  560. return $this->getConnection()->fetchRow(
  561. $this->getConnection()
  562. ->select()
  563. ->from($this->getTable('admin_passwords'))
  564. ->where('user_id = :user_id')
  565. ->order('password_id ' . \Magento\Framework\DB\Select::SQL_DESC)
  566. ->limit(1),
  567. [':user_id' => $userId]
  568. );
  569. }
  570. }