DbManager.php 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\rbac;
  8. use Yii;
  9. use yii\base\InvalidArgumentException;
  10. use yii\base\InvalidCallException;
  11. use yii\caching\CacheInterface;
  12. use yii\db\Connection;
  13. use yii\db\Expression;
  14. use yii\db\Query;
  15. use yii\di\Instance;
  16. /**
  17. * DbManager represents an authorization manager that stores authorization information in database.
  18. *
  19. * The database connection is specified by [[db]]. The database schema could be initialized by applying migration:
  20. *
  21. * ```
  22. * yii migrate --migrationPath=@yii/rbac/migrations/
  23. * ```
  24. *
  25. * If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
  26. *
  27. * You may change the names of the tables used to store the authorization and rule data by setting [[itemTable]],
  28. * [[itemChildTable]], [[assignmentTable]] and [[ruleTable]].
  29. *
  30. * For more details and usage information on DbManager, see the [guide article on security authorization](guide:security-authorization).
  31. *
  32. * @author Qiang Xue <qiang.xue@gmail.com>
  33. * @author Alexander Kochetov <creocoder@gmail.com>
  34. * @since 2.0
  35. */
  36. class DbManager extends BaseManager
  37. {
  38. /**
  39. * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
  40. * After the DbManager object is created, if you want to change this property, you should only assign it
  41. * with a DB connection object.
  42. * Starting from version 2.0.2, this can also be a configuration array for creating the object.
  43. */
  44. public $db = 'db';
  45. /**
  46. * @var string the name of the table storing authorization items. Defaults to "auth_item".
  47. */
  48. public $itemTable = '{{%auth_item}}';
  49. /**
  50. * @var string the name of the table storing authorization item hierarchy. Defaults to "auth_item_child".
  51. */
  52. public $itemChildTable = '{{%auth_item_child}}';
  53. /**
  54. * @var string the name of the table storing authorization item assignments. Defaults to "auth_assignment".
  55. */
  56. public $assignmentTable = '{{%auth_assignment}}';
  57. /**
  58. * @var string the name of the table storing rules. Defaults to "auth_rule".
  59. */
  60. public $ruleTable = '{{%auth_rule}}';
  61. /**
  62. * @var CacheInterface|array|string the cache used to improve RBAC performance. This can be one of the following:
  63. *
  64. * - an application component ID (e.g. `cache`)
  65. * - a configuration array
  66. * - a [[\yii\caching\Cache]] object
  67. *
  68. * When this is not set, it means caching is not enabled.
  69. *
  70. * Note that by enabling RBAC cache, all auth items, rules and auth item parent-child relationships will
  71. * be cached and loaded into memory. This will improve the performance of RBAC permission check. However,
  72. * it does require extra memory and as a result may not be appropriate if your RBAC system contains too many
  73. * auth items. You should seek other RBAC implementations (e.g. RBAC based on Redis storage) in this case.
  74. *
  75. * Also note that if you modify RBAC items, rules or parent-child relationships from outside of this component,
  76. * you have to manually call [[invalidateCache()]] to ensure data consistency.
  77. *
  78. * @since 2.0.3
  79. */
  80. public $cache;
  81. /**
  82. * @var string the key used to store RBAC data in cache
  83. * @see cache
  84. * @since 2.0.3
  85. */
  86. public $cacheKey = 'rbac';
  87. /**
  88. * @var Item[] all auth items (name => Item)
  89. */
  90. protected $items;
  91. /**
  92. * @var Rule[] all auth rules (name => Rule)
  93. */
  94. protected $rules;
  95. /**
  96. * @var array auth item parent-child relationships (childName => list of parents)
  97. */
  98. protected $parents;
  99. /**
  100. * Initializes the application component.
  101. * This method overrides the parent implementation by establishing the database connection.
  102. */
  103. public function init()
  104. {
  105. parent::init();
  106. $this->db = Instance::ensure($this->db, Connection::className());
  107. if ($this->cache !== null) {
  108. $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
  109. }
  110. }
  111. private $_checkAccessAssignments = [];
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function checkAccess($userId, $permissionName, $params = [])
  116. {
  117. if (isset($this->_checkAccessAssignments[(string) $userId])) {
  118. $assignments = $this->_checkAccessAssignments[(string) $userId];
  119. } else {
  120. $assignments = $this->getAssignments($userId);
  121. $this->_checkAccessAssignments[(string) $userId] = $assignments;
  122. }
  123. if ($this->hasNoAssignments($assignments)) {
  124. return false;
  125. }
  126. $this->loadFromCache();
  127. if ($this->items !== null) {
  128. return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
  129. }
  130. return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
  131. }
  132. /**
  133. * Performs access check for the specified user based on the data loaded from cache.
  134. * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
  135. * @param string|int $user the user ID. This should can be either an integer or a string representing
  136. * the unique identifier of a user. See [[\yii\web\User::id]].
  137. * @param string $itemName the name of the operation that need access check
  138. * @param array $params name-value pairs that would be passed to rules associated
  139. * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
  140. * which holds the value of `$userId`.
  141. * @param Assignment[] $assignments the assignments to the specified user
  142. * @return bool whether the operations can be performed by the user.
  143. * @since 2.0.3
  144. */
  145. protected function checkAccessFromCache($user, $itemName, $params, $assignments)
  146. {
  147. if (!isset($this->items[$itemName])) {
  148. return false;
  149. }
  150. $item = $this->items[$itemName];
  151. Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
  152. if (!$this->executeRule($user, $item, $params)) {
  153. return false;
  154. }
  155. if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
  156. return true;
  157. }
  158. if (!empty($this->parents[$itemName])) {
  159. foreach ($this->parents[$itemName] as $parent) {
  160. if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
  161. return true;
  162. }
  163. }
  164. }
  165. return false;
  166. }
  167. /**
  168. * Performs access check for the specified user.
  169. * This method is internally called by [[checkAccess()]].
  170. * @param string|int $user the user ID. This should can be either an integer or a string representing
  171. * the unique identifier of a user. See [[\yii\web\User::id]].
  172. * @param string $itemName the name of the operation that need access check
  173. * @param array $params name-value pairs that would be passed to rules associated
  174. * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
  175. * which holds the value of `$userId`.
  176. * @param Assignment[] $assignments the assignments to the specified user
  177. * @return bool whether the operations can be performed by the user.
  178. */
  179. protected function checkAccessRecursive($user, $itemName, $params, $assignments)
  180. {
  181. if (($item = $this->getItem($itemName)) === null) {
  182. return false;
  183. }
  184. Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
  185. if (!$this->executeRule($user, $item, $params)) {
  186. return false;
  187. }
  188. if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
  189. return true;
  190. }
  191. $query = new Query();
  192. $parents = $query->select(['parent'])
  193. ->from($this->itemChildTable)
  194. ->where(['child' => $itemName])
  195. ->column($this->db);
  196. foreach ($parents as $parent) {
  197. if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
  198. return true;
  199. }
  200. }
  201. return false;
  202. }
  203. /**
  204. * {@inheritdoc}
  205. */
  206. protected function getItem($name)
  207. {
  208. if (empty($name)) {
  209. return null;
  210. }
  211. if (!empty($this->items[$name])) {
  212. return $this->items[$name];
  213. }
  214. $row = (new Query())->from($this->itemTable)
  215. ->where(['name' => $name])
  216. ->one($this->db);
  217. if ($row === false) {
  218. return null;
  219. }
  220. return $this->populateItem($row);
  221. }
  222. /**
  223. * Returns a value indicating whether the database supports cascading update and delete.
  224. * The default implementation will return false for SQLite database and true for all other databases.
  225. * @return bool whether the database supports cascading update and delete.
  226. */
  227. protected function supportsCascadeUpdate()
  228. {
  229. return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
  230. }
  231. /**
  232. * {@inheritdoc}
  233. */
  234. protected function addItem($item)
  235. {
  236. $time = time();
  237. if ($item->createdAt === null) {
  238. $item->createdAt = $time;
  239. }
  240. if ($item->updatedAt === null) {
  241. $item->updatedAt = $time;
  242. }
  243. $this->db->createCommand()
  244. ->insert($this->itemTable, [
  245. 'name' => $item->name,
  246. 'type' => $item->type,
  247. 'description' => $item->description,
  248. 'rule_name' => $item->ruleName,
  249. 'data' => $item->data === null ? null : serialize($item->data),
  250. 'created_at' => $item->createdAt,
  251. 'updated_at' => $item->updatedAt,
  252. ])->execute();
  253. $this->invalidateCache();
  254. return true;
  255. }
  256. /**
  257. * {@inheritdoc}
  258. */
  259. protected function removeItem($item)
  260. {
  261. if (!$this->supportsCascadeUpdate()) {
  262. $this->db->createCommand()
  263. ->delete($this->itemChildTable, ['or', '[[parent]]=:name', '[[child]]=:name'], [':name' => $item->name])
  264. ->execute();
  265. $this->db->createCommand()
  266. ->delete($this->assignmentTable, ['item_name' => $item->name])
  267. ->execute();
  268. }
  269. $this->db->createCommand()
  270. ->delete($this->itemTable, ['name' => $item->name])
  271. ->execute();
  272. $this->invalidateCache();
  273. return true;
  274. }
  275. /**
  276. * {@inheritdoc}
  277. */
  278. protected function updateItem($name, $item)
  279. {
  280. if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
  281. $this->db->createCommand()
  282. ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
  283. ->execute();
  284. $this->db->createCommand()
  285. ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
  286. ->execute();
  287. $this->db->createCommand()
  288. ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
  289. ->execute();
  290. }
  291. $item->updatedAt = time();
  292. $this->db->createCommand()
  293. ->update($this->itemTable, [
  294. 'name' => $item->name,
  295. 'description' => $item->description,
  296. 'rule_name' => $item->ruleName,
  297. 'data' => $item->data === null ? null : serialize($item->data),
  298. 'updated_at' => $item->updatedAt,
  299. ], [
  300. 'name' => $name,
  301. ])->execute();
  302. $this->invalidateCache();
  303. return true;
  304. }
  305. /**
  306. * {@inheritdoc}
  307. */
  308. protected function addRule($rule)
  309. {
  310. $time = time();
  311. if ($rule->createdAt === null) {
  312. $rule->createdAt = $time;
  313. }
  314. if ($rule->updatedAt === null) {
  315. $rule->updatedAt = $time;
  316. }
  317. $this->db->createCommand()
  318. ->insert($this->ruleTable, [
  319. 'name' => $rule->name,
  320. 'data' => serialize($rule),
  321. 'created_at' => $rule->createdAt,
  322. 'updated_at' => $rule->updatedAt,
  323. ])->execute();
  324. $this->invalidateCache();
  325. return true;
  326. }
  327. /**
  328. * {@inheritdoc}
  329. */
  330. protected function updateRule($name, $rule)
  331. {
  332. if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
  333. $this->db->createCommand()
  334. ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
  335. ->execute();
  336. }
  337. $rule->updatedAt = time();
  338. $this->db->createCommand()
  339. ->update($this->ruleTable, [
  340. 'name' => $rule->name,
  341. 'data' => serialize($rule),
  342. 'updated_at' => $rule->updatedAt,
  343. ], [
  344. 'name' => $name,
  345. ])->execute();
  346. $this->invalidateCache();
  347. return true;
  348. }
  349. /**
  350. * {@inheritdoc}
  351. */
  352. protected function removeRule($rule)
  353. {
  354. if (!$this->supportsCascadeUpdate()) {
  355. $this->db->createCommand()
  356. ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
  357. ->execute();
  358. }
  359. $this->db->createCommand()
  360. ->delete($this->ruleTable, ['name' => $rule->name])
  361. ->execute();
  362. $this->invalidateCache();
  363. return true;
  364. }
  365. /**
  366. * {@inheritdoc}
  367. */
  368. protected function getItems($type)
  369. {
  370. $query = (new Query())
  371. ->from($this->itemTable)
  372. ->where(['type' => $type]);
  373. $items = [];
  374. foreach ($query->all($this->db) as $row) {
  375. $items[$row['name']] = $this->populateItem($row);
  376. }
  377. return $items;
  378. }
  379. /**
  380. * Populates an auth item with the data fetched from database.
  381. * @param array $row the data from the auth item table
  382. * @return Item the populated auth item instance (either Role or Permission)
  383. */
  384. protected function populateItem($row)
  385. {
  386. $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
  387. if (!isset($row['data']) || ($data = @unserialize(is_resource($row['data']) ? stream_get_contents($row['data']) : $row['data'])) === false) {
  388. $data = null;
  389. }
  390. return new $class([
  391. 'name' => $row['name'],
  392. 'type' => $row['type'],
  393. 'description' => $row['description'],
  394. 'ruleName' => $row['rule_name'] ?: null,
  395. 'data' => $data,
  396. 'createdAt' => $row['created_at'],
  397. 'updatedAt' => $row['updated_at'],
  398. ]);
  399. }
  400. /**
  401. * {@inheritdoc}
  402. * The roles returned by this method include the roles assigned via [[$defaultRoles]].
  403. */
  404. public function getRolesByUser($userId)
  405. {
  406. if ($this->isEmptyUserId($userId)) {
  407. return [];
  408. }
  409. $query = (new Query())->select('b.*')
  410. ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
  411. ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
  412. ->andWhere(['a.user_id' => (string) $userId])
  413. ->andWhere(['b.type' => Item::TYPE_ROLE]);
  414. $roles = $this->getDefaultRoleInstances();
  415. foreach ($query->all($this->db) as $row) {
  416. $roles[$row['name']] = $this->populateItem($row);
  417. }
  418. return $roles;
  419. }
  420. /**
  421. * {@inheritdoc}
  422. */
  423. public function getChildRoles($roleName)
  424. {
  425. $role = $this->getRole($roleName);
  426. if ($role === null) {
  427. throw new InvalidArgumentException("Role \"$roleName\" not found.");
  428. }
  429. $result = [];
  430. $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);
  431. $roles = [$roleName => $role];
  432. $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
  433. return array_key_exists($roleItem->name, $result);
  434. });
  435. return $roles;
  436. }
  437. /**
  438. * {@inheritdoc}
  439. */
  440. public function getPermissionsByRole($roleName)
  441. {
  442. $childrenList = $this->getChildrenList();
  443. $result = [];
  444. $this->getChildrenRecursive($roleName, $childrenList, $result);
  445. if (empty($result)) {
  446. return [];
  447. }
  448. $query = (new Query())->from($this->itemTable)->where([
  449. 'type' => Item::TYPE_PERMISSION,
  450. 'name' => array_keys($result),
  451. ]);
  452. $permissions = [];
  453. foreach ($query->all($this->db) as $row) {
  454. $permissions[$row['name']] = $this->populateItem($row);
  455. }
  456. return $permissions;
  457. }
  458. /**
  459. * {@inheritdoc}
  460. */
  461. public function getPermissionsByUser($userId)
  462. {
  463. if ($this->isEmptyUserId($userId)) {
  464. return [];
  465. }
  466. $directPermission = $this->getDirectPermissionsByUser($userId);
  467. $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
  468. return array_merge($directPermission, $inheritedPermission);
  469. }
  470. /**
  471. * Returns all permissions that are directly assigned to user.
  472. * @param string|int $userId the user ID (see [[\yii\web\User::id]])
  473. * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
  474. * @since 2.0.7
  475. */
  476. protected function getDirectPermissionsByUser($userId)
  477. {
  478. $query = (new Query())->select('b.*')
  479. ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
  480. ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
  481. ->andWhere(['a.user_id' => (string) $userId])
  482. ->andWhere(['b.type' => Item::TYPE_PERMISSION]);
  483. $permissions = [];
  484. foreach ($query->all($this->db) as $row) {
  485. $permissions[$row['name']] = $this->populateItem($row);
  486. }
  487. return $permissions;
  488. }
  489. /**
  490. * Returns all permissions that the user inherits from the roles assigned to him.
  491. * @param string|int $userId the user ID (see [[\yii\web\User::id]])
  492. * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
  493. * @since 2.0.7
  494. */
  495. protected function getInheritedPermissionsByUser($userId)
  496. {
  497. $query = (new Query())->select('item_name')
  498. ->from($this->assignmentTable)
  499. ->where(['user_id' => (string) $userId]);
  500. $childrenList = $this->getChildrenList();
  501. $result = [];
  502. foreach ($query->column($this->db) as $roleName) {
  503. $this->getChildrenRecursive($roleName, $childrenList, $result);
  504. }
  505. if (empty($result)) {
  506. return [];
  507. }
  508. $query = (new Query())->from($this->itemTable)->where([
  509. 'type' => Item::TYPE_PERMISSION,
  510. 'name' => array_keys($result),
  511. ]);
  512. $permissions = [];
  513. foreach ($query->all($this->db) as $row) {
  514. $permissions[$row['name']] = $this->populateItem($row);
  515. }
  516. return $permissions;
  517. }
  518. /**
  519. * Returns the children for every parent.
  520. * @return array the children list. Each array key is a parent item name,
  521. * and the corresponding array value is a list of child item names.
  522. */
  523. protected function getChildrenList()
  524. {
  525. $query = (new Query())->from($this->itemChildTable);
  526. $parents = [];
  527. foreach ($query->all($this->db) as $row) {
  528. $parents[$row['parent']][] = $row['child'];
  529. }
  530. return $parents;
  531. }
  532. /**
  533. * Recursively finds all children and grand children of the specified item.
  534. * @param string $name the name of the item whose children are to be looked for.
  535. * @param array $childrenList the child list built via [[getChildrenList()]]
  536. * @param array $result the children and grand children (in array keys)
  537. */
  538. protected function getChildrenRecursive($name, $childrenList, &$result)
  539. {
  540. if (isset($childrenList[$name])) {
  541. foreach ($childrenList[$name] as $child) {
  542. $result[$child] = true;
  543. $this->getChildrenRecursive($child, $childrenList, $result);
  544. }
  545. }
  546. }
  547. /**
  548. * {@inheritdoc}
  549. */
  550. public function getRule($name)
  551. {
  552. if ($this->rules !== null) {
  553. return isset($this->rules[$name]) ? $this->rules[$name] : null;
  554. }
  555. $row = (new Query())->select(['data'])
  556. ->from($this->ruleTable)
  557. ->where(['name' => $name])
  558. ->one($this->db);
  559. if ($row === false) {
  560. return null;
  561. }
  562. $data = $row['data'];
  563. if (is_resource($data)) {
  564. $data = stream_get_contents($data);
  565. }
  566. return unserialize($data);
  567. }
  568. /**
  569. * {@inheritdoc}
  570. */
  571. public function getRules()
  572. {
  573. if ($this->rules !== null) {
  574. return $this->rules;
  575. }
  576. $query = (new Query())->from($this->ruleTable);
  577. $rules = [];
  578. foreach ($query->all($this->db) as $row) {
  579. $data = $row['data'];
  580. if (is_resource($data)) {
  581. $data = stream_get_contents($data);
  582. }
  583. $rules[$row['name']] = unserialize($data);
  584. }
  585. return $rules;
  586. }
  587. /**
  588. * {@inheritdoc}
  589. */
  590. public function getAssignment($roleName, $userId)
  591. {
  592. if ($this->isEmptyUserId($userId)) {
  593. return null;
  594. }
  595. $row = (new Query())->from($this->assignmentTable)
  596. ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
  597. ->one($this->db);
  598. if ($row === false) {
  599. return null;
  600. }
  601. return new Assignment([
  602. 'userId' => $row['user_id'],
  603. 'roleName' => $row['item_name'],
  604. 'createdAt' => $row['created_at'],
  605. ]);
  606. }
  607. /**
  608. * {@inheritdoc}
  609. */
  610. public function getAssignments($userId)
  611. {
  612. if ($this->isEmptyUserId($userId)) {
  613. return [];
  614. }
  615. $query = (new Query())
  616. ->from($this->assignmentTable)
  617. ->where(['user_id' => (string) $userId]);
  618. $assignments = [];
  619. foreach ($query->all($this->db) as $row) {
  620. $assignments[$row['item_name']] = new Assignment([
  621. 'userId' => $row['user_id'],
  622. 'roleName' => $row['item_name'],
  623. 'createdAt' => $row['created_at'],
  624. ]);
  625. }
  626. return $assignments;
  627. }
  628. /**
  629. * {@inheritdoc}
  630. * @since 2.0.8
  631. */
  632. public function canAddChild($parent, $child)
  633. {
  634. return !$this->detectLoop($parent, $child);
  635. }
  636. /**
  637. * {@inheritdoc}
  638. */
  639. public function addChild($parent, $child)
  640. {
  641. if ($parent->name === $child->name) {
  642. throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
  643. }
  644. if ($parent instanceof Permission && $child instanceof Role) {
  645. throw new InvalidArgumentException('Cannot add a role as a child of a permission.');
  646. }
  647. if ($this->detectLoop($parent, $child)) {
  648. throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
  649. }
  650. $this->db->createCommand()
  651. ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
  652. ->execute();
  653. $this->invalidateCache();
  654. return true;
  655. }
  656. /**
  657. * {@inheritdoc}
  658. */
  659. public function removeChild($parent, $child)
  660. {
  661. $result = $this->db->createCommand()
  662. ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
  663. ->execute() > 0;
  664. $this->invalidateCache();
  665. return $result;
  666. }
  667. /**
  668. * {@inheritdoc}
  669. */
  670. public function removeChildren($parent)
  671. {
  672. $result = $this->db->createCommand()
  673. ->delete($this->itemChildTable, ['parent' => $parent->name])
  674. ->execute() > 0;
  675. $this->invalidateCache();
  676. return $result;
  677. }
  678. /**
  679. * {@inheritdoc}
  680. */
  681. public function hasChild($parent, $child)
  682. {
  683. return (new Query())
  684. ->from($this->itemChildTable)
  685. ->where(['parent' => $parent->name, 'child' => $child->name])
  686. ->one($this->db) !== false;
  687. }
  688. /**
  689. * {@inheritdoc}
  690. */
  691. public function getChildren($name)
  692. {
  693. $query = (new Query())
  694. ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
  695. ->from([$this->itemTable, $this->itemChildTable])
  696. ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
  697. $children = [];
  698. foreach ($query->all($this->db) as $row) {
  699. $children[$row['name']] = $this->populateItem($row);
  700. }
  701. return $children;
  702. }
  703. /**
  704. * Checks whether there is a loop in the authorization item hierarchy.
  705. * @param Item $parent the parent item
  706. * @param Item $child the child item to be added to the hierarchy
  707. * @return bool whether a loop exists
  708. */
  709. protected function detectLoop($parent, $child)
  710. {
  711. if ($child->name === $parent->name) {
  712. return true;
  713. }
  714. foreach ($this->getChildren($child->name) as $grandchild) {
  715. if ($this->detectLoop($parent, $grandchild)) {
  716. return true;
  717. }
  718. }
  719. return false;
  720. }
  721. /**
  722. * {@inheritdoc}
  723. */
  724. public function assign($role, $userId)
  725. {
  726. $assignment = new Assignment([
  727. 'userId' => $userId,
  728. 'roleName' => $role->name,
  729. 'createdAt' => time(),
  730. ]);
  731. $this->db->createCommand()
  732. ->insert($this->assignmentTable, [
  733. 'user_id' => $assignment->userId,
  734. 'item_name' => $assignment->roleName,
  735. 'created_at' => $assignment->createdAt,
  736. ])->execute();
  737. unset($this->_checkAccessAssignments[(string) $userId]);
  738. return $assignment;
  739. }
  740. /**
  741. * {@inheritdoc}
  742. */
  743. public function revoke($role, $userId)
  744. {
  745. if ($this->isEmptyUserId($userId)) {
  746. return false;
  747. }
  748. unset($this->_checkAccessAssignments[(string) $userId]);
  749. return $this->db->createCommand()
  750. ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
  751. ->execute() > 0;
  752. }
  753. /**
  754. * {@inheritdoc}
  755. */
  756. public function revokeAll($userId)
  757. {
  758. if ($this->isEmptyUserId($userId)) {
  759. return false;
  760. }
  761. unset($this->_checkAccessAssignments[(string) $userId]);
  762. return $this->db->createCommand()
  763. ->delete($this->assignmentTable, ['user_id' => (string) $userId])
  764. ->execute() > 0;
  765. }
  766. /**
  767. * {@inheritdoc}
  768. */
  769. public function removeAll()
  770. {
  771. $this->removeAllAssignments();
  772. $this->db->createCommand()->delete($this->itemChildTable)->execute();
  773. $this->db->createCommand()->delete($this->itemTable)->execute();
  774. $this->db->createCommand()->delete($this->ruleTable)->execute();
  775. $this->invalidateCache();
  776. }
  777. /**
  778. * {@inheritdoc}
  779. */
  780. public function removeAllPermissions()
  781. {
  782. $this->removeAllItems(Item::TYPE_PERMISSION);
  783. }
  784. /**
  785. * {@inheritdoc}
  786. */
  787. public function removeAllRoles()
  788. {
  789. $this->removeAllItems(Item::TYPE_ROLE);
  790. }
  791. /**
  792. * Removes all auth items of the specified type.
  793. * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
  794. */
  795. protected function removeAllItems($type)
  796. {
  797. if (!$this->supportsCascadeUpdate()) {
  798. $names = (new Query())
  799. ->select(['name'])
  800. ->from($this->itemTable)
  801. ->where(['type' => $type])
  802. ->column($this->db);
  803. if (empty($names)) {
  804. return;
  805. }
  806. $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
  807. $this->db->createCommand()
  808. ->delete($this->itemChildTable, [$key => $names])
  809. ->execute();
  810. $this->db->createCommand()
  811. ->delete($this->assignmentTable, ['item_name' => $names])
  812. ->execute();
  813. }
  814. $this->db->createCommand()
  815. ->delete($this->itemTable, ['type' => $type])
  816. ->execute();
  817. $this->invalidateCache();
  818. }
  819. /**
  820. * {@inheritdoc}
  821. */
  822. public function removeAllRules()
  823. {
  824. if (!$this->supportsCascadeUpdate()) {
  825. $this->db->createCommand()
  826. ->update($this->itemTable, ['rule_name' => null])
  827. ->execute();
  828. }
  829. $this->db->createCommand()->delete($this->ruleTable)->execute();
  830. $this->invalidateCache();
  831. }
  832. /**
  833. * {@inheritdoc}
  834. */
  835. public function removeAllAssignments()
  836. {
  837. $this->_checkAccessAssignments = [];
  838. $this->db->createCommand()->delete($this->assignmentTable)->execute();
  839. }
  840. public function invalidateCache()
  841. {
  842. if ($this->cache !== null) {
  843. $this->cache->delete($this->cacheKey);
  844. $this->items = null;
  845. $this->rules = null;
  846. $this->parents = null;
  847. }
  848. $this->_checkAccessAssignments = [];
  849. }
  850. public function loadFromCache()
  851. {
  852. if ($this->items !== null || !$this->cache instanceof CacheInterface) {
  853. return;
  854. }
  855. $data = $this->cache->get($this->cacheKey);
  856. if (is_array($data) && isset($data[0], $data[1], $data[2])) {
  857. list($this->items, $this->rules, $this->parents) = $data;
  858. return;
  859. }
  860. $query = (new Query())->from($this->itemTable);
  861. $this->items = [];
  862. foreach ($query->all($this->db) as $row) {
  863. $this->items[$row['name']] = $this->populateItem($row);
  864. }
  865. $query = (new Query())->from($this->ruleTable);
  866. $this->rules = [];
  867. foreach ($query->all($this->db) as $row) {
  868. $data = $row['data'];
  869. if (is_resource($data)) {
  870. $data = stream_get_contents($data);
  871. }
  872. $this->rules[$row['name']] = unserialize($data);
  873. }
  874. $query = (new Query())->from($this->itemChildTable);
  875. $this->parents = [];
  876. foreach ($query->all($this->db) as $row) {
  877. if (isset($this->items[$row['child']])) {
  878. $this->parents[$row['child']][] = $row['parent'];
  879. }
  880. }
  881. $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
  882. }
  883. /**
  884. * Returns all role assignment information for the specified role.
  885. * @param string $roleName
  886. * @return string[] the ids. An empty array will be
  887. * returned if role is not assigned to any user.
  888. * @since 2.0.7
  889. */
  890. public function getUserIdsByRole($roleName)
  891. {
  892. if (empty($roleName)) {
  893. return [];
  894. }
  895. return (new Query())->select('[[user_id]]')
  896. ->from($this->assignmentTable)
  897. ->where(['item_name' => $roleName])->column($this->db);
  898. }
  899. /**
  900. * Check whether $userId is empty.
  901. * @param mixed $userId
  902. * @return bool
  903. */
  904. private function isEmptyUserId($userId)
  905. {
  906. return !isset($userId) || $userId === '';
  907. }
  908. }