pictcode / lib / Cake / Controller / Component / Acl / PhpAcl.php @ 0b1b8047
履歴 | 表示 | アノテート | ダウンロード (13.458 KB)
1 |
<?php
|
---|---|
2 |
/**
|
3 |
* PHP configuration based AclInterface implementation
|
4 |
*
|
5 |
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
6 |
* Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
|
13 |
* @link http://cakephp.org CakePHP(tm) Project
|
14 |
* @package Cake.Controller.Component.Acl
|
15 |
* @since CakePHP(tm) v 2.1
|
16 |
* @license http://www.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 |
*/
|
25 |
class PhpAcl extends Object 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' => APP . 'Config' . DS . '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 |
*/
|
200 |
class 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 |
*/
|
364 |
class 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 |
} |