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