PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Cake/Controller/Component/Acl/PhpAcl.php

https://bitbucket.org/LeThanhDat/firstdummyapp
PHP | 559 lines | 258 code | 78 blank | 223 comment | 49 complexity | ec37221ddc27f98973eeea8bb10f3028 MD5 | raw file
  1. <?php
  2. /**
  3. * PHP configuration based AclInterface implementation
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * For full copyright and license information, please see the LICENSE.txt
  12. * Redistributions of files must retain the above copyright notice.
  13. *
  14. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  15. * @link http://cakephp.org CakePHP(tm) Project
  16. * @package Cake.Controller.Component.Acl
  17. * @since CakePHP(tm) v 2.1
  18. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  19. */
  20. /**
  21. * PhpAcl implements an access control system using a plain PHP configuration file.
  22. * An example file can be found in app/Config/acl.php
  23. *
  24. * @package Cake.Controller.Component.Acl
  25. */
  26. class PhpAcl extends Object implements AclInterface {
  27. /**
  28. * Constant for deny
  29. */
  30. const DENY = false;
  31. /**
  32. * Constant for allow
  33. */
  34. const ALLOW = true;
  35. /**
  36. * Options:
  37. * - policy: determines behavior of the check method. Deny policy needs explicit allow rules, allow policy needs explicit deny rules
  38. * - config: absolute path to config file that contains the acl rules (@see app/Config/acl.php)
  39. *
  40. * @var array
  41. */
  42. public $options = array();
  43. /**
  44. * Aro Object
  45. *
  46. * @var PhpAro
  47. */
  48. public $Aro = null;
  49. /**
  50. * Aco Object
  51. *
  52. * @var PhpAco
  53. */
  54. public $Aco = null;
  55. /**
  56. * Constructor
  57. *
  58. * Sets a few default settings up.
  59. */
  60. public function __construct() {
  61. $this->options = array(
  62. 'policy' => self::DENY,
  63. 'config' => APP . 'Config' . DS . 'acl.php',
  64. );
  65. }
  66. /**
  67. * Initialize method
  68. *
  69. * @param AclComponent $Component Component instance
  70. * @return void
  71. */
  72. public function initialize(Component $Component) {
  73. if (!empty($Component->settings['adapter'])) {
  74. $this->options = array_merge($this->options, $Component->settings['adapter']);
  75. }
  76. App::uses('PhpReader', 'Configure');
  77. $Reader = new PhpReader(dirname($this->options['config']) . DS);
  78. $config = $Reader->read(basename($this->options['config']));
  79. $this->build($config);
  80. $Component->Aco = $this->Aco;
  81. $Component->Aro = $this->Aro;
  82. }
  83. /**
  84. * build and setup internal ACL representation
  85. *
  86. * @param array $config configuration array, see docs
  87. * @return void
  88. * @throws AclException When required keys are missing.
  89. */
  90. public function build(array $config) {
  91. if (empty($config['roles'])) {
  92. throw new AclException(__d('cake_dev','"roles" section not found in configuration.'));
  93. }
  94. if (empty($config['rules']['allow']) && empty($config['rules']['deny'])) {
  95. throw new AclException(__d('cake_dev','Neither "allow" nor "deny" rules were provided in configuration.'));
  96. }
  97. $rules['allow'] = !empty($config['rules']['allow']) ? $config['rules']['allow'] : array();
  98. $rules['deny'] = !empty($config['rules']['deny']) ? $config['rules']['deny'] : array();
  99. $roles = !empty($config['roles']) ? $config['roles'] : array();
  100. $map = !empty($config['map']) ? $config['map'] : array();
  101. $alias = !empty($config['alias']) ? $config['alias'] : array();
  102. $this->Aro = new PhpAro($roles, $map, $alias);
  103. $this->Aco = new PhpAco($rules);
  104. }
  105. /**
  106. * No op method, allow cannot be done with PhpAcl
  107. *
  108. * @param string $aro ARO The requesting object identifier.
  109. * @param string $aco ACO The controlled object identifier.
  110. * @param string $action Action (defaults to *)
  111. * @return boolean Success
  112. */
  113. public function allow($aro, $aco, $action = "*") {
  114. return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'allow');
  115. }
  116. /**
  117. * deny ARO access to ACO
  118. *
  119. * @param string $aro ARO The requesting object identifier.
  120. * @param string $aco ACO The controlled object identifier.
  121. * @param string $action Action (defaults to *)
  122. * @return boolean Success
  123. */
  124. public function deny($aro, $aco, $action = "*") {
  125. return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'deny');
  126. }
  127. /**
  128. * No op method
  129. *
  130. * @param string $aro ARO The requesting object identifier.
  131. * @param string $aco ACO The controlled object identifier.
  132. * @param string $action Action (defaults to *)
  133. * @return boolean Success
  134. */
  135. public function inherit($aro, $aco, $action = "*") {
  136. return false;
  137. }
  138. /**
  139. * Main ACL check function. Checks to see if the ARO (access request object) has access to the
  140. * ACO (access control object).
  141. *
  142. * @param string $aro ARO
  143. * @param string $aco ACO
  144. * @param string $action Action
  145. * @return boolean true if access is granted, false otherwise
  146. */
  147. public function check($aro, $aco, $action = "*") {
  148. $allow = $this->options['policy'];
  149. $prioritizedAros = $this->Aro->roles($aro);
  150. if ($action && $action !== "*") {
  151. $aco .= '/' . $action;
  152. }
  153. $path = $this->Aco->path($aco);
  154. if (empty($path)) {
  155. return $allow;
  156. }
  157. foreach ($path as $node) {
  158. foreach ($prioritizedAros as $aros) {
  159. if (!empty($node['allow'])) {
  160. $allow = $allow || count(array_intersect($node['allow'], $aros));
  161. }
  162. if (!empty($node['deny'])) {
  163. $allow = $allow && !count(array_intersect($node['deny'], $aros));
  164. }
  165. }
  166. }
  167. return $allow;
  168. }
  169. }
  170. /**
  171. * Access Control Object
  172. *
  173. */
  174. class PhpAco {
  175. /**
  176. * holds internal ACO representation
  177. *
  178. * @var array
  179. */
  180. protected $_tree = array();
  181. /**
  182. * map modifiers for ACO paths to their respective PCRE pattern
  183. *
  184. * @var array
  185. */
  186. public static $modifiers = array(
  187. '*' => '.*',
  188. );
  189. /**
  190. * Constructor
  191. *
  192. * @param array $rules Rules array
  193. */
  194. public function __construct(array $rules = array()) {
  195. foreach (array('allow', 'deny') as $type) {
  196. if (empty($rules[$type])) {
  197. $rules[$type] = array();
  198. }
  199. }
  200. $this->build($rules['allow'], $rules['deny']);
  201. }
  202. /**
  203. * return path to the requested ACO with allow and deny rules attached on each level
  204. *
  205. * @param string $aco ACO string
  206. * @return array
  207. */
  208. public function path($aco) {
  209. $aco = $this->resolve($aco);
  210. $path = array();
  211. $level = 0;
  212. $root = $this->_tree;
  213. $stack = array(array($root, 0));
  214. while (!empty($stack)) {
  215. list($root, $level) = array_pop($stack);
  216. if (empty($path[$level])) {
  217. $path[$level] = array();
  218. }
  219. foreach ($root as $node => $elements) {
  220. $pattern = '/^' . str_replace(array_keys(self::$modifiers), array_values(self::$modifiers), $node) . '$/';
  221. if ($node == $aco[$level] || preg_match($pattern, $aco[$level])) {
  222. // merge allow/denies with $path of current level
  223. foreach (array('allow', 'deny') as $policy) {
  224. if (!empty($elements[$policy])) {
  225. if (empty($path[$level][$policy])) {
  226. $path[$level][$policy] = array();
  227. }
  228. $path[$level][$policy] = array_merge($path[$level][$policy], $elements[$policy]);
  229. }
  230. }
  231. // traverse
  232. if (!empty($elements['children']) && isset($aco[$level + 1])) {
  233. array_push($stack, array($elements['children'], $level + 1));
  234. }
  235. }
  236. }
  237. }
  238. return $path;
  239. }
  240. /**
  241. * allow/deny ARO access to ARO
  242. *
  243. * @param string $aro ARO string
  244. * @param string $aco ACO string
  245. * @param string $action Action string
  246. * @param string $type access type
  247. * @return void
  248. */
  249. public function access($aro, $aco, $action, $type = 'deny') {
  250. $aco = $this->resolve($aco);
  251. $depth = count($aco);
  252. $root = $this->_tree;
  253. $tree = &$root;
  254. foreach ($aco as $i => $node) {
  255. if (!isset($tree[$node])) {
  256. $tree[$node] = array(
  257. 'children' => array(),
  258. );
  259. }
  260. if ($i < $depth - 1) {
  261. $tree = &$tree[$node]['children'];
  262. } else {
  263. if (empty($tree[$node][$type])) {
  264. $tree[$node][$type] = array();
  265. }
  266. $tree[$node][$type] = array_merge(is_array($aro) ? $aro : array($aro), $tree[$node][$type]);
  267. }
  268. }
  269. $this->_tree = &$root;
  270. }
  271. /**
  272. * resolve given ACO string to a path
  273. *
  274. * @param string $aco ACO string
  275. * @return array path
  276. */
  277. public function resolve($aco) {
  278. if (is_array($aco)) {
  279. return array_map('strtolower', $aco);
  280. }
  281. // strip multiple occurrences of '/'
  282. $aco = preg_replace('#/+#', '/', $aco);
  283. // make case insensitive
  284. $aco = ltrim(strtolower($aco), '/');
  285. return array_filter(array_map('trim', explode('/', $aco)));
  286. }
  287. /**
  288. * build a tree representation from the given allow/deny informations for ACO paths
  289. *
  290. * @param array $allow ACO allow rules
  291. * @param array $deny ACO deny rules
  292. * @return void
  293. */
  294. public function build(array $allow, array $deny = array()) {
  295. $this->_tree = array();
  296. foreach ($allow as $dotPath => $aros) {
  297. if (is_string($aros)) {
  298. $aros = array_map('trim', explode(',', $aros));
  299. }
  300. $this->access($aros, $dotPath, null, 'allow');
  301. }
  302. foreach ($deny as $dotPath => $aros) {
  303. if (is_string($aros)) {
  304. $aros = array_map('trim', explode(',', $aros));
  305. }
  306. $this->access($aros, $dotPath, null, 'deny');
  307. }
  308. }
  309. }
  310. /**
  311. * Access Request Object
  312. *
  313. */
  314. class PhpAro {
  315. /**
  316. * role to resolve to when a provided ARO is not listed in
  317. * the internal tree
  318. */
  319. const DEFAULT_ROLE = 'Role/default';
  320. /**
  321. * map external identifiers. E.g. if
  322. *
  323. * array('User' => array('username' => 'jeff', 'role' => 'editor'))
  324. *
  325. * is passed as an ARO to one of the methods of AclComponent, PhpAcl
  326. * will check if it can be resolved to an User or a Role defined in the
  327. * configuration file.
  328. *
  329. * @var array
  330. * @see app/Config/acl.php
  331. */
  332. public $map = array(
  333. 'User' => 'User/username',
  334. 'Role' => 'User/role',
  335. );
  336. /**
  337. * aliases to map
  338. *
  339. * @var array
  340. */
  341. public $aliases = array();
  342. /**
  343. * internal ARO representation
  344. *
  345. * @var array
  346. */
  347. protected $_tree = array();
  348. /**
  349. * Constructor
  350. *
  351. * @param array $aro
  352. * @param array $map
  353. * @param array $aliases
  354. */
  355. public function __construct(array $aro = array(), array $map = array(), array $aliases = array()) {
  356. if (!empty($map)) {
  357. $this->map = $map;
  358. }
  359. $this->aliases = $aliases;
  360. $this->build($aro);
  361. }
  362. /**
  363. * From the perspective of the given ARO, walk down the tree and
  364. * collect all inherited AROs levelwise such that AROs from different
  365. * branches with equal distance to the requested ARO will be collected at the same
  366. * index. The resulting array will contain a prioritized list of (list of) roles ordered from
  367. * the most distant AROs to the requested one itself.
  368. *
  369. * @param string|array $aro An ARO identifier
  370. * @return array prioritized AROs
  371. */
  372. public function roles($aro) {
  373. $aros = array();
  374. $aro = $this->resolve($aro);
  375. $stack = array(array($aro, 0));
  376. while (!empty($stack)) {
  377. list($element, $depth) = array_pop($stack);
  378. $aros[$depth][] = $element;
  379. foreach ($this->_tree as $node => $children) {
  380. if (in_array($element, $children)) {
  381. array_push($stack, array($node, $depth + 1));
  382. }
  383. }
  384. }
  385. return array_reverse($aros);
  386. }
  387. /**
  388. * resolve an ARO identifier to an internal ARO string using
  389. * the internal mapping information.
  390. *
  391. * @param string|array $aro ARO identifier (User.jeff, array('User' => ...), etc)
  392. * @return string internal aro string (e.g. User/jeff, Role/default)
  393. */
  394. public function resolve($aro) {
  395. foreach ($this->map as $aroGroup => $map) {
  396. list ($model, $field) = explode('/', $map, 2);
  397. $mapped = '';
  398. if (is_array($aro)) {
  399. if (isset($aro['model']) && isset($aro['foreign_key']) && $aro['model'] == $aroGroup) {
  400. $mapped = $aroGroup . '/' . $aro['foreign_key'];
  401. } elseif (isset($aro[$model][$field])) {
  402. $mapped = $aroGroup . '/' . $aro[$model][$field];
  403. } elseif (isset($aro[$field])) {
  404. $mapped = $aroGroup . '/' . $aro[$field];
  405. }
  406. } elseif (is_string($aro)) {
  407. $aro = ltrim($aro, '/');
  408. if (strpos($aro, '/') === false) {
  409. $mapped = $aroGroup . '/' . $aro;
  410. } else {
  411. list($aroModel, $aroValue) = explode('/', $aro, 2);
  412. $aroModel = Inflector::camelize($aroModel);
  413. if ($aroModel == $model || $aroModel == $aroGroup) {
  414. $mapped = $aroGroup . '/' . $aroValue;
  415. }
  416. }
  417. }
  418. if (isset($this->_tree[$mapped])) {
  419. return $mapped;
  420. }
  421. // is there a matching alias defined (e.g. Role/1 => Role/admin)?
  422. if (!empty($this->aliases[$mapped])) {
  423. return $this->aliases[$mapped];
  424. }
  425. }
  426. return self::DEFAULT_ROLE;
  427. }
  428. /**
  429. * adds a new ARO to the tree
  430. *
  431. * @param array $aro one or more ARO records
  432. * @return void
  433. */
  434. public function addRole(array $aro) {
  435. foreach ($aro as $role => $inheritedRoles) {
  436. if (!isset($this->_tree[$role])) {
  437. $this->_tree[$role] = array();
  438. }
  439. if (!empty($inheritedRoles)) {
  440. if (is_string($inheritedRoles)) {
  441. $inheritedRoles = array_map('trim', explode(',', $inheritedRoles));
  442. }
  443. foreach ($inheritedRoles as $dependency) {
  444. // detect cycles
  445. $roles = $this->roles($dependency);
  446. if (in_array($role, Hash::flatten($roles))) {
  447. $path = '';
  448. foreach ($roles as $roleDependencies) {
  449. $path .= implode('|', (array)$roleDependencies) . ' -> ';
  450. }
  451. trigger_error(__d('cake_dev', 'cycle detected when inheriting %s from %s. Path: %s', $role, $dependency, $path . $role));
  452. continue;
  453. }
  454. if (!isset($this->_tree[$dependency])) {
  455. $this->_tree[$dependency] = array();
  456. }
  457. $this->_tree[$dependency][] = $role;
  458. }
  459. }
  460. }
  461. }
  462. /**
  463. * adds one or more aliases to the internal map. Overwrites existing entries.
  464. *
  465. * @param array $alias alias from => to (e.g. Role/13 -> Role/editor)
  466. * @return void
  467. */
  468. public function addAlias(array $alias) {
  469. $this->aliases = array_merge($this->aliases, $alias);
  470. }
  471. /**
  472. * build an ARO tree structure for internal processing
  473. *
  474. * @param array $aros array of AROs as key and their inherited AROs as values
  475. * @return void
  476. */
  477. public function build(array $aros) {
  478. $this->_tree = array();
  479. $this->addRole($aros);
  480. }
  481. }