1: <?php
2:
3: /**
4: * This file is part of the Nette Framework (https://nette.org)
5: *
6: * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
7: *
8: * For the full copyright and license information, please view
9: * the file license.txt that was distributed with this source code.
10: */
11:
12: namespace Nette\Security;
13:
14: use Nette;
15:
16:
17:
18: /**
19: * Access control list (ACL) functionality and privileges management.
20: *
21: * This solution is mostly based on Zend_Acl (c) Zend Technologies USA Inc. (http://www.zend.com), new BSD license
22: *
23: * @copyright Copyright (c) 2005, 2007 Zend Technologies USA Inc.
24: * @author David Grudl
25: */
26: class Permission extends Nette\Object implements IAuthorizator
27: {
28: /** @var array Role storage */
29: private $roles = array();
30:
31: /** @var array Resource storage */
32: private $resources = array();
33:
34: /** @var array Access Control List rules; whitelist (deny everything to all) by default */
35: private $rules = array(
36: 'allResources' => array(
37: 'allRoles' => array(
38: 'allPrivileges' => array(
39: 'type' => self::DENY,
40: 'assert' => NULL,
41: ),
42: 'byPrivilege' => array(),
43: ),
44: 'byRole' => array(),
45: ),
46: 'byResource' => array(),
47: );
48:
49: /** @var mixed */
50: private $queriedRole, $queriedResource;
51:
52:
53:
54: /********************* roles ****************d*g**/
55:
56:
57: /**
58: * Adds a Role to the list.
59: *
60: * The $parents parameter may be a Role identifier (or array of identifiers)
61: * to indicate the Roles from which the newly added Role will directly inherit.
62: *
63: * In order to resolve potential ambiguities with conflicting rules inherited
64: * from different parents, the most recently added parent takes precedence over
65: * parents that were previously added. In other words, the first parent added
66: * will have the least priority, and the last parent added will have the
67: * highest priority.
68: *
69: * @param string
70: * @param string|array
71: * @throws \InvalidArgumentException
72: * @throws \InvalidStateException
73: * @return Permission provides a fluent interface
74: */
75: public function addRole($role, $parents = NULL)
76: {
77: $this->checkRole($role, FALSE);
78:
79: if (isset($this->roles[$role])) {
80: throw new \InvalidStateException("Role '$role' already exists in the list.");
81: }
82:
83: $roleParents = array();
84:
85: if ($parents !== NULL) {
86: if (!is_array($parents)) {
87: $parents = array($parents);
88: }
89:
90: foreach ($parents as $parent) {
91: $this->checkRole($parent);
92: $roleParents[$parent] = TRUE;
93: $this->roles[$parent]['children'][$role] = TRUE;
94: }
95: }
96:
97: $this->roles[$role] = array(
98: 'parents' => $roleParents,
99: 'children' => array(),
100: );
101:
102: return $this;
103: }
104:
105:
106:
107: /**
108: * Returns TRUE if the Role exists in the list.
109: * @param string
110: * @return bool
111: */
112: public function hasRole($role)
113: {
114: $this->checkRole($role, FALSE);
115: return isset($this->roles[$role]);
116: }
117:
118:
119:
120: /**
121: * Checks whether Role is valid and exists in the list.
122: * @param string
123: * @param bool
124: * @throws \InvalidStateException
125: * @return void
126: */
127: private function checkRole($role, $need = TRUE)
128: {
129: if (!is_string($role) || $role === '') {
130: throw new \InvalidArgumentException("Role must be a non-empty string.");
131:
132: } elseif ($need && !isset($this->roles[$role])) {
133: throw new \InvalidStateException("Role '$role' does not exist.");
134: }
135: }
136:
137:
138:
139: /**
140: * Returns an array of an existing Role's parents.
141: *
142: * The parent Roles are ordered in this array by ascending priority.
143: * The highest priority parent Role, last in the array, corresponds with
144: * the parent Role most recently added.
145: *
146: * If the Role does not have any parents, then an empty array is returned.
147: *
148: * @param string
149: * @return array
150: */
151: public function getRoleParents($role)
152: {
153: $this->checkRole($role);
154: return array_keys($this->roles[$role]['parents']);
155: }
156:
157:
158:
159: /**
160: * Returns TRUE if $role inherits from $inherit.
161: *
162: * If $onlyParents is TRUE, then $role must inherit directly from
163: * $inherit in order to return TRUE. By default, this method looks
164: * through the entire inheritance DAG to determine whether $role
165: * inherits from $inherit through its ancestor Roles.
166: *
167: * @param string
168: * @param string
169: * @param boolean
170: * @throws \InvalidStateException
171: * @return bool
172: */
173: public function roleInheritsFrom($role, $inherit, $onlyParents = FALSE)
174: {
175: $this->checkRole($role);
176: $this->checkRole($inherit);
177:
178: $inherits = isset($this->roles[$role]['parents'][$inherit]);
179:
180: if ($inherits || $onlyParents) {
181: return $inherits;
182: }
183:
184: foreach ($this->roles[$role]['parents'] as $parent => $foo) {
185: if ($this->roleInheritsFrom($parent, $inherit)) {
186: return TRUE;
187: }
188: }
189:
190: return FALSE;
191: }
192:
193:
194:
195: /**
196: * Removes the Role from the list.
197: *
198: * @param string
199: * @throws \InvalidStateException
200: * @return Permission provides a fluent interface
201: */
202: public function removeRole($role)
203: {
204: $this->checkRole($role);
205:
206: foreach ($this->roles[$role]['children'] as $child => $foo)
207: unset($this->roles[$child]['parents'][$role]);
208:
209: foreach ($this->roles[$role]['parents'] as $parent => $foo)
210: unset($this->roles[$parent]['children'][$role]);
211:
212: unset($this->roles[$role]);
213:
214: foreach ($this->rules['allResources']['byRole'] as $roleCurrent => $rules) {
215: if ($role === $roleCurrent) {
216: unset($this->rules['allResources']['byRole'][$roleCurrent]);
217: }
218: }
219:
220: foreach ($this->rules['byResource'] as $resourceCurrent => $visitor) {
221: if (isset($visitor['byRole'])) {
222: foreach ($visitor['byRole'] as $roleCurrent => $rules) {
223: if ($role === $roleCurrent) {
224: unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
225: }
226: }
227: }
228: }
229:
230: return $this;
231: }
232:
233:
234:
235: /**
236: * Removes all Roles from the list.
237: *
238: * @return Permission provides a fluent interface
239: */
240: public function removeAllRoles()
241: {
242: $this->roles = array();
243:
244: foreach ($this->rules['allResources']['byRole'] as $roleCurrent => $rules)
245: unset($this->rules['allResources']['byRole'][$roleCurrent]);
246:
247: foreach ($this->rules['byResource'] as $resourceCurrent => $visitor) {
248: foreach ($visitor['byRole'] as $roleCurrent => $rules) {
249: unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
250: }
251: }
252:
253: return $this;
254: }
255:
256:
257:
258: /********************* resources ****************d*g**/
259:
260:
261:
262: /**
263: * Adds a Resource having an identifier unique to the list.
264: *
265: * @param string
266: * @param string
267: * @throws \InvalidArgumentException
268: * @throws \InvalidStateException
269: * @return Permission provides a fluent interface
270: */
271: public function addResource($resource, $parent = NULL)
272: {
273: $this->checkResource($resource, FALSE);
274:
275: if (isset($this->resources[$resource])) {
276: throw new \InvalidStateException("Resource '$resource' already exists in the list.");
277: }
278:
279: if ($parent !== NULL) {
280: $this->checkResource($parent);
281: $this->resources[$parent]['children'][$resource] = TRUE;
282: }
283:
284: $this->resources[$resource] = array(
285: 'parent' => $parent,
286: 'children' => array()
287: );
288:
289: return $this;
290: }
291:
292:
293:
294: /**
295: * Returns TRUE if the Resource exists in the list.
296: * @param string
297: * @return bool
298: */
299: public function hasResource($resource)
300: {
301: $this->checkResource($resource, FALSE);
302: return isset($this->resources[$resource]);
303: }
304:
305:
306:
307: /**
308: * Checks whether Resource is valid and exists in the list.
309: * @param string
310: * @param bool
311: * @throws \InvalidStateException
312: * @return void
313: */
314: private function checkResource($resource, $need = TRUE)
315: {
316: if (!is_string($resource) || $resource === '') {
317: throw new \InvalidArgumentException("Resource must be a non-empty string.");
318:
319: } elseif ($need && !isset($this->resources[$resource])) {
320: throw new \InvalidStateException("Resource '$resource' does not exist.");
321: }
322: }
323:
324:
325:
326: /**
327: * Returns TRUE if $resource inherits from $inherit.
328: *
329: * If $onlyParents is TRUE, then $resource must inherit directly from
330: * $inherit in order to return TRUE. By default, this method looks
331: * through the entire inheritance tree to determine whether $resource
332: * inherits from $inherit through its ancestor Resources.
333: *
334: * @param string
335: * @param string
336: * @param boolean
337: * @throws \InvalidStateException
338: * @return bool
339: */
340: public function resourceInheritsFrom($resource, $inherit, $onlyParent = FALSE)
341: {
342: $this->checkResource($resource);
343: $this->checkResource($inherit);
344:
345: if ($this->resources[$resource]['parent'] === NULL) {
346: return FALSE;
347: }
348:
349: $parent = $this->resources[$resource]['parent'];
350: if ($inherit === $parent) {
351: return TRUE;
352:
353: } elseif ($onlyParent) {
354: return FALSE;
355: }
356:
357: while ($this->resources[$parent]['parent'] !== NULL) {
358: $parent = $this->resources[$parent]['parent'];
359: if ($inherit === $parent) {
360: return TRUE;
361: }
362: }
363:
364: return FALSE;
365: }
366:
367:
368:
369: /**
370: * Removes a Resource and all of its children.
371: *
372: * @param string
373: * @throws \InvalidStateException
374: * @return Permission provides a fluent interface
375: */
376: public function removeResource($resource)
377: {
378: $this->checkResource($resource);
379:
380: $parent = $this->resources[$resource]['parent'];
381: if ($parent !== NULL) {
382: unset($this->resources[$parent]['children'][$resource]);
383: }
384:
385: $removed = array($resource);
386: foreach ($this->resources[$resource]['children'] as $child => $foo) {
387: $this->removeResource($child);
388: $removed[] = $child;
389: }
390:
391: foreach ($removed as $resourceRemoved) {
392: foreach ($this->rules['byResource'] as $resourceCurrent => $rules) {
393: if ($resourceRemoved === $resourceCurrent) {
394: unset($this->rules['byResource'][$resourceCurrent]);
395: }
396: }
397: }
398:
399: unset($this->resources[$resource]);
400:
401: return $this;
402: }
403:
404:
405:
406: /**
407: * Removes all Resources.
408: *
409: * @return Permission provides a fluent interface
410: */
411: public function removeAllResources()
412: {
413: foreach ($this->resources as $resource => $foo) {
414: foreach ($this->rules['byResource'] as $resourceCurrent => $rules) {
415: if ($resource === $resourceCurrent) {
416: unset($this->rules['byResource'][$resourceCurrent]);
417: }
418: }
419: }
420:
421: $this->resources = array();
422: return $this;
423: }
424:
425:
426:
427: /********************* defining rules ****************d*g**/
428:
429:
430:
431: /**
432: * Adds an "allow" rule to the list. A rule is added that would allow one
433: * or more Roles access to [certain $privileges upon] the specified Resource(s).
434: *
435: * If either $roles or $resources is Permission::ALL, then the rule applies to all Roles or all Resources,
436: * respectively. Both may be Permission::ALL in order to work with the default rule of the ACL.
437: *
438: * The $privileges parameter may be used to further specify that the rule applies only
439: * to certain privileges upon the Resource(s) in question. This may be specified to be a single
440: * privilege with a string, and multiple privileges may be specified as an array of strings.
441: *
442: * If $assertion is provided, then its assert() method must return TRUE in order for
443: * the rule to apply. If $assertion is provided with $roles, $resources, and $privileges all
444: * equal to NULL, then a rule will imply a type of DENY when the rule's assertion fails.
445: *
446: * @param string|array|Permission::ALL roles
447: * @param string|array|Permission::ALL resources
448: * @param string|array|Permission::ALL privileges
449: * @param IPermissionAssertion assertion
450: * @return Permission provides a fluent interface
451: */
452: public function allow($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL, IPermissionAssertion $assertion = NULL)
453: {
454: $this->setRule(TRUE, self::ALLOW, $roles, $resources, $privileges, $assertion);
455: return $this;
456: }
457:
458:
459:
460: /**
461: * Adds a "deny" rule to the list. A rule is added that would deny one
462: * or more Roles access to [certain $privileges upon] the specified Resource(s).
463: *
464: * If either $roles or $resources is Permission::ALL, then the rule applies to all Roles or all Resources,
465: * respectively. Both may be Permission::ALL in order to work with the default rule of the ACL.
466: *
467: * The $privileges parameter may be used to further specify that the rule applies only
468: * to certain privileges upon the Resource(s) in question. This may be specified to be a single
469: * privilege with a string, and multiple privileges may be specified as an array of strings.
470: *
471: * If $assertion is provided, then its assert() method must return TRUE in order for
472: * the rule to apply. If $assertion is provided with $roles, $resources, and $privileges all
473: * equal to NULL, then a rule will imply a type of ALLOW when the rule's assertion fails.
474: *
475: * @param string|array|Permission::ALL roles
476: * @param string|array|Permission::ALL resources
477: * @param string|array|Permission::ALL privileges
478: * @param IPermissionAssertion assertion
479: * @return Permission provides a fluent interface
480: */
481: public function deny($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL, IPermissionAssertion $assertion = NULL)
482: {
483: $this->setRule(TRUE, self::DENY, $roles, $resources, $privileges, $assertion);
484: return $this;
485: }
486:
487:
488:
489: /**
490: * Removes "allow" permissions from the list. The rule is removed only in the context
491: * of the given Roles, Resources, and privileges. Existing rules to which the remove
492: * operation does not apply would remain in the
493: *
494: * @param string|array|Permission::ALL roles
495: * @param string|array|Permission::ALL resources
496: * @param string|array|Permission::ALL privileges
497: * @return Permission provides a fluent interface
498: */
499: public function removeAllow($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL)
500: {
501: $this->setRule(FALSE, self::ALLOW, $roles, $resources, $privileges);
502: return $this;
503: }
504:
505:
506:
507: /**
508: * Removes "deny" restrictions from the list. The rule is removed only in the context
509: * of the given Roles, Resources, and privileges. Existing rules to which the remove
510: * operation does not apply would remain in the
511: *
512: * @param string|array|Permission::ALL roles
513: * @param string|array|Permission::ALL resources
514: * @param string|array|Permission::ALL privileges
515: * @return Permission provides a fluent interface
516: */
517: public function removeDeny($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL)
518: {
519: $this->setRule(FALSE, self::DENY, $roles, $resources, $privileges);
520: return $this;
521: }
522:
523:
524:
525: /**
526: * Performs operations on Access Control List rules.
527: *
528: * @param bool operation add?
529: * @param bool type
530: * @param string|array|Permission::ALL roles
531: * @param string|array|Permission::ALL resources
532: * @param string|array|Permission::ALL privileges
533: * @param IPermissionAssertion assertion
534: * @throws \InvalidStateException
535: * @return Permission provides a fluent interface
536: */
537: protected function setRule($toAdd, $type, $roles, $resources, $privileges, IPermissionAssertion $assertion = NULL)
538: {
539: // ensure that all specified Roles exist; normalize input to array of Roles or NULL
540: if ($roles === self::ALL) {
541: $roles = array(self::ALL);
542:
543: } else {
544: if (!is_array($roles)) {
545: $roles = array($roles);
546: }
547:
548: foreach ($roles as $role) {
549: $this->checkRole($role);
550: }
551: }
552:
553: // ensure that all specified Resources exist; normalize input to array of Resources or NULL
554: if ($resources === self::ALL) {
555: $resources = array(self::ALL);
556:
557: } else {
558: if (!is_array($resources)) {
559: $resources = array($resources);
560: }
561:
562: foreach ($resources as $resource) {
563: $this->checkResource($resource);
564: }
565: }
566:
567: // normalize privileges to array
568: if ($privileges === self::ALL) {
569: $privileges = array();
570:
571: } elseif (!is_array($privileges)) {
572: $privileges = array($privileges);
573: }
574:
575:
576: if ($toAdd) { // add to the rules
577: foreach ($resources as $resource) {
578: foreach ($roles as $role) {
579: $rules = & $this->getRules($resource, $role, TRUE);
580: if (count($privileges) === 0) {
581: $rules['allPrivileges']['type'] = $type;
582: $rules['allPrivileges']['assert'] = $assertion;
583: if (!isset($rules['byPrivilege'])) {
584: $rules['byPrivilege'] = array();
585: }
586: } else {
587: foreach ($privileges as $privilege) {
588: $rules['byPrivilege'][$privilege]['type'] = $type;
589: $rules['byPrivilege'][$privilege]['assert'] = $assertion;
590: }
591: }
592: }
593: }
594:
595: } else { // remove from the rules
596: foreach ($resources as $resource) {
597: foreach ($roles as $role) {
598: $rules = & $this->getRules($resource, $role);
599: if ($rules === NULL) {
600: continue;
601: }
602: if (count($privileges) === 0) {
603: if ($resource === self::ALL && $role === self::ALL) {
604: if ($type === $rules['allPrivileges']['type']) {
605: $rules = array(
606: 'allPrivileges' => array(
607: 'type' => self::DENY,
608: 'assert' => NULL
609: ),
610: 'byPrivilege' => array()
611: );
612: }
613: continue;
614: }
615: if ($type === $rules['allPrivileges']['type']) {
616: unset($rules['allPrivileges']);
617: }
618: } else {
619: foreach ($privileges as $privilege) {
620: if (isset($rules['byPrivilege'][$privilege]) &&
621: $type === $rules['byPrivilege'][$privilege]['type']) {
622: unset($rules['byPrivilege'][$privilege]);
623: }
624: }
625: }
626: }
627: }
628: }
629: return $this;
630: }
631:
632:
633:
634: /********************* querying the ACL ****************d*g**/
635:
636:
637:
638: /**
639: * Returns TRUE if and only if the Role has access to the Resource.
640: *
641: * If either $role or $resource is Permission::ALL, then the query applies to all Roles or all Resources,
642: * respectively. Both may be Permission::ALL to query whether the ACL has a "blacklist" rule
643: * (allow everything to all). By default, Permission creates a "whitelist" rule (deny
644: * everything to all), and this method would return FALSE unless this default has
645: * been overridden (i.e., by executing $acl->allow()).
646: *
647: * If a $privilege is not provided, then this method returns FALSE if and only if the
648: * Role is denied access to at least one privilege upon the Resource. In other words, this
649: * method returns TRUE if and only if the Role is allowed all privileges on the Resource.
650: *
651: * This method checks Role inheritance using a depth-first traversal of the Role list.
652: * The highest priority parent (i.e., the parent most recently added) is checked first,
653: * and its respective parents are checked similarly before the lower-priority parents of
654: * the Role are checked.
655: *
656: * @param string|Permission::ALL|IRole role
657: * @param string|Permission::ALL|IResource resource
658: * @param string|Permission::ALL privilege
659: * @throws \InvalidStateException
660: * @return bool
661: */
662: public function isAllowed($role = self::ALL, $resource = self::ALL, $privilege = self::ALL)
663: {
664: $this->queriedRole = $role;
665: if ($role !== self::ALL) {
666: if ($role instanceof IRole) {
667: $role = $role->getRoleId();
668: }
669: $this->checkRole($role);
670: }
671:
672: $this->queriedResource = $resource;
673: if ($resource !== self::ALL) {
674: if ($resource instanceof IResource) {
675: $resource = $resource->getResourceId();
676: }
677: $this->checkResource($resource);
678: }
679:
680: if ($privilege === self::ALL) {
681: // query on all privileges
682: do {
683: // depth-first search on $role if it is not 'allRoles' pseudo-parent
684: if ($role !== NULL && NULL !== ($result = $this->roleDFSAllPrivileges($role, $resource))) {
685: break;
686: }
687:
688: // look for rule on 'allRoles' psuedo-parent
689: if (NULL !== ($rules = $this->getRules($resource, self::ALL))) {
690: foreach ($rules['byPrivilege'] as $privilege => $rule) {
691: if (self::DENY === ($ruleTypeOnePrivilege = $this->getRuleType($resource, NULL, $privilege))) {
692: $result = self::DENY;
693: break 2;
694: }
695: }
696: if (NULL !== ($ruleTypeAllPrivileges = $this->getRuleType($resource, NULL, NULL))) {
697: $result = self::ALLOW === $ruleTypeAllPrivileges;
698: break;
699: }
700: }
701:
702: // try next Resource
703: $resource = $this->resources[$resource]['parent'];
704:
705: } while (TRUE); // loop terminates at 'allResources' pseudo-parent
706:
707: } else {
708: // query on one privilege
709: do {
710: // depth-first search on $role if it is not 'allRoles' pseudo-parent
711: if ($role !== NULL && NULL !== ($result = $this->roleDFSOnePrivilege($role, $resource, $privilege))) {
712: break;
713: }
714:
715: // look for rule on 'allRoles' pseudo-parent
716: if (NULL !== ($ruleType = $this->getRuleType($resource, NULL, $privilege))) {
717: $result = self::ALLOW === $ruleType;
718: break;
719:
720: } elseif (NULL !== ($ruleTypeAllPrivileges = $this->getRuleType($resource, NULL, NULL))) {
721: $result = self::ALLOW === $ruleTypeAllPrivileges;
722: break;
723: }
724:
725: // try next Resource
726: $resource = $this->resources[$resource]['parent'];
727:
728: } while (TRUE); // loop terminates at 'allResources' pseudo-parent
729: }
730:
731: $this->queriedRole = $this->queriedResource = NULL;
732: return $result;
733: }
734:
735:
736:
737: /**
738: * Returns real currently queried Role. Use by {@link IPermissionAssertion::asert()}.
739: * @return mixed
740: */
741: public function getQueriedRole()
742: {
743: return $this->queriedRole;
744: }
745:
746:
747:
748: /**
749: * Returns real currently queried Resource. Use by {@link IPermissionAssertion::asert()}.
750: * @return mixed
751: */
752: public function getQueriedResource()
753: {
754: return $this->queriedResource;
755: }
756:
757:
758:
759: /********************* internals ****************d*g**/
760:
761:
762:
763: /**
764: * Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule.
765: * allowing/denying $role access to all privileges upon $resource
766: *
767: * This method returns TRUE if a rule is found and allows access. If a rule exists and denies access,
768: * then this method returns FALSE. If no applicable rule is found, then this method returns NULL.
769: *
770: * @param string role
771: * @param string resource
772: * @return bool|NULL
773: */
774: private function roleDFSAllPrivileges($role, $resource)
775: {
776: $dfs = array(
777: 'visited' => array(),
778: 'stack' => array($role),
779: );
780:
781: while (NULL !== ($role = array_pop($dfs['stack']))) {
782: if (!isset($dfs['visited'][$role])) {
783: if (NULL !== ($result = $this->roleDFSVisitAllPrivileges($role, $resource, $dfs))) {
784: return $result;
785: }
786: }
787: }
788:
789: return NULL;
790: }
791:
792:
793:
794: /**
795: * Visits a $role in order to look for a rule allowing/denying $role access to all privileges upon $resource.
796: *
797: * This method returns TRUE if a rule is found and allows access. If a rule exists and denies access,
798: * then this method returns FALSE. If no applicable rule is found, then this method returns NULL.
799: *
800: * This method is used by the internal depth-first search algorithm and may modify the DFS data structure.
801: *
802: * @param string role
803: * @param string resource
804: * @param array dfs
805: * @return bool|NULL
806: */
807: private function roleDFSVisitAllPrivileges($role, $resource, &$dfs)
808: {
809: if (NULL !== ($rules = $this->getRules($resource, $role))) {
810: foreach ($rules['byPrivilege'] as $privilege => $rule) {
811: if (self::DENY === $this->getRuleType($resource, $role, $privilege)) {
812: return self::DENY;
813: }
814: }
815: if (NULL !== ($type = $this->getRuleType($resource, $role, NULL))) {
816: return self::ALLOW === $type;
817: }
818: }
819:
820: $dfs['visited'][$role] = TRUE;
821: foreach ($this->roles[$role]['parents'] as $roleParent => $foo) {
822: $dfs['stack'][] = $roleParent;
823: }
824:
825: return NULL;
826: }
827:
828:
829:
830: /**
831: * Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule.
832: * allowing/denying $role access to a $privilege upon $resource
833: *
834: * This method returns TRUE if a rule is found and allows access. If a rule exists and denies access,
835: * then this method returns FALSE. If no applicable rule is found, then this method returns NULL.
836: *
837: * @param string role
838: * @param string resource
839: * @param string privilege
840: * @return bool|NULL
841: */
842: private function roleDFSOnePrivilege($role, $resource, $privilege)
843: {
844: $dfs = array(
845: 'visited' => array(),
846: 'stack' => array($role),
847: );
848:
849: while (NULL !== ($role = array_pop($dfs['stack']))) {
850: if (!isset($dfs['visited'][$role])) {
851: if (NULL !== ($result = $this->roleDFSVisitOnePrivilege($role, $resource, $privilege, $dfs))) {
852: return $result;
853: }
854: }
855: }
856:
857: return NULL;
858: }
859:
860:
861:
862: /**
863: * Visits a $role in order to look for a rule allowing/denying $role access to a $privilege upon $resource.
864: *
865: * This method returns TRUE if a rule is found and allows access. If a rule exists and denies access,
866: * then this method returns FALSE. If no applicable rule is found, then this method returns NULL.
867: *
868: * This method is used by the internal depth-first search algorithm and may modify the DFS data structure.
869: *
870: * @param string role
871: * @param string resource
872: * @param string privilege
873: * @param array dfs
874: * @return bool|NULL
875: */
876: private function roleDFSVisitOnePrivilege($role, $resource, $privilege, &$dfs)
877: {
878: if (NULL !== ($type = $this->getRuleType($resource, $role, $privilege))) {
879: return self::ALLOW === $type;
880: }
881:
882: if (NULL !== ($type = $this->getRuleType($resource, $role, NULL))) {
883: return self::ALLOW === $type;
884: }
885:
886: $dfs['visited'][$role] = TRUE;
887: foreach ($this->roles[$role]['parents'] as $roleParent => $foo)
888: $dfs['stack'][] = $roleParent;
889:
890: return NULL;
891: }
892:
893:
894:
895: /**
896: * Returns the rule type associated with the specified Resource, Role, and privilege.
897: * combination.
898: *
899: * If a rule does not exist or its attached assertion fails, which means that
900: * the rule is not applicable, then this method returns NULL. Otherwise, the
901: * rule type applies and is returned as either ALLOW or DENY.
902: *
903: * If $resource or $role is Permission::ALL, then this means that the rule must apply to
904: * all Resources or Roles, respectively.
905: *
906: * If $privilege is Permission::ALL, then the rule must apply to all privileges.
907: *
908: * If all three parameters are Permission::ALL, then the default ACL rule type is returned,
909: * based on whether its assertion method passes.
910: *
911: * @param string|Permission::ALL role
912: * @param string|Permission::ALL resource
913: * @param string|Permission::ALL privilege
914: * @return bool|NULL
915: */
916: private function getRuleType($resource, $role, $privilege)
917: {
918: // get the rules for the $resource and $role
919: if (NULL === ($rules = $this->getRules($resource, $role))) {
920: return NULL;
921: }
922:
923: // follow $privilege
924: if ($privilege === self::ALL) {
925: if (isset($rules['allPrivileges'])) {
926: $rule = $rules['allPrivileges'];
927: } else {
928: return NULL;
929: }
930: } elseif (!isset($rules['byPrivilege'][$privilege])) {
931: return NULL;
932:
933: } else {
934: $rule = $rules['byPrivilege'][$privilege];
935: }
936:
937: // check assertion if necessary
938: if ($rule['assert'] === NULL || $rule['assert']->assert($this, $role, $resource, $privilege)) {
939: return $rule['type'];
940:
941: } elseif ($resource !== self::ALL || $role !== self::ALL || $privilege !== self::ALL) {
942: return NULL;
943:
944: } elseif (self::ALLOW === $rule['type']) {
945: return self::DENY;
946:
947: } else {
948: return self::ALLOW;
949: }
950: }
951:
952:
953:
954: /**
955: * Returns the rules associated with a Resource and a Role, or NULL if no such rules exist.
956: *
957: * If either $resource or $role is Permission::ALL, this means that the rules returned are for all Resources or all Roles,
958: * respectively. Both can be Permission::ALL to return the default rule set for all Resources and all Roles.
959: *
960: * If the $create parameter is TRUE, then a rule set is first created and then returned to the caller.
961: *
962: * @param string|Permission::ALL resource
963: * @param string|Permission::ALL role
964: * @param boolean create
965: * @return array|NULL
966: */
967: private function & getRules($resource, $role, $create = FALSE)
968: {
969: // follow $resource
970: if ($resource === self::ALL) {
971: $visitor = & $this->rules['allResources'];
972: } else {
973: if (!isset($this->rules['byResource'][$resource])) {
974: if (!$create) {
975: $null = NULL;
976: return $null;
977: }
978: $this->rules['byResource'][$resource] = array();
979: }
980: $visitor = & $this->rules['byResource'][$resource];
981: }
982:
983:
984: // follow $role
985: if ($role === self::ALL) {
986: if (!isset($visitor['allRoles'])) {
987: if (!$create) {
988: $null = NULL;
989: return $null;
990: }
991: $visitor['allRoles']['byPrivilege'] = array();
992: }
993: return $visitor['allRoles'];
994: }
995:
996: if (!isset($visitor['byRole'][$role])) {
997: if (!$create) {
998: $null = NULL;
999: return $null;
1000: }
1001: $visitor['byRole'][$role]['byPrivilege'] = array();
1002: }
1003:
1004: return $visitor['byRole'][$role];
1005: }
1006:
1007: }
1008: