統計
| ブランチ: | リビジョン:

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
}