DbManager.php 33 KB

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