Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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