You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

770 lines
22KB

  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\base\InvalidCallException;
  9. use yii\base\InvalidParamException;
  10. use Yii;
  11. use yii\helpers\VarDumper;
  12. /**
  13. * PhpManager represents an authorization manager that stores authorization
  14. * information in terms of a PHP script file.
  15. *
  16. * The authorization data will be saved to and loaded from three files
  17. * specified by [[itemFile]], [[assignmentFile]] and [[ruleFile]].
  18. *
  19. * PhpManager is mainly suitable for authorization data that is not too big
  20. * (for example, the authorization data for a personal blog system).
  21. * Use [[DbManager]] for more complex authorization data.
  22. *
  23. * Note that PhpManager is not compatible with facebooks [HHVM](http://hhvm.com/) because
  24. * it relies on writing php files and including them afterwards which is not supported by HHVM.
  25. *
  26. * @author Qiang Xue <qiang.xue@gmail.com>
  27. * @author Alexander Kochetov <creocoder@gmail.com>
  28. * @author Christophe Boulain <christophe.boulain@gmail.com>
  29. * @author Alexander Makarov <sam@rmcreative.ru>
  30. * @since 2.0
  31. */
  32. class PhpManager extends BaseManager
  33. {
  34. /**
  35. * @var string the path of the PHP script that contains the authorization items.
  36. * This can be either a file path or a path alias to the file.
  37. * Make sure this file is writable by the Web server process if the authorization needs to be changed online.
  38. * @see loadFromFile()
  39. * @see saveToFile()
  40. */
  41. public $itemFile = '@app/rbac/items.php';
  42. /**
  43. * @var string the path of the PHP script that contains the authorization assignments.
  44. * This can be either a file path or a path alias to the file.
  45. * Make sure this file is writable by the Web server process if the authorization needs to be changed online.
  46. * @see loadFromFile()
  47. * @see saveToFile()
  48. */
  49. public $assignmentFile = '@app/rbac/assignments.php';
  50. /**
  51. * @var string the path of the PHP script that contains the authorization rules.
  52. * This can be either a file path or a path alias to the file.
  53. * Make sure this file is writable by the Web server process if the authorization needs to be changed online.
  54. * @see loadFromFile()
  55. * @see saveToFile()
  56. */
  57. public $ruleFile = '@app/rbac/rules.php';
  58. /**
  59. * @var Item[]
  60. */
  61. protected $items = []; // itemName => item
  62. /**
  63. * @var array
  64. */
  65. protected $children = []; // itemName, childName => child
  66. /**
  67. * @var array
  68. */
  69. protected $assignments = []; // userId, itemName => assignment
  70. /**
  71. * @var Rule[]
  72. */
  73. protected $rules = []; // ruleName => rule
  74. /**
  75. * Initializes the application component.
  76. * This method overrides parent implementation by loading the authorization data
  77. * from PHP script.
  78. */
  79. public function init()
  80. {
  81. parent::init();
  82. $this->itemFile = Yii::getAlias($this->itemFile);
  83. $this->assignmentFile = Yii::getAlias($this->assignmentFile);
  84. $this->ruleFile = Yii::getAlias($this->ruleFile);
  85. $this->load();
  86. }
  87. /**
  88. * @inheritdoc
  89. */
  90. public function checkAccess($userId, $permissionName, $params = [])
  91. {
  92. $assignments = $this->getAssignments($userId);
  93. return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
  94. }
  95. /**
  96. * @inheritdoc
  97. */
  98. public function getAssignments($userId)
  99. {
  100. return isset($this->assignments[$userId]) ? $this->assignments[$userId] : [];
  101. }
  102. /**
  103. * Performs access check for the specified user.
  104. * This method is internally called by [[checkAccess()]].
  105. *
  106. * @param string|integer $user the user ID. This should can be either an integer or a string representing
  107. * the unique identifier of a user. See [[\yii\web\User::id]].
  108. * @param string $itemName the name of the operation that need access check
  109. * @param array $params name-value pairs that would be passed to rules associated
  110. * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
  111. * which holds the value of `$userId`.
  112. * @param Assignment[] $assignments the assignments to the specified user
  113. * @return boolean whether the operations can be performed by the user.
  114. */
  115. protected function checkAccessRecursive($user, $itemName, $params, $assignments)
  116. {
  117. if (!isset($this->items[$itemName])) {
  118. return false;
  119. }
  120. /* @var $item Item */
  121. $item = $this->items[$itemName];
  122. Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission : $itemName", __METHOD__);
  123. if (!$this->executeRule($user, $item, $params)) {
  124. return false;
  125. }
  126. if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
  127. return true;
  128. }
  129. foreach ($this->children as $parentName => $children) {
  130. if (isset($children[$itemName]) && $this->checkAccessRecursive($user, $parentName, $params, $assignments)) {
  131. return true;
  132. }
  133. }
  134. return false;
  135. }
  136. /**
  137. * @inheritdoc
  138. */
  139. public function addChild($parent, $child)
  140. {
  141. if (!isset($this->items[$parent->name], $this->items[$child->name])) {
  142. throw new InvalidParamException("Either '{$parent->name}' or '{$child->name}' does not exist.");
  143. }
  144. if ($parent->name == $child->name) {
  145. throw new InvalidParamException("Cannot add '{$parent->name} ' as a child of itself.");
  146. }
  147. if ($parent instanceof Permission && $child instanceof Role) {
  148. throw new InvalidParamException("Cannot add a role as a child of a permission.");
  149. }
  150. if ($this->detectLoop($parent, $child)) {
  151. throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
  152. }
  153. if (isset($this->children[$parent->name][$child->name])) {
  154. throw new InvalidCallException("The item '{$parent->name}' already has a child '{$child->name}'.");
  155. }
  156. $this->children[$parent->name][$child->name] = $this->items[$child->name];
  157. $this->saveItems();
  158. return true;
  159. }
  160. /**
  161. * Checks whether there is a loop in the authorization item hierarchy.
  162. *
  163. * @param Item $parent parent item
  164. * @param Item $child the child item that is to be added to the hierarchy
  165. * @return boolean whether a loop exists
  166. */
  167. protected function detectLoop($parent, $child)
  168. {
  169. if ($child->name === $parent->name) {
  170. return true;
  171. }
  172. if (!isset($this->children[$child->name], $this->items[$parent->name])) {
  173. return false;
  174. }
  175. foreach ($this->children[$child->name] as $grandchild) {
  176. /* @var $grandchild Item */
  177. if ($this->detectLoop($parent, $grandchild)) {
  178. return true;
  179. }
  180. }
  181. return false;
  182. }
  183. /**
  184. * @inheritdoc
  185. */
  186. public function removeChild($parent, $child)
  187. {
  188. if (isset($this->children[$parent->name][$child->name])) {
  189. unset($this->children[$parent->name][$child->name]);
  190. $this->saveItems();
  191. return true;
  192. } else {
  193. return false;
  194. }
  195. }
  196. /**
  197. * @inheritdoc
  198. */
  199. public function removeChildren($parent)
  200. {
  201. if (isset($this->children[$parent->name])) {
  202. unset($this->children[$parent->name]);
  203. $this->saveItems();
  204. return true;
  205. } else {
  206. return false;
  207. }
  208. }
  209. /**
  210. * @inheritdoc
  211. */
  212. public function hasChild($parent, $child)
  213. {
  214. return isset($this->children[$parent->name][$child->name]);
  215. }
  216. /**
  217. * @inheritdoc
  218. */
  219. public function assign($role, $userId)
  220. {
  221. if (!isset($this->items[$role->name])) {
  222. throw new InvalidParamException("Unknown role '{$role->name}'.");
  223. } elseif (isset($this->assignments[$userId][$role->name])) {
  224. throw new InvalidParamException("Authorization item '{$role->name}' has already been assigned to user '$userId'.");
  225. } else {
  226. $this->assignments[$userId][$role->name] = new Assignment([
  227. 'userId' => $userId,
  228. 'roleName' => $role->name,
  229. 'createdAt' => time(),
  230. ]);
  231. $this->saveAssignments();
  232. return $this->assignments[$userId][$role->name];
  233. }
  234. }
  235. /**
  236. * @inheritdoc
  237. */
  238. public function revoke($role, $userId)
  239. {
  240. if (isset($this->assignments[$userId][$role->name])) {
  241. unset($this->assignments[$userId][$role->name]);
  242. $this->saveAssignments();
  243. return true;
  244. } else {
  245. return false;
  246. }
  247. }
  248. /**
  249. * @inheritdoc
  250. */
  251. public function revokeAll($userId)
  252. {
  253. if (isset($this->assignments[$userId]) && is_array($this->assignments[$userId])) {
  254. foreach ($this->assignments[$userId] as $itemName => $value) {
  255. unset($this->assignments[$userId][$itemName]);
  256. }
  257. $this->saveAssignments();
  258. return true;
  259. } else {
  260. return false;
  261. }
  262. }
  263. /**
  264. * @inheritdoc
  265. */
  266. public function getAssignment($roleName, $userId)
  267. {
  268. return isset($this->assignments[$userId][$roleName]) ? $this->assignments[$userId][$roleName] : null;
  269. }
  270. /**
  271. * @inheritdoc
  272. */
  273. public function getItems($type)
  274. {
  275. $items = [];
  276. foreach ($this->items as $name => $item) {
  277. /* @var $item Item */
  278. if ($item->type == $type) {
  279. $items[$name] = $item;
  280. }
  281. }
  282. return $items;
  283. }
  284. /**
  285. * @inheritdoc
  286. */
  287. public function removeItem($item)
  288. {
  289. if (isset($this->items[$item->name])) {
  290. foreach ($this->children as &$children) {
  291. unset($children[$item->name]);
  292. }
  293. foreach ($this->assignments as &$assignments) {
  294. unset($assignments[$item->name]);
  295. }
  296. unset($this->items[$item->name]);
  297. $this->saveItems();
  298. return true;
  299. } else {
  300. return false;
  301. }
  302. }
  303. /**
  304. * @inheritdoc
  305. */
  306. public function getItem($name)
  307. {
  308. return isset($this->items[$name]) ? $this->items[$name] : null;
  309. }
  310. /**
  311. * @inheritdoc
  312. */
  313. public function updateRule($name, $rule)
  314. {
  315. if ($rule->name !== $name) {
  316. unset($this->rules[$name]);
  317. }
  318. $this->rules[$rule->name] = $rule;
  319. $this->saveRules();
  320. return true;
  321. }
  322. /**
  323. * @inheritdoc
  324. */
  325. public function getRule($name)
  326. {
  327. return isset($this->rules[$name]) ? $this->rules[$name] : null;
  328. }
  329. /**
  330. * @inheritdoc
  331. */
  332. public function getRules()
  333. {
  334. return $this->rules;
  335. }
  336. /**
  337. * @inheritdoc
  338. */
  339. public function getRolesByUser($userId)
  340. {
  341. $roles = [];
  342. foreach ($this->getAssignments($userId) as $name => $assignment) {
  343. $roles[$name] = $this->items[$assignment->roleName];
  344. }
  345. return $roles;
  346. }
  347. /**
  348. * @inheritdoc
  349. */
  350. public function getPermissionsByRole($roleName)
  351. {
  352. $result = [];
  353. $this->getChildrenRecursive($roleName, $result);
  354. if (empty($result)) {
  355. return [];
  356. }
  357. $permissions = [];
  358. foreach (array_keys($result) as $itemName) {
  359. if (isset($this->items[$itemName]) && $this->items[$itemName] instanceof Permission) {
  360. $permissions[$itemName] = $this->items[$itemName];
  361. }
  362. }
  363. return $permissions;
  364. }
  365. /**
  366. * Recursively finds all children and grand children of the specified item.
  367. *
  368. * @param string $name the name of the item whose children are to be looked for.
  369. * @param array $result the children and grand children (in array keys)
  370. */
  371. protected function getChildrenRecursive($name, &$result)
  372. {
  373. if (isset($this->children[$name])) {
  374. foreach ($this->children[$name] as $child) {
  375. $result[$child->name] = true;
  376. $this->getChildrenRecursive($child->name, $result);
  377. }
  378. }
  379. }
  380. /**
  381. * @inheritdoc
  382. */
  383. public function getPermissionsByUser($userId)
  384. {
  385. $assignments = $this->getAssignments($userId);
  386. $result = [];
  387. foreach (array_keys($assignments) as $roleName) {
  388. $this->getChildrenRecursive($roleName, $result);
  389. }
  390. if (empty($result)) {
  391. return [];
  392. }
  393. $permissions = [];
  394. foreach (array_keys($result) as $itemName) {
  395. if (isset($this->items[$itemName]) && $this->items[$itemName] instanceof Permission) {
  396. $permissions[$itemName] = $this->items[$itemName];
  397. }
  398. }
  399. return $permissions;
  400. }
  401. /**
  402. * @inheritdoc
  403. */
  404. public function getChildren($name)
  405. {
  406. return isset($this->children[$name]) ? $this->children[$name] : [];
  407. }
  408. /**
  409. * @inheritdoc
  410. */
  411. public function removeAll()
  412. {
  413. $this->children = [];
  414. $this->items = [];
  415. $this->assignments = [];
  416. $this->rules = [];
  417. $this->save();
  418. }
  419. /**
  420. * @inheritdoc
  421. */
  422. public function removeAllPermissions()
  423. {
  424. $this->removeAllItems(Item::TYPE_PERMISSION);
  425. }
  426. /**
  427. * @inheritdoc
  428. */
  429. public function removeAllRoles()
  430. {
  431. $this->removeAllItems(Item::TYPE_ROLE);
  432. }
  433. /**
  434. * Removes all auth items of the specified type.
  435. * @param integer $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
  436. */
  437. protected function removeAllItems($type)
  438. {
  439. $names = [];
  440. foreach ($this->items as $name => $item) {
  441. if ($item->type == $type) {
  442. unset($this->items[$name]);
  443. $names[$name] = true;
  444. }
  445. }
  446. if (empty($names)) {
  447. return;
  448. }
  449. foreach ($this->assignments as $i => $assignment) {
  450. if (isset($names[$assignment->roleName])) {
  451. unset($this->assignments[$i]);
  452. }
  453. }
  454. foreach ($this->children as $name => $children) {
  455. if (isset($names[$name])) {
  456. unset($this->children[$name]);
  457. } else {
  458. foreach ($children as $childName => $item) {
  459. if (isset($names[$childName])) {
  460. unset($children[$childName]);
  461. }
  462. }
  463. $this->children[$name] = $children;
  464. }
  465. }
  466. $this->saveItems();
  467. }
  468. /**
  469. * @inheritdoc
  470. */
  471. public function removeAllRules()
  472. {
  473. foreach ($this->items as $item) {
  474. $item->ruleName = null;
  475. }
  476. $this->rules = [];
  477. $this->saveRules();
  478. }
  479. /**
  480. * @inheritdoc
  481. */
  482. public function removeAllAssignments()
  483. {
  484. $this->assignments = [];
  485. $this->saveAssignments();
  486. }
  487. /**
  488. * @inheritdoc
  489. */
  490. protected function removeRule($rule)
  491. {
  492. if (isset($this->rules[$rule->name])) {
  493. unset($this->rules[$rule->name]);
  494. foreach ($this->items as $item) {
  495. if ($item->ruleName === $rule->name) {
  496. $item->ruleName = null;
  497. }
  498. }
  499. $this->saveRules();
  500. return true;
  501. } else {
  502. return false;
  503. }
  504. }
  505. /**
  506. * @inheritdoc
  507. */
  508. protected function addRule($rule)
  509. {
  510. $this->rules[$rule->name] = $rule;
  511. $this->saveRules();
  512. return true;
  513. }
  514. /**
  515. * @inheritdoc
  516. */
  517. protected function updateItem($name, $item)
  518. {
  519. $this->items[$item->name] = $item;
  520. if ($name !== $item->name) {
  521. if (isset($this->items[$item->name])) {
  522. throw new InvalidParamException("Unable to change the item name. The name '{$item->name}' is already used by another item.");
  523. }
  524. if (isset($this->items[$name])) {
  525. unset ($this->items[$name]);
  526. if (isset($this->children[$name])) {
  527. $this->children[$item->name] = $this->children[$name];
  528. unset ($this->children[$name]);
  529. }
  530. foreach ($this->children as &$children) {
  531. if (isset($children[$name])) {
  532. $children[$item->name] = $children[$name];
  533. unset ($children[$name]);
  534. }
  535. }
  536. foreach ($this->assignments as &$assignments) {
  537. if (isset($assignments[$name])) {
  538. $assignments[$item->name] = $assignments[$name];
  539. unset($assignments[$name]);
  540. }
  541. }
  542. }
  543. }
  544. $this->saveItems();
  545. return true;
  546. }
  547. /**
  548. * @inheritdoc
  549. */
  550. protected function addItem($item)
  551. {
  552. $time = time();
  553. if ($item->createdAt === null) {
  554. $item->createdAt = $time;
  555. }
  556. if ($item->updatedAt === null) {
  557. $item->updatedAt = $time;
  558. }
  559. $this->items[$item->name] = $item;
  560. $this->saveItems();
  561. return true;
  562. }
  563. /**
  564. * Loads authorization data from persistent storage.
  565. */
  566. protected function load()
  567. {
  568. $this->children = [];
  569. $this->rules = [];
  570. $this->assignments = [];
  571. $this->items = [];
  572. $items = $this->loadFromFile($this->itemFile);
  573. $itemsMtime = @filemtime($this->itemFile);
  574. $assignments = $this->loadFromFile($this->assignmentFile);
  575. $assignmentsMtime = @filemtime($this->assignmentFile);
  576. $rules = $this->loadFromFile($this->ruleFile);
  577. foreach ($items as $name => $item) {
  578. $class = $item['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
  579. $this->items[$name] = new $class([
  580. 'name' => $name,
  581. 'description' => isset($item['description']) ? $item['description'] : null,
  582. 'ruleName' => isset($item['ruleName']) ? $item['ruleName'] : null,
  583. 'data' => isset($item['data']) ? $item['data'] : null,
  584. 'createdAt' => $itemsMtime,
  585. 'updatedAt' => $itemsMtime,
  586. ]);
  587. }
  588. foreach ($items as $name => $item) {
  589. if (isset($item['children'])) {
  590. foreach ($item['children'] as $childName) {
  591. if (isset($this->items[$childName])) {
  592. $this->children[$name][$childName] = $this->items[$childName];
  593. }
  594. }
  595. }
  596. }
  597. foreach ($assignments as $userId => $roles) {
  598. foreach ($roles as $role) {
  599. $this->assignments[$userId][$role] = new Assignment([
  600. 'userId' => $userId,
  601. 'roleName' => $role,
  602. 'createdAt' => $assignmentsMtime,
  603. ]);
  604. }
  605. }
  606. foreach ($rules as $name => $ruleData) {
  607. $this->rules[$name] = unserialize($ruleData);
  608. }
  609. }
  610. /**
  611. * Saves authorization data into persistent storage.
  612. */
  613. protected function save()
  614. {
  615. $this->saveItems();
  616. $this->saveAssignments();
  617. $this->saveRules();
  618. }
  619. /**
  620. * Loads the authorization data from a PHP script file.
  621. *
  622. * @param string $file the file path.
  623. * @return array the authorization data
  624. * @see saveToFile()
  625. */
  626. protected function loadFromFile($file)
  627. {
  628. if (is_file($file)) {
  629. return require($file);
  630. } else {
  631. return [];
  632. }
  633. }
  634. /**
  635. * Saves the authorization data to a PHP script file.
  636. *
  637. * @param array $data the authorization data
  638. * @param string $file the file path.
  639. * @see loadFromFile()
  640. */
  641. protected function saveToFile($data, $file)
  642. {
  643. file_put_contents($file, "<?php\nreturn " . VarDumper::export($data) . ";\n", LOCK_EX);
  644. }
  645. /**
  646. * Saves items data into persistent storage.
  647. */
  648. protected function saveItems()
  649. {
  650. $items = [];
  651. foreach ($this->items as $name => $item) {
  652. /* @var $item Item */
  653. $items[$name] = array_filter(
  654. [
  655. 'type' => $item->type,
  656. 'description' => $item->description,
  657. 'ruleName' => $item->ruleName,
  658. 'data' => $item->data,
  659. ]
  660. );
  661. if (isset($this->children[$name])) {
  662. foreach ($this->children[$name] as $child) {
  663. /* @var $child Item */
  664. $items[$name]['children'][] = $child->name;
  665. }
  666. }
  667. }
  668. $this->saveToFile($items, $this->itemFile);
  669. }
  670. /**
  671. * Saves assignments data into persistent storage.
  672. */
  673. protected function saveAssignments()
  674. {
  675. $assignmentData = [];
  676. foreach ($this->assignments as $userId => $assignments) {
  677. foreach ($assignments as $name => $assignment) {
  678. /* @var $assignment Assignment */
  679. $assignmentData[$userId][] = $assignment->roleName;
  680. }
  681. }
  682. $this->saveToFile($assignmentData, $this->assignmentFile);
  683. }
  684. /**
  685. * Saves rules data into persistent storage.
  686. */
  687. protected function saveRules()
  688. {
  689. $rules = [];
  690. foreach ($this->rules as $name => $rule) {
  691. $rules[$name] = serialize($rule);
  692. }
  693. $this->saveToFile($rules, $this->ruleFile);
  694. }
  695. }