PageRenderTime 33ms CodeModel.GetById 7ms RepoModel.GetById 1ms app.codeStats 0ms

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

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