1: <?php
  2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17: 
 18: 
 19:  20:  21:  22:  23:  24: 
 25: class PhpAcl extends Object implements AclInterface {
 26: 
 27:  28:  29:  30:  31: 
 32:     const DENY = false;
 33: 
 34:  35:  36:  37:  38: 
 39:     const ALLOW = true;
 40: 
 41:  42:  43:  44:  45:  46:  47: 
 48:     public $options = array();
 49: 
 50:  51:  52:  53:  54: 
 55:     public $Aro = null;
 56: 
 57:  58:  59:  60:  61: 
 62:     public $Aco = null;
 63: 
 64:  65:  66:  67:  68: 
 69:     public function __construct() {
 70:         $this->options = array(
 71:             'policy' => self::DENY,
 72:             'config' => APP . 'Config' . DS . 'acl.php',
 73:         );
 74:     }
 75: 
 76:  77:  78:  79:  80:  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:  97:  98:  99: 100: 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: 123: 124: 125: 126: 127: 128: 
129:     public function allow($aro, $aco, $action = "*") {
130:         return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'allow');
131:     }
132: 
133: 134: 135: 136: 137: 138: 139: 140: 
141:     public function deny($aro, $aco, $action = "*") {
142:         return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'deny');
143:     }
144: 
145: 146: 147: 148: 149: 150: 151: 152: 
153:     public function inherit($aro, $aco, $action = "*") {
154:         return false;
155:     }
156: 
157: 158: 159: 160: 161: 162: 163: 164: 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: 199: 200: 
201: class PhpAco {
202: 
203: 204: 205: 206: 207: 
208:     protected $_tree = array();
209: 
210: 211: 212: 213: 214: 
215:     public static $modifiers = array(
216:         '*' => '.*',
217:     );
218: 
219: 220: 221: 222: 223: 
224:     public function __construct(array $rules = array()) {
225:         foreach (array('allow', 'deny') as $type) {
226:             if (empty($rules[$type])) {
227:                 $rules[$type] = array();
228:             }
229:         }
230: 
231:         $this->build($rules['allow'], $rules['deny']);
232:     }
233: 
234: 235: 236: 237: 238: 239: 
240:     public function path($aco) {
241:         $aco = $this->resolve($aco);
242:         $path = array();
243:         $level = 0;
244:         $root = $this->_tree;
245:         $stack = array(array($root, 0));
246: 
247:         while (!empty($stack)) {
248:             list($root, $level) = array_pop($stack);
249: 
250:             if (empty($path[$level])) {
251:                 $path[$level] = array();
252:             }
253: 
254:             foreach ($root as $node => $elements) {
255:                 $pattern = '/^' . str_replace(array_keys(self::$modifiers), array_values(self::$modifiers), $node) . '$/';
256: 
257:                 if ($node == $aco[$level] || preg_match($pattern, $aco[$level])) {
258:                     
259:                     foreach (array('allow', 'deny') as $policy) {
260:                         if (!empty($elements[$policy])) {
261:                             if (empty($path[$level][$policy])) {
262:                                 $path[$level][$policy] = array();
263:                             }
264:                             $path[$level][$policy] = array_merge($path[$level][$policy], $elements[$policy]);
265:                         }
266:                     }
267: 
268:                     
269:                     if (!empty($elements['children']) && isset($aco[$level + 1])) {
270:                         array_push($stack, array($elements['children'], $level + 1));
271:                     }
272:                 }
273:             }
274:         }
275: 
276:         return $path;
277:     }
278: 
279: 280: 281: 282: 283: 284: 285: 286: 287: 
288:     public function access($aro, $aco, $action, $type = 'deny') {
289:         $aco = $this->resolve($aco);
290:         $depth = count($aco);
291:         $root = $this->_tree;
292:         $tree = &$root;
293: 
294:         foreach ($aco as $i => $node) {
295:             if (!isset($tree[$node])) {
296:                 $tree[$node] = array(
297:                     'children' => array(),
298:                 );
299:             }
300: 
301:             if ($i < $depth - 1) {
302:                 $tree = &$tree[$node]['children'];
303:             } else {
304:                 if (empty($tree[$node][$type])) {
305:                     $tree[$node][$type] = array();
306:                 }
307: 
308:                 $tree[$node][$type] = array_merge(is_array($aro) ? $aro : array($aro), $tree[$node][$type]);
309:             }
310:         }
311: 
312:         $this->_tree = &$root;
313:     }
314: 
315: 316: 317: 318: 319: 320: 
321:     public function resolve($aco) {
322:         if (is_array($aco)) {
323:             return array_map('strtolower', $aco);
324:         }
325: 
326:         
327:         $aco = preg_replace('#/+#', '/', $aco);
328:         
329:         $aco = ltrim(strtolower($aco), '/');
330:         return array_filter(array_map('trim', explode('/', $aco)));
331:     }
332: 
333: 334: 335: 336: 337: 338: 339: 
340:     public function build(array $allow, array $deny = array()) {
341:         $this->_tree = array();
342: 
343:         foreach ($allow as $dotPath => $aros) {
344:             if (is_string($aros)) {
345:                 $aros = array_map('trim', explode(',', $aros));
346:             }
347: 
348:             $this->access($aros, $dotPath, null, 'allow');
349:         }
350: 
351:         foreach ($deny as $dotPath => $aros) {
352:             if (is_string($aros)) {
353:                 $aros = array_map('trim', explode(',', $aros));
354:             }
355: 
356:             $this->access($aros, $dotPath, null, 'deny');
357:         }
358:     }
359: 
360: }
361: 
362: 363: 364: 365: 
366: class PhpAro {
367: 
368: 369: 370: 371: 372: 373: 
374:     const DEFAULT_ROLE = 'Role/default';
375: 
376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 
388:     public $map = array(
389:         'User' => 'User/username',
390:         'Role' => 'User/role',
391:     );
392: 
393: 394: 395: 396: 397: 
398:     public $aliases = array();
399: 
400: 401: 402: 403: 404: 
405:     protected $_tree = array();
406: 
407: 408: 409: 410: 411: 412: 413: 
414:     public function __construct(array $aro = array(), array $map = array(), array $aliases = array()) {
415:         if (!empty($map)) {
416:             $this->map = $map;
417:         }
418: 
419:         $this->aliases = $aliases;
420:         $this->build($aro);
421:     }
422: 
423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 
433:     public function roles($aro) {
434:         $aros = array();
435:         $aro = $this->resolve($aro);
436:         $stack = array(array($aro, 0));
437: 
438:         while (!empty($stack)) {
439:             list($element, $depth) = array_pop($stack);
440:             $aros[$depth][] = $element;
441: 
442:             foreach ($this->_tree as $node => $children) {
443:                 if (in_array($element, $children)) {
444:                     array_push($stack, array($node, $depth + 1));
445:                 }
446:             }
447:         }
448: 
449:         return array_reverse($aros);
450:     }
451: 
452: 453: 454: 455: 456: 457: 458: 
459:     public function resolve($aro) {
460:         foreach ($this->map as $aroGroup => $map) {
461:             list ($model, $field) = explode('/', $map, 2);
462:             $mapped = '';
463: 
464:             if (is_array($aro)) {
465:                 if (isset($aro['model']) && isset($aro['foreign_key']) && $aro['model'] === $aroGroup) {
466:                     $mapped = $aroGroup . '/' . $aro['foreign_key'];
467:                 } elseif (isset($aro[$model][$field])) {
468:                     $mapped = $aroGroup . '/' . $aro[$model][$field];
469:                 } elseif (isset($aro[$field])) {
470:                     $mapped = $aroGroup . '/' . $aro[$field];
471:                 }
472:             } elseif (is_string($aro)) {
473:                 $aro = ltrim($aro, '/');
474: 
475:                 if (strpos($aro, '/') === false) {
476:                     $mapped = $aroGroup . '/' . $aro;
477:                 } else {
478:                     list($aroModel, $aroValue) = explode('/', $aro, 2);
479: 
480:                     $aroModel = Inflector::camelize($aroModel);
481: 
482:                     if ($aroModel === $model || $aroModel === $aroGroup) {
483:                         $mapped = $aroGroup . '/' . $aroValue;
484:                     }
485:                 }
486:             }
487: 
488:             if (isset($this->_tree[$mapped])) {
489:                 return $mapped;
490:             }
491: 
492:             
493:             if (!empty($this->aliases[$mapped])) {
494:                 return $this->aliases[$mapped];
495:             }
496:         }
497:         return self::DEFAULT_ROLE;
498:     }
499: 
500: 501: 502: 503: 504: 505: 
506:     public function addRole(array $aro) {
507:         foreach ($aro as $role => $inheritedRoles) {
508:             if (!isset($this->_tree[$role])) {
509:                 $this->_tree[$role] = array();
510:             }
511: 
512:             if (!empty($inheritedRoles)) {
513:                 if (is_string($inheritedRoles)) {
514:                     $inheritedRoles = array_map('trim', explode(',', $inheritedRoles));
515:                 }
516: 
517:                 foreach ($inheritedRoles as $dependency) {
518:                     
519:                     $roles = $this->roles($dependency);
520: 
521:                     if (in_array($role, Hash::flatten($roles))) {
522:                         $path = '';
523: 
524:                         foreach ($roles as $roleDependencies) {
525:                             $path .= implode('|', (array)$roleDependencies) . ' -> ';
526:                         }
527: 
528:                         trigger_error(__d('cake_dev', 'cycle detected when inheriting %s from %s. Path: %s', $role, $dependency, $path . $role));
529:                         continue;
530:                     }
531: 
532:                     if (!isset($this->_tree[$dependency])) {
533:                         $this->_tree[$dependency] = array();
534:                     }
535: 
536:                     $this->_tree[$dependency][] = $role;
537:                 }
538:             }
539:         }
540:     }
541: 
542: 543: 544: 545: 546: 547: 
548:     public function addAlias(array $alias) {
549:         $this->aliases = $alias + $this->aliases;
550:     }
551: 
552: 553: 554: 555: 556: 557: 
558:     public function build(array $aros) {
559:         $this->_tree = array();
560:         $this->addRole($aros);
561:     }
562: 
563: }
564: