PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

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

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