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