1: <?php
2: /**
3: * Access Control List factory class.
4: *
5: * Permissions system.
6: *
7: * PHP 5
8: *
9: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10: * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
11: *
12: * Licensed under The MIT License
13: * Redistributions of files must retain the above copyright notice.
14: *
15: * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
16: * @link http://cakephp.org CakePHP(tm) Project
17: * @package Cake.Controller.Component
18: * @since CakePHP(tm) v 0.10.0.1076
19: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
20: */
21:
22: App::uses('Component', 'Controller');
23:
24: /**
25: * Access Control List factory class.
26: *
27: * Uses a strategy pattern to allow custom ACL implementations to be used with the same component interface.
28: * You can define by changing `Configure::write('Acl.classname', 'DbAcl');` in your core.php. Concrete ACL
29: * implementations should extend `AclBase` and implement the methods it defines.
30: *
31: * @package Cake.Controller.Component
32: * @link http://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html
33: */
34: class AclComponent extends Component {
35:
36: /**
37: * Instance of an ACL class
38: *
39: * @var AclInterface
40: */
41: protected $_Instance = null;
42:
43: /**
44: * Aro object.
45: *
46: * @var string
47: */
48: public $Aro;
49:
50: /**
51: * Aco object
52: *
53: * @var string
54: */
55: public $Aco;
56:
57: /**
58: * Constructor. Will return an instance of the correct ACL class as defined in `Configure::read('Acl.classname')`
59: *
60: * @param ComponentCollection $collection
61: * @param array $settings
62: * @throws CakeException when Acl.classname could not be loaded.
63: */
64: public function __construct(ComponentCollection $collection, $settings = array()) {
65: parent::__construct($collection, $settings);
66: $name = Inflector::camelize(strtolower(Configure::read('Acl.classname')));
67: if (!class_exists($name)) {
68: if (App::import('Component', $name)) {
69: list($plugin, $name) = pluginSplit($name);
70: $name .= 'Component';
71: } else {
72: throw new CakeException(__d('cake_dev', 'Could not find %s.', $name));
73: }
74: }
75: $this->adapter($name);
76: }
77:
78: /**
79: * Sets or gets the Adapter object currently in the AclComponent.
80: *
81: * `$this->Acl->adapter();` will get the current adapter class while
82: * `$this->Acl->adapter($obj);` will set the adapter class
83: *
84: * Will call the initialize method on the adapter if setting a new one.
85: *
86: * @param mixed $adapter Instance of AclBase or a string name of the class to use. (optional)
87: * @return mixed either null, or instance of AclBase
88: * @throws CakeException when the given class is not an AclBase
89: */
90: public function adapter($adapter = null) {
91: if ($adapter) {
92: if (is_string($adapter)) {
93: $adapter = new $adapter();
94: }
95: if (!$adapter instanceof AclInterface) {
96: throw new CakeException(__d('cake_dev', 'AclComponent adapters must implement AclInterface'));
97: }
98: $this->_Instance = $adapter;
99: $this->_Instance->initialize($this);
100: return;
101: }
102: return $this->_Instance;
103: }
104:
105: /**
106: * Pass-thru function for ACL check instance. Check methods
107: * are used to check whether or not an ARO can access an ACO
108: *
109: * @param string $aro ARO The requesting object identifier.
110: * @param string $aco ACO The controlled object identifier.
111: * @param string $action Action (defaults to *)
112: * @return boolean Success
113: */
114: public function check($aro, $aco, $action = "*") {
115: return $this->_Instance->check($aro, $aco, $action);
116: }
117:
118: /**
119: * Pass-thru function for ACL allow instance. Allow methods
120: * are used to grant an ARO access to an ACO.
121: *
122: * @param string $aro ARO The requesting object identifier.
123: * @param string $aco ACO The controlled object identifier.
124: * @param string $action Action (defaults to *)
125: * @return boolean Success
126: */
127: public function allow($aro, $aco, $action = "*") {
128: return $this->_Instance->allow($aro, $aco, $action);
129: }
130:
131: /**
132: * Pass-thru function for ACL deny instance. Deny methods
133: * are used to remove permission from an ARO to access an ACO.
134: *
135: * @param string $aro ARO The requesting object identifier.
136: * @param string $aco ACO The controlled object identifier.
137: * @param string $action Action (defaults to *)
138: * @return boolean Success
139: */
140: public function deny($aro, $aco, $action = "*") {
141: return $this->_Instance->deny($aro, $aco, $action);
142: }
143:
144: /**
145: * Pass-thru function for ACL inherit instance. Inherit methods
146: * modify the permission for an ARO to be that of its parent object.
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 boolean Success
152: */
153: public function inherit($aro, $aco, $action = "*") {
154: return $this->_Instance->inherit($aro, $aco, $action);
155: }
156:
157: /**
158: * Pass-thru function for ACL grant instance. An alias for AclComponent::allow()
159: *
160: * @param string $aro ARO The requesting object identifier.
161: * @param string $aco ACO The controlled object identifier.
162: * @param string $action Action (defaults to *)
163: * @return boolean Success
164: * @deprecated
165: */
166: public function grant($aro, $aco, $action = "*") {
167: trigger_error(__d('cake_dev', 'AclComponent::grant() is deprecated, use allow() instead'), E_USER_WARNING);
168: return $this->_Instance->allow($aro, $aco, $action);
169: }
170:
171: /**
172: * Pass-thru function for ACL grant instance. An alias for AclComponent::deny()
173: *
174: * @param string $aro ARO The requesting object identifier.
175: * @param string $aco ACO The controlled object identifier.
176: * @param string $action Action (defaults to *)
177: * @return boolean Success
178: * @deprecated
179: */
180: public function revoke($aro, $aco, $action = "*") {
181: trigger_error(__d('cake_dev', 'AclComponent::revoke() is deprecated, use deny() instead'), E_USER_WARNING);
182: return $this->_Instance->deny($aro, $aco, $action);
183: }
184: }
185:
186: /**
187: * Access Control List interface.
188: * Implementing classes are used by AclComponent to perform ACL checks in Cake.
189: *
190: * @package Cake.Controller.Component
191: */
192: interface AclInterface {
193:
194: /**
195: * Empty method to be overridden in subclasses
196: *
197: * @param string $aro ARO The requesting object identifier.
198: * @param string $aco ACO The controlled object identifier.
199: * @param string $action Action (defaults to *)
200: */
201: public function check($aro, $aco, $action = "*");
202:
203: /**
204: * Allow methods are used to grant an ARO access to an ACO.
205: *
206: * @param string $aro ARO The requesting object identifier.
207: * @param string $aco ACO The controlled object identifier.
208: * @param string $action Action (defaults to *)
209: * @return boolean Success
210: */
211: public function allow($aro, $aco, $action = "*");
212:
213: /**
214: * Deny methods are used to remove permission from an ARO to access an ACO.
215: *
216: * @param string $aro ARO The requesting object identifier.
217: * @param string $aco ACO The controlled object identifier.
218: * @param string $action Action (defaults to *)
219: * @return boolean Success
220: */
221: public function deny($aro, $aco, $action = "*");
222:
223: /**
224: * Inherit methods modify the permission for an ARO to be that of its parent object.
225: *
226: * @param string $aro ARO The requesting object identifier.
227: * @param string $aco ACO The controlled object identifier.
228: * @param string $action Action (defaults to *)
229: * @return boolean Success
230: */
231: public function inherit($aro, $aco, $action = "*");
232:
233: /**
234: * Initialization method for the Acl implementation
235: *
236: * @param AclComponent $component
237: */
238: public function initialize($component);
239: }
240:
241: /**
242: * DbAcl implements an ACL control system in the database. ARO's and ACO's are
243: * structured into trees and a linking table is used to define permissions. You
244: * can install the schema for DbAcl with the Schema Shell.
245: *
246: * `$aco` and `$aro` parameters can be slash delimited paths to tree nodes.
247: *
248: * eg. `controllers/Users/edit`
249: *
250: * Would point to a tree structure like
251: *
252: * {{{
253: * controllers
254: * Users
255: * edit
256: * }}}
257: *
258: * @package Cake.Controller.Component
259: */
260: class DbAcl extends Object implements AclInterface {
261:
262: /**
263: * Constructor
264: *
265: */
266: public function __construct() {
267: parent::__construct();
268: App::uses('AclNode', 'Model');
269: $this->Aro = ClassRegistry::init(array('class' => 'Aro', 'alias' => 'Aro'));
270: $this->Aco = ClassRegistry::init(array('class' => 'Aco', 'alias' => 'Aco'));
271: }
272:
273: /**
274: * Initializes the containing component and sets the Aro/Aco objects to it.
275: *
276: * @param AclComponent $component
277: * @return void
278: */
279: public function initialize($component) {
280: $component->Aro = $this->Aro;
281: $component->Aco = $this->Aco;
282: }
283:
284: /**
285: * Checks if the given $aro has access to action $action in $aco
286: *
287: * @param string $aro ARO The requesting object identifier.
288: * @param string $aco ACO The controlled object identifier.
289: * @param string $action Action (defaults to *)
290: * @return boolean Success (true if ARO has access to action in ACO, false otherwise)
291: * @link http://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#checking-permissions-the-acl-component
292: */
293: public function check($aro, $aco, $action = "*") {
294: if ($aro == null || $aco == null) {
295: return false;
296: }
297:
298: $permKeys = $this->_getAcoKeys($this->Aro->Permission->schema());
299: $aroPath = $this->Aro->node($aro);
300: $acoPath = $this->Aco->node($aco);
301:
302: if (empty($aroPath) || empty($acoPath)) {
303: trigger_error(__d('cake_dev', "DbAcl::check() - Failed ARO/ACO node lookup in permissions check. Node references:\nAro: ") . print_r($aro, true) . "\nAco: " . print_r($aco, true), E_USER_WARNING);
304: return false;
305: }
306:
307: if ($acoPath == null || $acoPath == array()) {
308: trigger_error(__d('cake_dev', "DbAcl::check() - Failed ACO node lookup in permissions check. Node references:\nAro: ") . print_r($aro, true) . "\nAco: " . print_r($aco, true), E_USER_WARNING);
309: return false;
310: }
311:
312: if ($action != '*' && !in_array('_' . $action, $permKeys)) {
313: trigger_error(__d('cake_dev', "ACO permissions key %s does not exist in DbAcl::check()", $action), E_USER_NOTICE);
314: return false;
315: }
316:
317: $inherited = array();
318: $acoIDs = Set::extract($acoPath, '{n}.' . $this->Aco->alias . '.id');
319:
320: $count = count($aroPath);
321: for ($i = 0 ; $i < $count; $i++) {
322: $permAlias = $this->Aro->Permission->alias;
323:
324: $perms = $this->Aro->Permission->find('all', array(
325: 'conditions' => array(
326: "{$permAlias}.aro_id" => $aroPath[$i][$this->Aro->alias]['id'],
327: "{$permAlias}.aco_id" => $acoIDs
328: ),
329: 'order' => array($this->Aco->alias . '.lft' => 'desc'),
330: 'recursive' => 0
331: ));
332:
333: if (empty($perms)) {
334: continue;
335: } else {
336: $perms = Set::extract($perms, '{n}.' . $this->Aro->Permission->alias);
337: foreach ($perms as $perm) {
338: if ($action == '*') {
339:
340: foreach ($permKeys as $key) {
341: if (!empty($perm)) {
342: if ($perm[$key] == -1) {
343: return false;
344: } elseif ($perm[$key] == 1) {
345: $inherited[$key] = 1;
346: }
347: }
348: }
349:
350: if (count($inherited) === count($permKeys)) {
351: return true;
352: }
353: } else {
354: switch ($perm['_' . $action]) {
355: case -1:
356: return false;
357: case 0:
358: continue;
359: break;
360: case 1:
361: return true;
362: break;
363: }
364: }
365: }
366: }
367: }
368: return false;
369: }
370:
371: /**
372: * Allow $aro to have access to action $actions in $aco
373: *
374: * @param string $aro ARO The requesting object identifier.
375: * @param string $aco ACO The controlled object identifier.
376: * @param string $actions Action (defaults to *)
377: * @param integer $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit)
378: * @return boolean Success
379: * @link http://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#assigning-permissions
380: */
381: public function allow($aro, $aco, $actions = "*", $value = 1) {
382: $perms = $this->getAclLink($aro, $aco);
383: $permKeys = $this->_getAcoKeys($this->Aro->Permission->schema());
384: $save = array();
385:
386: if ($perms == false) {
387: trigger_error(__d('cake_dev', 'DbAcl::allow() - Invalid node'), E_USER_WARNING);
388: return false;
389: }
390: if (isset($perms[0])) {
391: $save = $perms[0][$this->Aro->Permission->alias];
392: }
393:
394: if ($actions == "*") {
395: $permKeys = $this->_getAcoKeys($this->Aro->Permission->schema());
396: $save = array_combine($permKeys, array_pad(array(), count($permKeys), $value));
397: } else {
398: if (!is_array($actions)) {
399: $actions = array('_' . $actions);
400: }
401: if (is_array($actions)) {
402: foreach ($actions as $action) {
403: if ($action{0} != '_') {
404: $action = '_' . $action;
405: }
406: if (in_array($action, $permKeys)) {
407: $save[$action] = $value;
408: }
409: }
410: }
411: }
412: list($save['aro_id'], $save['aco_id']) = array($perms['aro'], $perms['aco']);
413:
414: if ($perms['link'] != null && !empty($perms['link'])) {
415: $save['id'] = $perms['link'][0][$this->Aro->Permission->alias]['id'];
416: } else {
417: unset($save['id']);
418: $this->Aro->Permission->id = null;
419: }
420: return ($this->Aro->Permission->save($save) !== false);
421: }
422:
423: /**
424: * Deny access for $aro to action $action in $aco
425: *
426: * @param string $aro ARO The requesting object identifier.
427: * @param string $aco ACO The controlled object identifier.
428: * @param string $action Action (defaults to *)
429: * @return boolean Success
430: * @link http://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#assigning-permissions
431: */
432: public function deny($aro, $aco, $action = "*") {
433: return $this->allow($aro, $aco, $action, -1);
434: }
435:
436: /**
437: * Let access for $aro to action $action in $aco be inherited
438: *
439: * @param string $aro ARO The requesting object identifier.
440: * @param string $aco ACO The controlled object identifier.
441: * @param string $action Action (defaults to *)
442: * @return boolean Success
443: */
444: public function inherit($aro, $aco, $action = "*") {
445: return $this->allow($aro, $aco, $action, 0);
446: }
447:
448: /**
449: * Allow $aro to have access to action $actions in $aco
450: *
451: * @param string $aro ARO The requesting object identifier.
452: * @param string $aco ACO The controlled object identifier.
453: * @param string $action Action (defaults to *)
454: * @return boolean Success
455: * @see allow()
456: */
457: public function grant($aro, $aco, $action = "*") {
458: return $this->allow($aro, $aco, $action);
459: }
460:
461: /**
462: * Deny access for $aro to action $action in $aco
463: *
464: * @param string $aro ARO The requesting object identifier.
465: * @param string $aco ACO The controlled object identifier.
466: * @param string $action Action (defaults to *)
467: * @return boolean Success
468: * @see deny()
469: */
470: public function revoke($aro, $aco, $action = "*") {
471: return $this->deny($aro, $aco, $action);
472: }
473:
474: /**
475: * Get an array of access-control links between the given Aro and Aco
476: *
477: * @param string $aro ARO The requesting object identifier.
478: * @param string $aco ACO The controlled object identifier.
479: * @return array Indexed array with: 'aro', 'aco' and 'link'
480: */
481: public function getAclLink($aro, $aco) {
482: $obj = array();
483: $obj['Aro'] = $this->Aro->node($aro);
484: $obj['Aco'] = $this->Aco->node($aco);
485:
486: if (empty($obj['Aro']) || empty($obj['Aco'])) {
487: return false;
488: }
489:
490: return array(
491: 'aro' => Set::extract($obj, 'Aro.0.' . $this->Aro->alias . '.id'),
492: 'aco' => Set::extract($obj, 'Aco.0.' . $this->Aco->alias . '.id'),
493: 'link' => $this->Aro->Permission->find('all', array('conditions' => array(
494: $this->Aro->Permission->alias . '.aro_id' => Set::extract($obj, 'Aro.0.' . $this->Aro->alias . '.id'),
495: $this->Aro->Permission->alias . '.aco_id' => Set::extract($obj, 'Aco.0.' . $this->Aco->alias . '.id')
496: )))
497: );
498: }
499:
500: /**
501: * Get the keys used in an ACO
502: *
503: * @param array $keys Permission model info
504: * @return array ACO keys
505: */
506: protected function _getAcoKeys($keys) {
507: $newKeys = array();
508: $keys = array_keys($keys);
509: foreach ($keys as $key) {
510: if (!in_array($key, array('id', 'aro_id', 'aco_id'))) {
511: $newKeys[] = $key;
512: }
513: }
514: return $newKeys;
515: }
516: }
517:
518: /**
519: * IniAcl implements an access control system using an INI file. An example
520: * of the ini file used can be found in /config/acl.ini.php.
521: *
522: * @package Cake.Controller.Component
523: */
524: class IniAcl extends Object implements AclInterface {
525:
526: /**
527: * Array with configuration, parsed from ini file
528: *
529: * @var array
530: */
531: public $config = null;
532:
533: /**
534: * The Set::classicExtract() path to the user/aro identifier in the
535: * acl.ini file. This path will be used to extract the string
536: * representation of a user used in the ini file.
537: *
538: * @var string
539: */
540: public $userPath = 'User.username';
541:
542: /**
543: * Initialize method
544: *
545: * @param AclBase $component
546: * @return void
547: */
548: public function initialize($component) {
549:
550: }
551:
552: /**
553: * No op method, allow cannot be done with IniAcl
554: *
555: * @param string $aro ARO The requesting object identifier.
556: * @param string $aco ACO The controlled object identifier.
557: * @param string $action Action (defaults to *)
558: * @return boolean Success
559: */
560: public function allow($aro, $aco, $action = "*") {
561:
562: }
563:
564: /**
565: * No op method, deny cannot be done with IniAcl
566: *
567: * @param string $aro ARO The requesting object identifier.
568: * @param string $aco ACO The controlled object identifier.
569: * @param string $action Action (defaults to *)
570: * @return boolean Success
571: */
572: public function deny($aro, $aco, $action = "*") {
573:
574: }
575:
576: /**
577: * No op method, inherit cannot be done with IniAcl
578: *
579: * @param string $aro ARO The requesting object identifier.
580: * @param string $aco ACO The controlled object identifier.
581: * @param string $action Action (defaults to *)
582: * @return boolean Success
583: */
584: public function inherit($aro, $aco, $action = "*") {
585:
586: }
587:
588: /**
589: * Main ACL check function. Checks to see if the ARO (access request object) has access to the
590: * ACO (access control object).Looks at the acl.ini.php file for permissions
591: * (see instructions in /config/acl.ini.php).
592: *
593: * @param string $aro ARO
594: * @param string $aco ACO
595: * @param string $aco_action Action
596: * @return boolean Success
597: */
598: public function check($aro, $aco, $aco_action = null) {
599: if ($this->config == null) {
600: $this->config = $this->readConfigFile(APP . 'Config' . DS . 'acl.ini.php');
601: }
602: $aclConfig = $this->config;
603:
604: if (is_array($aro)) {
605: $aro = Set::classicExtract($aro, $this->userPath);
606: }
607:
608: if (isset($aclConfig[$aro]['deny'])) {
609: $userDenies = $this->arrayTrim(explode(",", $aclConfig[$aro]['deny']));
610:
611: if (array_search($aco, $userDenies)) {
612: return false;
613: }
614: }
615:
616: if (isset($aclConfig[$aro]['allow'])) {
617: $userAllows = $this->arrayTrim(explode(",", $aclConfig[$aro]['allow']));
618:
619: if (array_search($aco, $userAllows)) {
620: return true;
621: }
622: }
623:
624: if (isset($aclConfig[$aro]['groups'])) {
625: $userGroups = $this->arrayTrim(explode(",", $aclConfig[$aro]['groups']));
626:
627: foreach ($userGroups as $group) {
628: if (array_key_exists($group, $aclConfig)) {
629: if (isset($aclConfig[$group]['deny'])) {
630: $groupDenies = $this->arrayTrim(explode(",", $aclConfig[$group]['deny']));
631:
632: if (array_search($aco, $groupDenies)) {
633: return false;
634: }
635: }
636:
637: if (isset($aclConfig[$group]['allow'])) {
638: $groupAllows = $this->arrayTrim(explode(",", $aclConfig[$group]['allow']));
639:
640: if (array_search($aco, $groupAllows)) {
641: return true;
642: }
643: }
644: }
645: }
646: }
647: return false;
648: }
649:
650: /**
651: * Parses an INI file and returns an array that reflects the INI file's section structure. Double-quote friendly.
652: *
653: * @param string $filename File
654: * @return array INI section structure
655: */
656: public function readConfigFile($filename) {
657: App::uses('IniReader', 'Configure');
658: $iniFile = new IniReader(dirname($filename) . DS);
659: return $iniFile->read(basename($filename));
660: }
661:
662: /**
663: * Removes trailing spaces on all array elements (to prepare for searching)
664: *
665: * @param array $array Array to trim
666: * @return array Trimmed array
667: */
668: public function arrayTrim($array) {
669: foreach ($array as $key => $value) {
670: $array[$key] = trim($value);
671: }
672: array_unshift($array, "");
673: return $array;
674: }
675: }
676: