Source for file Permission.php
Documentation is available at Permission.php
6: * @copyright Copyright (c) 2004, 2010 David Grudl
7: * @license http://nettephp.com/license Nette license
8: * @link http://nettephp.com
10: * @package Nette\Security
16: * Access control list (ACL) functionality and privileges management.
18: * This solution is mostly based on Zend_Acl (c) Zend Technologies USA Inc. (http://www.zend.com), new BSD license
20: * @copyright Copyright (c) 2005, 2007 Zend Technologies USA Inc.
21: * @copyright Copyright (c) 2004, 2010 David Grudl
22: * @package Nette\Security
26: /** @var array Role storage */
27: private $roles =
array();
29: /** @var array Resource storage */
30: private $resources =
array();
32: /** @var array Access Control List rules; whitelist (deny everything to all) by default */
33: private $rules =
array(
34: 'allResources' =>
array(
36: 'allPrivileges' =>
array(
37: 'type' =>
self::DENY,
40: 'byPrivilege' =>
array(),
44: 'byResource' =>
array(),
48: private $queriedRole, $queriedResource;
52: /********************* roles ****************d*g**/
56: * Adds a Role to the list.
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.
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
68: * @param string|array
69: * @throws InvalidArgumentException
70: * @throws InvalidStateException
71: * @return Permission provides a fluent interface
73: public function addRole($role, $parents =
NULL)
75: $this->checkRole($role, FALSE);
77: if (isset($this->roles[$role])) {
81: $roleParents =
array();
83: if ($parents !==
NULL) {
85: $parents =
array($parents);
88: foreach ($parents as $parent) {
89: $this->checkRole($parent);
90: $roleParents[$parent] =
TRUE;
91: $this->roles[$parent]['children'][$role] =
TRUE;
95: $this->roles[$role] =
array(
96: 'parents' =>
$roleParents,
97: 'children' =>
array(),
106: * Returns TRUE if the Role exists in the list.
112: $this->checkRole($role, FALSE);
113: return isset($this->roles[$role]);
119: * Checks whether Role is valid and exists in the list.
122: * @throws InvalidStateException
125: private function checkRole($role, $need =
TRUE)
128: throw new InvalidArgumentException("Role must be a non-empty string.");
130: } elseif ($need &&
!isset($this->roles[$role])) {
138: * Returns an array of an existing Role's parents.
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.
144: * If the Role does not have any parents, then an empty array is returned.
151: $this->checkRole($role);
158: * Returns TRUE if $role inherits from $inherit.
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.
168: * @throws InvalidStateException
173: $this->checkRole($role);
174: $this->checkRole($inherit);
176: $inherits =
isset($this->roles[$role]['parents'][$inherit]);
178: if ($inherits ||
$onlyParents) {
182: foreach ($this->roles[$role]['parents'] as $parent =>
$foo) {
194: * Removes the Role from the list.
197: * @throws InvalidStateException
198: * @return Permission provides a fluent interface
202: $this->checkRole($role);
204: foreach ($this->roles[$role]['children'] as $child =>
$foo)
205: unset($this->roles[$child]['parents'][$role]);
207: foreach ($this->roles[$role]['parents'] as $parent =>
$foo)
208: unset($this->roles[$parent]['children'][$role]);
210: unset($this->roles[$role]);
212: foreach ($this->rules['allResources']['byRole'] as $roleCurrent =>
$rules) {
213: if ($role ===
$roleCurrent) {
214: unset($this->rules['allResources']['byRole'][$roleCurrent]);
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]);
234: * Removes all Roles from the list.
236: * @return Permission provides a fluent interface
240: $this->roles =
array();
242: foreach ($this->rules['allResources']['byRole'] as $roleCurrent =>
$rules)
243: unset($this->rules['allResources']['byRole'][$roleCurrent]);
245: foreach ($this->rules['byResource'] as $resourceCurrent =>
$visitor) {
246: foreach ($visitor['byRole'] as $roleCurrent =>
$rules) {
247: unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
256: /********************* resources ****************d*g**/
261: * Adds a Resource having an identifier unique to the list.
265: * @throws InvalidArgumentException
266: * @throws InvalidStateException
267: * @return Permission provides a fluent interface
271: $this->checkResource($resource, FALSE);
273: if (isset($this->resources[$resource])) {
277: if ($parent !==
NULL) {
278: $this->checkResource($parent);
279: $this->resources[$parent]['children'][$resource] =
TRUE;
282: $this->resources[$resource] =
array(
283: 'parent' =>
$parent,
284: 'children' =>
array()
293: * Returns TRUE if the Resource exists in the list.
299: $this->checkResource($resource, FALSE);
300: return isset($this->resources[$resource]);
306: * Checks whether Resource is valid and exists in the list.
309: * @throws InvalidStateException
312: private function checkResource($resource, $need =
TRUE)
315: throw new InvalidArgumentException("Resource must be a non-empty string.");
317: } elseif ($need &&
!isset($this->resources[$resource])) {
325: * Returns TRUE if $resource inherits from $inherit.
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.
335: * @throws InvalidStateException
340: $this->checkResource($resource);
341: $this->checkResource($inherit);
343: if ($this->resources[$resource]['parent'] ===
NULL) {
347: $parent =
$this->resources[$resource]['parent'];
348: if ($inherit ===
$parent) {
351: } elseif ($onlyParent) {
355: while ($this->resources[$parent]['parent'] !==
NULL) {
356: $parent =
$this->resources[$parent]['parent'];
357: if ($inherit ===
$parent) {
368: * Removes a Resource and all of its children.
371: * @throws InvalidStateException
372: * @return Permission provides a fluent interface
376: $this->checkResource($resource);
378: $parent =
$this->resources[$resource]['parent'];
379: if ($parent !==
NULL) {
380: unset($this->resources[$parent]['children'][$resource]);
383: $removed =
array($resource);
384: foreach ($this->resources[$resource]['children'] as $child =>
$foo) {
386: $removed[] =
$child;
389: foreach ($removed as $resourceRemoved) {
390: foreach ($this->rules['byResource'] as $resourceCurrent =>
$rules) {
391: if ($resourceRemoved ===
$resourceCurrent) {
392: unset($this->rules['byResource'][$resourceCurrent]);
397: unset($this->resources[$resource]);
405: * Removes all Resources.
407: * @return Permission provides a fluent interface
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]);
419: $this->resources =
array();
425: /********************* defining rules ****************d*g**/
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).
433: * If either $roles or $resources is Permission::ALL, then the rule applies to all Roles or all Resources,
434: * respectively. Both may be Permission::ALL in order to work with the default rule of the ACL.
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.
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.
444: * @param string|array|Permission::ALL roles
445: * @param string|array|Permission::ALL resources
446: * @param string|array|Permission::ALL privileges
447: * @param IPermissionAssertion assertion
448: * @return Permission provides a fluent interface
450: public function allow($roles =
self::ALL, $resources =
self::ALL, $privileges =
self::ALL, IPermissionAssertion $assertion =
NULL)
452: $this->setRule(TRUE, self::ALLOW, $roles, $resources, $privileges, $assertion);
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).
462: * If either $roles or $resources is Permission::ALL, then the rule applies to all Roles or all Resources,
463: * respectively. Both may be Permission::ALL in order to work with the default rule of the ACL.
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.
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.
473: * @param string|array|Permission::ALL roles
474: * @param string|array|Permission::ALL resources
475: * @param string|array|Permission::ALL privileges
476: * @param IPermissionAssertion assertion
477: * @return Permission provides a fluent interface
479: public function deny($roles =
self::ALL, $resources =
self::ALL, $privileges =
self::ALL, IPermissionAssertion $assertion =
NULL)
481: $this->setRule(TRUE, self::DENY, $roles, $resources, $privileges, $assertion);
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
492: * @param string|array|Permission::ALL roles
493: * @param string|array|Permission::ALL resources
494: * @param string|array|Permission::ALL privileges
495: * @return Permission provides a fluent interface
497: public function removeAllow($roles =
self::ALL, $resources =
self::ALL, $privileges =
self::ALL)
499: $this->setRule(FALSE, self::ALLOW, $roles, $resources, $privileges);
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
510: * @param string|array|Permission::ALL roles
511: * @param string|array|Permission::ALL resources
512: * @param string|array|Permission::ALL privileges
513: * @return Permission provides a fluent interface
515: public function removeDeny($roles =
self::ALL, $resources =
self::ALL, $privileges =
self::ALL)
517: $this->setRule(FALSE, self::DENY, $roles, $resources, $privileges);
524: * Performs operations on Access Control List rules.
526: * @param bool operation add?
528: * @param string|array|Permission::ALL roles
529: * @param string|array|Permission::ALL resources
530: * @param string|array|Permission::ALL privileges
531: * @param IPermissionAssertion assertion
532: * @throws InvalidStateException
533: * @return Permission provides a fluent interface
535: protected function setRule($toAdd, $type, $roles, $resources, $privileges, IPermissionAssertion $assertion =
NULL)
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);
543: $roles =
array($roles);
546: foreach ($roles as $role) {
547: $this->checkRole($role);
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);
557: $resources =
array($resources);
560: foreach ($resources as $resource) {
561: $this->checkResource($resource);
565: // normalize privileges to array
566: if ($privileges ===
self::ALL) {
567: $privileges =
array();
570: $privileges =
array($privileges);
574: if ($toAdd) { // add to the rules
575: foreach ($resources as $resource) {
576: foreach ($roles as $role) {
577: $rules =
& $this->getRules($resource, $role, TRUE);
579: $rules['allPrivileges']['type'] =
$type;
580: $rules['allPrivileges']['assert'] =
$assertion;
581: if (!isset($rules['byPrivilege'])) {
582: $rules['byPrivilege'] =
array();
585: foreach ($privileges as $privilege) {
586: $rules['byPrivilege'][$privilege]['type'] =
$type;
587: $rules['byPrivilege'][$privilege]['assert'] =
$assertion;
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) {
601: if ($resource ===
self::ALL &&
$role ===
self::ALL) {
602: if ($type ===
$rules['allPrivileges']['type']) {
604: 'allPrivileges' =>
array(
605: 'type' =>
self::DENY,
608: 'byPrivilege' =>
array()
613: if ($type ===
$rules['allPrivileges']['type']) {
614: unset($rules['allPrivileges']);
617: foreach ($privileges as $privilege) {
618: if (isset($rules['byPrivilege'][$privilege]) &&
619: $type ===
$rules['byPrivilege'][$privilege]['type']) {
620: unset($rules['byPrivilege'][$privilege]);
632: /********************* querying the ACL ****************d*g**/
637: * Returns TRUE if and only if the Role has access to the Resource.
639: * If either $role or $resource is Permission::ALL, then the query applies to all Roles or all Resources,
640: * respectively. Both may be Permission::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()).
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.
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.
654: * @param string|Permission::ALL|IRole role
655: * @param string|Permission::ALL|IResource resource
656: * @param string|Permission::ALL privilege
657: * @throws InvalidStateException
660: public function isAllowed($role =
self::ALL, $resource =
self::ALL, $privilege =
self::ALL)
662: $this->queriedRole =
$role;
663: if ($role !==
self::ALL) {
665: $role =
$role->getRoleId();
667: $this->checkRole($role);
670: $this->queriedResource =
$resource;
671: if ($resource !==
self::ALL) {
673: $resource =
$resource->getResourceId();
675: $this->checkResource($resource);
678: if ($privilege ===
self::ALL) {
679: // query on all privileges
681: // depth-first search on $role if it is not 'allRoles' pseudo-parent
682: if ($role !==
NULL &&
NULL !==
($result =
$this->roleDFSAllPrivileges($role, $resource))) {
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;
694: if (NULL !==
($ruleTypeAllPrivileges =
$this->getRuleType($resource, NULL, NULL))) {
695: $result =
self::ALLOW ===
$ruleTypeAllPrivileges;
700: // try next Resource
701: $resource =
$this->resources[$resource]['parent'];
703: } while (TRUE); // loop terminates at 'allResources' pseudo-parent
706: // query on one privilege
708: // depth-first search on $role if it is not 'allRoles' pseudo-parent
709: if ($role !==
NULL &&
NULL !==
($result =
$this->roleDFSOnePrivilege($role, $resource, $privilege))) {
713: // look for rule on 'allRoles' pseudo-parent
714: if (NULL !==
($ruleType =
$this->getRuleType($resource, NULL, $privilege))) {
715: $result =
self::ALLOW ===
$ruleType;
718: } elseif (NULL !==
($ruleTypeAllPrivileges =
$this->getRuleType($resource, NULL, NULL))) {
719: $result =
self::ALLOW ===
$ruleTypeAllPrivileges;
723: // try next Resource
724: $resource =
$this->resources[$resource]['parent'];
726: } while (TRUE); // loop terminates at 'allResources' pseudo-parent
729: $this->queriedRole =
$this->queriedResource =
NULL;
736: * Returns real currently queried Role. Use by {@link IPermissionAssertion::asert()}.
741: return $this->queriedRole;
747: * Returns real currently queried Resource. Use by {@link IPermissionAssertion::asert()}.
752: return $this->queriedResource;
757: /********************* internals ****************d*g**/
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
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.
768: * @param string role
769: * @param string resource
772: private function roleDFSAllPrivileges($role, $resource)
775: 'visited' =>
array(),
776: 'stack' =>
array($role),
780: if (!isset($dfs['visited'][$role])) {
781: if (NULL !==
($result =
$this->roleDFSVisitAllPrivileges($role, $resource, $dfs))) {
793: * Visits a $role in order to look for a rule allowing/denying $role access to all privileges upon $resource.
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.
798: * This method is used by the internal depth-first search algorithm and may modify the DFS data structure.
800: * @param string role
801: * @param string resource
805: private function roleDFSVisitAllPrivileges($role, $resource, &$dfs)
807: if (NULL !==
($rules =
$this->getRules($resource, $role))) {
808: foreach ($rules['byPrivilege'] as $privilege =>
$rule) {
809: if (self::DENY ===
$this->getRuleType($resource, $role, $privilege)) {
813: if (NULL !==
($type =
$this->getRuleType($resource, $role, NULL))) {
814: return self::ALLOW ===
$type;
818: $dfs['visited'][$role] =
TRUE;
819: foreach ($this->roles[$role]['parents'] as $roleParent =>
$foo) {
820: $dfs['stack'][] =
$roleParent;
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
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.
835: * @param string role
836: * @param string resource
837: * @param string privilege
840: private function roleDFSOnePrivilege($role, $resource, $privilege)
843: 'visited' =>
array(),
844: 'stack' =>
array($role),
848: if (!isset($dfs['visited'][$role])) {
849: if (NULL !==
($result =
$this->roleDFSVisitOnePrivilege($role, $resource, $privilege, $dfs))) {
861: * Visits a $role in order to look for a rule allowing/denying $role access to a $privilege upon $resource.
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.
866: * This method is used by the internal depth-first search algorithm and may modify the DFS data structure.
868: * @param string role
869: * @param string resource
870: * @param string privilege
874: private function roleDFSVisitOnePrivilege($role, $resource, $privilege, &$dfs)
876: if (NULL !==
($type =
$this->getRuleType($resource, $role, $privilege))) {
877: return self::ALLOW ===
$type;
880: if (NULL !==
($type =
$this->getRuleType($resource, $role, NULL))) {
881: return self::ALLOW ===
$type;
884: $dfs['visited'][$role] =
TRUE;
885: foreach ($this->roles[$role]['parents'] as $roleParent =>
$foo)
886: $dfs['stack'][] =
$roleParent;
894: * Returns the rule type associated with the specified Resource, Role, and privilege.
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.
901: * If $resource or $role is Permission::ALL, then this means that the rule must apply to
902: * all Resources or Roles, respectively.
904: * If $privilege is Permission::ALL, then the rule must apply to all privileges.
906: * If all three parameters are Permission::ALL, then the default ACL rule type is returned,
907: * based on whether its assertion method passes.
909: * @param string|Permission::ALL role
910: * @param string|Permission::ALL resource
911: * @param string|Permission::ALL privilege
914: private function getRuleType($resource, $role, $privilege)
916: // get the rules for the $resource and $role
917: if (NULL ===
($rules =
$this->getRules($resource, $role))) {
921: // follow $privilege
922: if ($privilege ===
self::ALL) {
923: if (isset($rules['allPrivileges'])) {
924: $rule =
$rules['allPrivileges'];
928: } elseif (!isset($rules['byPrivilege'][$privilege])) {
932: $rule =
$rules['byPrivilege'][$privilege];
935: // check assertion if necessary
936: if ($rule['assert'] ===
NULL ||
$rule['assert']->assert($this, $role, $resource, $privilege)) {
937: return $rule['type'];
939: } elseif ($resource !==
self::ALL ||
$role !==
self::ALL ||
$privilege !==
self::ALL) {
942: } elseif (self::ALLOW ===
$rule['type']) {
953: * Returns the rules associated with a Resource and a Role, or NULL if no such rules exist.
955: * If either $resource or $role is Permission::ALL, this means that the rules returned are for all Resources or all Roles,
956: * respectively. Both can be Permission::ALL to return the default rule set for all Resources and all Roles.
958: * If the $create parameter is TRUE, then a rule set is first created and then returned to the caller.
960: * @param string|Permission::ALL resource
961: * @param string|Permission::ALL role
962: * @param boolean create
963: * @return array|NULL
965: private function & getRules($resource, $role, $create =
FALSE)
968: if ($resource ===
self::ALL) {
969: $visitor =
& $this->rules['allResources'];
971: if (!isset($this->rules['byResource'][$resource])) {
976: $this->rules['byResource'][$resource] =
array();
978: $visitor =
& $this->rules['byResource'][$resource];
983: if ($role ===
self::ALL) {
984: if (!isset($visitor['allRoles'])) {
989: $visitor['allRoles']['byPrivilege'] =
array();
991: return $visitor['allRoles'];
994: if (!isset($visitor['byRole'][$role])) {
999: $visitor['byRole'][$role]['byPrivilege'] =
array();
1002: return $visitor['byRole'][$role];