Blame | Last modification | View Log | RSS feed
<?php/*** PHP configuration based AclInterface implementation** CakePHP(tm) : Rapid Development Framework (http://cakephp.org)* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)** Licensed under The MIT License* For full copyright and license information, please see the LICENSE.txt* Redistributions of files must retain the above copyright notice.** @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)* @link http://cakephp.org CakePHP(tm) Project* @package Cake.Controller.Component.Acl* @since CakePHP(tm) v 2.1* @license http://www.opensource.org/licenses/mit-license.php MIT License*//*** PhpAcl implements an access control system using a plain PHP configuration file.* An example file can be found in app/Config/acl.php** @package Cake.Controller.Component.Acl*/class PhpAcl extends Object implements AclInterface {/*** Constant for deny** @var bool*/const DENY = false;/*** Constant for allow** @var bool*/const ALLOW = true;/*** Options:* - policy: determines behavior of the check method. Deny policy needs explicit allow rules, allow policy needs explicit deny rules* - config: absolute path to config file that contains the acl rules (@see app/Config/acl.php)** @var array*/public $options = array();/*** Aro Object** @var PhpAro*/public $Aro = null;/*** Aco Object** @var PhpAco*/public $Aco = null;/*** Constructor** Sets a few default settings up.*/public function __construct() {$this->options = array('policy' => static::DENY,'config' => APP . 'Config' . DS . 'acl.php',);}/*** Initialize method** @param AclComponent $Component Component instance* @return void*/public function initialize(Component $Component) {if (!empty($Component->settings['adapter'])) {$this->options = $Component->settings['adapter'] + $this->options;}App::uses('PhpReader', 'Configure');$Reader = new PhpReader(dirname($this->options['config']) . DS);$config = $Reader->read(basename($this->options['config']));$this->build($config);$Component->Aco = $this->Aco;$Component->Aro = $this->Aro;}/*** build and setup internal ACL representation** @param array $config configuration array, see docs* @return void* @throws AclException When required keys are missing.*/public function build(array $config) {if (empty($config['roles'])) {throw new AclException(__d('cake_dev', '"roles" section not found in configuration.'));}if (empty($config['rules']['allow']) && empty($config['rules']['deny'])) {throw new AclException(__d('cake_dev', 'Neither "allow" nor "deny" rules were provided in configuration.'));}$rules['allow'] = !empty($config['rules']['allow']) ? $config['rules']['allow'] : array();$rules['deny'] = !empty($config['rules']['deny']) ? $config['rules']['deny'] : array();$roles = !empty($config['roles']) ? $config['roles'] : array();$map = !empty($config['map']) ? $config['map'] : array();$alias = !empty($config['alias']) ? $config['alias'] : array();$this->Aro = new PhpAro($roles, $map, $alias);$this->Aco = new PhpAco($rules);}/*** No op method, allow cannot be done with PhpAcl** @param string $aro ARO The requesting object identifier.* @param string $aco ACO The controlled object identifier.* @param string $action Action (defaults to *)* @return bool Success*/public function allow($aro, $aco, $action = "*") {return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'allow');}/*** deny ARO access to ACO** @param string $aro ARO The requesting object identifier.* @param string $aco ACO The controlled object identifier.* @param string $action Action (defaults to *)* @return bool Success*/public function deny($aro, $aco, $action = "*") {return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'deny');}/*** No op method** @param string $aro ARO The requesting object identifier.* @param string $aco ACO The controlled object identifier.* @param string $action Action (defaults to *)* @return bool Success*/public function inherit($aro, $aco, $action = "*") {return false;}/*** Main ACL check function. Checks to see if the ARO (access request object) has access to the* ACO (access control object).** @param string $aro ARO* @param string $aco ACO* @param string $action Action* @return bool true if access is granted, false otherwise*/public function check($aro, $aco, $action = "*") {$allow = $this->options['policy'];$prioritizedAros = $this->Aro->roles($aro);if ($action && $action !== "*") {$aco .= '/' . $action;}$path = $this->Aco->path($aco);if (empty($path)) {return $allow;}foreach ($path as $node) {foreach ($prioritizedAros as $aros) {if (!empty($node['allow'])) {$allow = $allow || count(array_intersect($node['allow'], $aros));}if (!empty($node['deny'])) {$allow = $allow && !count(array_intersect($node['deny'], $aros));}}}return $allow;}}/*** Access Control Object**/class PhpAco {/*** holds internal ACO representation** @var array*/protected $_tree = array();/*** map modifiers for ACO paths to their respective PCRE pattern** @var array*/public static $modifiers = array('*' => '.*',);/*** Constructor** @param array $rules Rules array*/public function __construct(array $rules = array()) {foreach (array('allow', 'deny') as $type) {if (empty($rules[$type])) {$rules[$type] = array();}}$this->build($rules['allow'], $rules['deny']);}/*** return path to the requested ACO with allow and deny rules attached on each level** @param string $aco ACO string* @return array*/public function path($aco) {$aco = $this->resolve($aco);$path = array();$level = 0;$root = $this->_tree;$stack = array(array($root, 0));while (!empty($stack)) {list($root, $level) = array_pop($stack);if (empty($path[$level])) {$path[$level] = array();}foreach ($root as $node => $elements) {$pattern = '/^' . str_replace(array_keys(static::$modifiers), array_values(static::$modifiers), $node) . '$/';if ($node == $aco[$level] || preg_match($pattern, $aco[$level])) {// merge allow/denies with $path of current levelforeach (array('allow', 'deny') as $policy) {if (!empty($elements[$policy])) {if (empty($path[$level][$policy])) {$path[$level][$policy] = array();}$path[$level][$policy] = array_merge($path[$level][$policy], $elements[$policy]);}}// traverseif (!empty($elements['children']) && isset($aco[$level + 1])) {array_push($stack, array($elements['children'], $level + 1));}}}}return $path;}/*** allow/deny ARO access to ARO** @param string $aro ARO string* @param string $aco ACO string* @param string $action Action string* @param string $type access type* @return void*/public function access($aro, $aco, $action, $type = 'deny') {$aco = $this->resolve($aco);$depth = count($aco);$root = $this->_tree;$tree = &$root;foreach ($aco as $i => $node) {if (!isset($tree[$node])) {$tree[$node] = array('children' => array(),);}if ($i < $depth - 1) {$tree = &$tree[$node]['children'];} else {if (empty($tree[$node][$type])) {$tree[$node][$type] = array();}$tree[$node][$type] = array_merge(is_array($aro) ? $aro : array($aro), $tree[$node][$type]);}}$this->_tree = &$root;}/*** resolve given ACO string to a path** @param string $aco ACO string* @return array path*/public function resolve($aco) {if (is_array($aco)) {return array_map('strtolower', $aco);}// strip multiple occurrences of '/'$aco = preg_replace('#/+#', '/', $aco);// make case insensitive$aco = ltrim(strtolower($aco), '/');return array_filter(array_map('trim', explode('/', $aco)));}/*** build a tree representation from the given allow/deny informations for ACO paths** @param array $allow ACO allow rules* @param array $deny ACO deny rules* @return void*/public function build(array $allow, array $deny = array()) {$this->_tree = array();foreach ($allow as $dotPath => $aros) {if (is_string($aros)) {$aros = array_map('trim', explode(',', $aros));}$this->access($aros, $dotPath, null, 'allow');}foreach ($deny as $dotPath => $aros) {if (is_string($aros)) {$aros = array_map('trim', explode(',', $aros));}$this->access($aros, $dotPath, null, 'deny');}}}/*** Access Request Object**/class PhpAro {/*** role to resolve to when a provided ARO is not listed in* the internal tree** @var string*/const DEFAULT_ROLE = 'Role/default';/*** map external identifiers. E.g. if** array('User' => array('username' => 'jeff', 'role' => 'editor'))** is passed as an ARO to one of the methods of AclComponent, PhpAcl* will check if it can be resolved to an User or a Role defined in the* configuration file.** @var array* @see app/Config/acl.php*/public $map = array('User' => 'User/username','Role' => 'User/role',);/*** aliases to map** @var array*/public $aliases = array();/*** internal ARO representation** @var array*/protected $_tree = array();/*** Constructor** @param array $aro The aro data* @param array $map The identifier mappings* @param array $aliases The aliases to map.*/public function __construct(array $aro = array(), array $map = array(), array $aliases = array()) {if (!empty($map)) {$this->map = $map;}$this->aliases = $aliases;$this->build($aro);}/*** From the perspective of the given ARO, walk down the tree and* collect all inherited AROs levelwise such that AROs from different* branches with equal distance to the requested ARO will be collected at the same* index. The resulting array will contain a prioritized list of (list of) roles ordered from* the most distant AROs to the requested one itself.** @param string|array $aro An ARO identifier* @return array prioritized AROs*/public function roles($aro) {$aros = array();$aro = $this->resolve($aro);$stack = array(array($aro, 0));while (!empty($stack)) {list($element, $depth) = array_pop($stack);$aros[$depth][] = $element;foreach ($this->_tree as $node => $children) {if (in_array($element, $children)) {array_push($stack, array($node, $depth + 1));}}}return array_reverse($aros);}/*** resolve an ARO identifier to an internal ARO string using* the internal mapping information.** @param string|array $aro ARO identifier (User.jeff, array('User' => ...), etc)* @return string internal aro string (e.g. User/jeff, Role/default)*/public function resolve($aro) {foreach ($this->map as $aroGroup => $map) {list ($model, $field) = explode('/', $map, 2);$mapped = '';if (is_array($aro)) {if (isset($aro['model']) && isset($aro['foreign_key']) && $aro['model'] === $aroGroup) {$mapped = $aroGroup . '/' . $aro['foreign_key'];} elseif (isset($aro[$model][$field])) {$mapped = $aroGroup . '/' . $aro[$model][$field];} elseif (isset($aro[$field])) {$mapped = $aroGroup . '/' . $aro[$field];}} elseif (is_string($aro)) {$aro = ltrim($aro, '/');if (strpos($aro, '/') === false) {$mapped = $aroGroup . '/' . $aro;} else {list($aroModel, $aroValue) = explode('/', $aro, 2);$aroModel = Inflector::camelize($aroModel);if ($aroModel === $model || $aroModel === $aroGroup) {$mapped = $aroGroup . '/' . $aroValue;}}}if (isset($this->_tree[$mapped])) {return $mapped;}// is there a matching alias defined (e.g. Role/1 => Role/admin)?if (!empty($this->aliases[$mapped])) {return $this->aliases[$mapped];}}return static::DEFAULT_ROLE;}/*** adds a new ARO to the tree** @param array $aro one or more ARO records* @return void*/public function addRole(array $aro) {foreach ($aro as $role => $inheritedRoles) {if (!isset($this->_tree[$role])) {$this->_tree[$role] = array();}if (!empty($inheritedRoles)) {if (is_string($inheritedRoles)) {$inheritedRoles = array_map('trim', explode(',', $inheritedRoles));}foreach ($inheritedRoles as $dependency) {// detect cycles$roles = $this->roles($dependency);if (in_array($role, Hash::flatten($roles))) {$path = '';foreach ($roles as $roleDependencies) {$path .= implode('|', (array)$roleDependencies) . ' -> ';}trigger_error(__d('cake_dev', 'cycle detected when inheriting %s from %s. Path: %s', $role, $dependency, $path . $role));continue;}if (!isset($this->_tree[$dependency])) {$this->_tree[$dependency] = array();}$this->_tree[$dependency][] = $role;}}}}/*** adds one or more aliases to the internal map. Overwrites existing entries.** @param array $alias alias from => to (e.g. Role/13 -> Role/editor)* @return void*/public function addAlias(array $alias) {$this->aliases = $alias + $this->aliases;}/*** build an ARO tree structure for internal processing** @param array $aros array of AROs as key and their inherited AROs as values* @return void*/public function build(array $aros) {$this->_tree = array();$this->addRole($aros);}}