Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
16591 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
 * @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
 */
201
class PhpAco {
202
 
203
/**
204
 * holds internal ACO representation
205
 *
206
 * @var array
207
 */
208
	protected $_tree = array();
209
 
210
/**
211
 * map modifiers for ACO paths to their respective PCRE pattern
212
 *
213
 * @var array
214
 */
215
	public static $modifiers = array(
216
		'*' => '.*',
217
	);
218
 
219
/**
220
 * Constructor
221
 *
222
 * @param array $rules Rules array
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
 * return path to the requested ACO with allow and deny rules attached on each level
236
 *
237
 * @param string $aco ACO string
238
 * @return array
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(static::$modifiers), array_values(static::$modifiers), $node) . '$/';
256
 
257
				if ($node == $aco[$level] || preg_match($pattern, $aco[$level])) {
258
					// merge allow/denies with $path of current level
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
					// traverse
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
 * allow/deny ARO access to ARO
281
 *
282
 * @param string $aro ARO string
283
 * @param string $aco ACO string
284
 * @param string $action Action string
285
 * @param string $type access type
286
 * @return void
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
 * resolve given ACO string to a path
317
 *
318
 * @param string $aco ACO string
319
 * @return array path
320
 */
321
	public function resolve($aco) {
322
		if (is_array($aco)) {
323
			return array_map('strtolower', $aco);
324
		}
325
 
326
		// strip multiple occurrences of '/'
327
		$aco = preg_replace('#/+#', '/', $aco);
328
		// make case insensitive
329
		$aco = ltrim(strtolower($aco), '/');
330
		return array_filter(array_map('trim', explode('/', $aco)));
331
	}
332
 
333
/**
334
 * build a tree representation from the given allow/deny informations for ACO paths
335
 *
336
 * @param array $allow ACO allow rules
337
 * @param array $deny ACO deny rules
338
 * @return void
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
 * Access Request Object
364
 *
365
 */
366
class PhpAro {
367
 
368
/**
369
 * role to resolve to when a provided ARO is not listed in
370
 * the internal tree
371
 *
372
 * @var string
373
 */
374
	const DEFAULT_ROLE = 'Role/default';
375
 
376
/**
377
 * map external identifiers. E.g. if
378
 *
379
 * array('User' => array('username' => 'jeff', 'role' => 'editor'))
380
 *
381
 * is passed as an ARO to one of the methods of AclComponent, PhpAcl
382
 * will check if it can be resolved to an User or a Role defined in the
383
 * configuration file.
384
 *
385
 * @var array
386
 * @see app/Config/acl.php
387
 */
388
	public $map = array(
389
		'User' => 'User/username',
390
		'Role' => 'User/role',
391
	);
392
 
393
/**
394
 * aliases to map
395
 *
396
 * @var array
397
 */
398
	public $aliases = array();
399
 
400
/**
401
 * internal ARO representation
402
 *
403
 * @var array
404
 */
405
	protected $_tree = array();
406
 
407
/**
408
 * Constructor
409
 *
410
 * @param array $aro The aro data
411
 * @param array $map The identifier mappings
412
 * @param array $aliases The aliases to map.
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
 * From the perspective of the given ARO, walk down the tree and
425
 * collect all inherited AROs levelwise such that AROs from different
426
 * branches with equal distance to the requested ARO will be collected at the same
427
 * index. The resulting array will contain a prioritized list of (list of) roles ordered from
428
 * the most distant AROs to the requested one itself.
429
 *
430
 * @param string|array $aro An ARO identifier
431
 * @return array prioritized AROs
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
 * resolve an ARO identifier to an internal ARO string using
454
 * the internal mapping information.
455
 *
456
 * @param string|array $aro ARO identifier (User.jeff, array('User' => ...), etc)
457
 * @return string internal aro string (e.g. User/jeff, Role/default)
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
			// is there a matching alias defined (e.g. Role/1 => Role/admin)?
493
			if (!empty($this->aliases[$mapped])) {
494
				return $this->aliases[$mapped];
495
			}
496
		}
497
		return static::DEFAULT_ROLE;
498
	}
499
 
500
/**
501
 * adds a new ARO to the tree
502
 *
503
 * @param array $aro one or more ARO records
504
 * @return void
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
					// detect cycles
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
 * adds one or more aliases to the internal map. Overwrites existing entries.
544
 *
545
 * @param array $alias alias from => to (e.g. Role/13 -> Role/editor)
546
 * @return void
547
 */
548
	public function addAlias(array $alias) {
549
		$this->aliases = $alias + $this->aliases;
550
	}
551
 
552
/**
553
 * build an ARO tree structure for internal processing
554
 *
555
 * @param array $aros array of AROs as key and their inherited AROs as values
556
 * @return void
557
 */
558
	public function build(array $aros) {
559
		$this->_tree = array();
560
		$this->addRole($aros);
561
	}
562
 
563
}