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