1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Security;
9:
10: use Nette;
11:
12:
13: 14: 15: 16: 17: 18: 19:
20: class Permission implements IAuthorizator
21: {
22: use Nette\SmartObject;
23:
24:
25: private $roles = [];
26:
27:
28: private $resources = [];
29:
30:
31: private $rules = [
32: 'allResources' => [
33: 'allRoles' => [
34: 'allPrivileges' => [
35: 'type' => self::DENY,
36: 'assert' => null,
37: ],
38: 'byPrivilege' => [],
39: ],
40: 'byRole' => [],
41: ],
42: 'byResource' => [],
43: ];
44:
45:
46: private $queriedRole;
47: private $queriedResource;
48:
49:
50:
51:
52:
53: 54: 55: 56: 57: 58: 59: 60: 61:
62: public function addRole($role, $parents = null)
63: {
64: $this->checkRole($role, false);
65: if (isset($this->roles[$role])) {
66: throw new Nette\InvalidStateException("Role '$role' already exists in the list.");
67: }
68:
69: $roleParents = [];
70:
71: if ($parents !== null) {
72: if (!is_array($parents)) {
73: $parents = [$parents];
74: }
75:
76: foreach ($parents as $parent) {
77: $this->checkRole($parent);
78: $roleParents[$parent] = true;
79: $this->roles[$parent]['children'][$role] = true;
80: }
81: }
82:
83: $this->roles[$role] = [
84: 'parents' => $roleParents,
85: 'children' => [],
86: ];
87:
88: return $this;
89: }
90:
91:
92: 93: 94: 95: 96:
97: public function hasRole($role)
98: {
99: $this->checkRole($role, false);
100: return isset($this->roles[$role]);
101: }
102:
103:
104: 105: 106: 107: 108: 109: 110:
111: private function checkRole($role, $throw = true)
112: {
113: if (!is_string($role) || $role === '') {
114: throw new Nette\InvalidArgumentException('Role must be a non-empty string.');
115:
116: } elseif ($throw && !isset($this->roles[$role])) {
117: throw new Nette\InvalidStateException("Role '$role' does not exist.");
118: }
119: }
120:
121:
122: 123: 124: 125:
126: public function getRoles()
127: {
128: return array_keys($this->roles);
129: }
130:
131:
132: 133: 134: 135: 136:
137: public function getRoleParents($role)
138: {
139: $this->checkRole($role);
140: return array_keys($this->roles[$role]['parents']);
141: }
142:
143:
144: 145: 146: 147: 148: 149: 150: 151: 152:
153: public function roleInheritsFrom($role, $inherit, $onlyParents = false)
154: {
155: $this->checkRole($role);
156: $this->checkRole($inherit);
157:
158: $inherits = isset($this->roles[$role]['parents'][$inherit]);
159:
160: if ($inherits || $onlyParents) {
161: return $inherits;
162: }
163:
164: foreach ($this->roles[$role]['parents'] as $parent => $foo) {
165: if ($this->roleInheritsFrom($parent, $inherit)) {
166: return true;
167: }
168: }
169:
170: return false;
171: }
172:
173:
174: 175: 176: 177: 178: 179: 180:
181: public function removeRole($role)
182: {
183: $this->checkRole($role);
184:
185: foreach ($this->roles[$role]['children'] as $child => $foo) {
186: unset($this->roles[$child]['parents'][$role]);
187: }
188:
189: foreach ($this->roles[$role]['parents'] as $parent => $foo) {
190: unset($this->roles[$parent]['children'][$role]);
191: }
192:
193: unset($this->roles[$role]);
194:
195: foreach ($this->rules['allResources']['byRole'] as $roleCurrent => $rules) {
196: if ($role === $roleCurrent) {
197: unset($this->rules['allResources']['byRole'][$roleCurrent]);
198: }
199: }
200:
201: foreach ($this->rules['byResource'] as $resourceCurrent => $visitor) {
202: if (isset($visitor['byRole'])) {
203: foreach ($visitor['byRole'] as $roleCurrent => $rules) {
204: if ($role === $roleCurrent) {
205: unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
206: }
207: }
208: }
209: }
210:
211: return $this;
212: }
213:
214:
215: 216: 217: 218: 219:
220: public function removeAllRoles()
221: {
222: $this->roles = [];
223:
224: foreach ($this->rules['allResources']['byRole'] as $roleCurrent => $rules) {
225: unset($this->rules['allResources']['byRole'][$roleCurrent]);
226: }
227:
228: foreach ($this->rules['byResource'] as $resourceCurrent => $visitor) {
229: foreach ($visitor['byRole'] as $roleCurrent => $rules) {
230: unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
231: }
232: }
233:
234: return $this;
235: }
236:
237:
238:
239:
240:
241: 242: 243: 244: 245: 246: 247: 248: 249:
250: public function addResource($resource, $parent = null)
251: {
252: $this->checkResource($resource, false);
253:
254: if (isset($this->resources[$resource])) {
255: throw new Nette\InvalidStateException("Resource '$resource' already exists in the list.");
256: }
257:
258: if ($parent !== null) {
259: $this->checkResource($parent);
260: $this->resources[$parent]['children'][$resource] = true;
261: }
262:
263: $this->resources[$resource] = [
264: 'parent' => $parent,
265: 'children' => [],
266: ];
267:
268: return $this;
269: }
270:
271:
272: 273: 274: 275: 276:
277: public function hasResource($resource)
278: {
279: $this->checkResource($resource, false);
280: return isset($this->resources[$resource]);
281: }
282:
283:
284: 285: 286: 287: 288: 289: 290:
291: private function checkResource($resource, $throw = true)
292: {
293: if (!is_string($resource) || $resource === '') {
294: throw new Nette\InvalidArgumentException('Resource must be a non-empty string.');
295:
296: } elseif ($throw && !isset($this->resources[$resource])) {
297: throw new Nette\InvalidStateException("Resource '$resource' does not exist.");
298: }
299: }
300:
301:
302: 303: 304: 305:
306: public function getResources()
307: {
308: return array_keys($this->resources);
309: }
310:
311:
312: 313: 314: 315: 316: 317: 318: 319: 320: 321:
322: public function resourceInheritsFrom($resource, $inherit, $onlyParent = false)
323: {
324: $this->checkResource($resource);
325: $this->checkResource($inherit);
326:
327: if ($this->resources[$resource]['parent'] === null) {
328: return false;
329: }
330:
331: $parent = $this->resources[$resource]['parent'];
332: if ($inherit === $parent) {
333: return true;
334:
335: } elseif ($onlyParent) {
336: return false;
337: }
338:
339: while ($this->resources[$parent]['parent'] !== null) {
340: $parent = $this->resources[$parent]['parent'];
341: if ($inherit === $parent) {
342: return true;
343: }
344: }
345:
346: return false;
347: }
348:
349:
350: 351: 352: 353: 354: 355: 356:
357: public function removeResource($resource)
358: {
359: $this->checkResource($resource);
360:
361: $parent = $this->resources[$resource]['parent'];
362: if ($parent !== null) {
363: unset($this->resources[$parent]['children'][$resource]);
364: }
365:
366: $removed = [$resource];
367: foreach ($this->resources[$resource]['children'] as $child => $foo) {
368: $this->removeResource($child);
369: $removed[] = $child;
370: }
371:
372: foreach ($removed as $resourceRemoved) {
373: foreach ($this->rules['byResource'] as $resourceCurrent => $rules) {
374: if ($resourceRemoved === $resourceCurrent) {
375: unset($this->rules['byResource'][$resourceCurrent]);
376: }
377: }
378: }
379:
380: unset($this->resources[$resource]);
381: return $this;
382: }
383:
384:
385: 386: 387: 388:
389: public function removeAllResources()
390: {
391: foreach ($this->resources as $resource => $foo) {
392: foreach ($this->rules['byResource'] as $resourceCurrent => $rules) {
393: if ($resource === $resourceCurrent) {
394: unset($this->rules['byResource'][$resourceCurrent]);
395: }
396: }
397: }
398:
399: $this->resources = [];
400: return $this;
401: }
402:
403:
404:
405:
406:
407: 408: 409: 410: 411: 412: 413: 414: 415: 416:
417: public function allow($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL, $assertion = null)
418: {
419: $this->setRule(true, self::ALLOW, $roles, $resources, $privileges, $assertion);
420: return $this;
421: }
422:
423:
424: 425: 426: 427: 428: 429: 430: 431: 432: 433:
434: public function deny($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL, $assertion = null)
435: {
436: $this->setRule(true, self::DENY, $roles, $resources, $privileges, $assertion);
437: return $this;
438: }
439:
440:
441: 442: 443: 444: 445: 446: 447: 448:
449: public function removeAllow($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL)
450: {
451: $this->setRule(false, self::ALLOW, $roles, $resources, $privileges);
452: return $this;
453: }
454:
455:
456: 457: 458: 459: 460: 461: 462: 463:
464: public function removeDeny($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL)
465: {
466: $this->setRule(false, self::DENY, $roles, $resources, $privileges);
467: return $this;
468: }
469:
470:
471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481:
482: protected function setRule($toAdd, $type, $roles, $resources, $privileges, $assertion = null)
483: {
484:
485: if ($roles === self::ALL) {
486: $roles = [self::ALL];
487:
488: } else {
489: if (!is_array($roles)) {
490: $roles = [$roles];
491: }
492:
493: foreach ($roles as $role) {
494: $this->checkRole($role);
495: }
496: }
497:
498:
499: if ($resources === self::ALL) {
500: $resources = [self::ALL];
501:
502: } else {
503: if (!is_array($resources)) {
504: $resources = [$resources];
505: }
506:
507: foreach ($resources as $resource) {
508: $this->checkResource($resource);
509: }
510: }
511:
512:
513: if ($privileges === self::ALL) {
514: $privileges = [];
515:
516: } elseif (!is_array($privileges)) {
517: $privileges = [$privileges];
518: }
519:
520: if ($toAdd) {
521: foreach ($resources as $resource) {
522: foreach ($roles as $role) {
523: $rules = &$this->getRules($resource, $role, true);
524: if (count($privileges) === 0) {
525: $rules['allPrivileges']['type'] = $type;
526: $rules['allPrivileges']['assert'] = $assertion;
527: if (!isset($rules['byPrivilege'])) {
528: $rules['byPrivilege'] = [];
529: }
530: } else {
531: foreach ($privileges as $privilege) {
532: $rules['byPrivilege'][$privilege]['type'] = $type;
533: $rules['byPrivilege'][$privilege]['assert'] = $assertion;
534: }
535: }
536: }
537: }
538:
539: } else {
540: foreach ($resources as $resource) {
541: foreach ($roles as $role) {
542: $rules = &$this->getRules($resource, $role);
543: if ($rules === null) {
544: continue;
545: }
546: if (count($privileges) === 0) {
547: if ($resource === self::ALL && $role === self::ALL) {
548: if ($type === $rules['allPrivileges']['type']) {
549: $rules = [
550: 'allPrivileges' => [
551: 'type' => self::DENY,
552: 'assert' => null,
553: ],
554: 'byPrivilege' => [],
555: ];
556: }
557: continue;
558: }
559: if ($type === $rules['allPrivileges']['type']) {
560: unset($rules['allPrivileges']);
561: }
562: } else {
563: foreach ($privileges as $privilege) {
564: if (isset($rules['byPrivilege'][$privilege]) &&
565: $type === $rules['byPrivilege'][$privilege]['type']
566: ) {
567: unset($rules['byPrivilege'][$privilege]);
568: }
569: }
570: }
571: }
572: }
573: }
574: return $this;
575: }
576:
577:
578:
579:
580:
581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594:
595: public function isAllowed($role = self::ALL, $resource = self::ALL, $privilege = self::ALL)
596: {
597: $this->queriedRole = $role;
598: if ($role !== self::ALL) {
599: if ($role instanceof IRole) {
600: $role = $role->getRoleId();
601: }
602: $this->checkRole($role);
603: }
604:
605: $this->queriedResource = $resource;
606: if ($resource !== self::ALL) {
607: if ($resource instanceof IResource) {
608: $resource = $resource->getResourceId();
609: }
610: $this->checkResource($resource);
611: }
612:
613: do {
614:
615: if ($role !== null && ($result = $this->searchRolePrivileges($privilege === self::ALL, $role, $resource, $privilege)) !== null) {
616: break;
617: }
618:
619: if ($privilege === self::ALL) {
620: if ($rules = $this->getRules($resource, self::ALL)) {
621: foreach ($rules['byPrivilege'] as $privilege => $rule) {
622: if (($result = $this->getRuleType($resource, null, $privilege)) === self::DENY) {
623: break 2;
624: }
625: }
626: if (($result = $this->getRuleType($resource, null, null)) !== null) {
627: break;
628: }
629: }
630: } else {
631: if (($result = $this->getRuleType($resource, null, $privilege)) !== null) {
632: break;
633:
634: } elseif (($result = $this->getRuleType($resource, null, null)) !== null) {
635: break;
636: }
637: }
638:
639: $resource = $this->resources[$resource]['parent'];
640: } while (true);
641:
642: $this->queriedRole = $this->queriedResource = null;
643: return $result;
644: }
645:
646:
647: 648: 649: 650:
651: public function getQueriedRole()
652: {
653: return $this->queriedRole;
654: }
655:
656:
657: 658: 659: 660:
661: public function getQueriedResource()
662: {
663: return $this->queriedResource;
664: }
665:
666:
667:
668:
669:
670: 671: 672: 673: 674: 675: 676: 677: 678:
679: private function searchRolePrivileges($all, $role, $resource, $privilege)
680: {
681: $dfs = [
682: 'visited' => [],
683: 'stack' => [$role],
684: ];
685:
686: while (($role = array_pop($dfs['stack'])) !== null) {
687: if (isset($dfs['visited'][$role])) {
688: continue;
689: }
690: if ($all) {
691: if ($rules = $this->getRules($resource, $role)) {
692: foreach ($rules['byPrivilege'] as $privilege2 => $rule) {
693: if ($this->getRuleType($resource, $role, $privilege2) === self::DENY) {
694: return self::DENY;
695: }
696: }
697: if (($type = $this->getRuleType($resource, $role, null)) !== null) {
698: return $type;
699: }
700: }
701: } else {
702: if (($type = $this->getRuleType($resource, $role, $privilege)) !== null) {
703: return $type;
704:
705: } elseif (($type = $this->getRuleType($resource, $role, null)) !== null) {
706: return $type;
707: }
708: }
709:
710: $dfs['visited'][$role] = true;
711: foreach ($this->roles[$role]['parents'] as $roleParent => $foo) {
712: $dfs['stack'][] = $roleParent;
713: }
714: }
715: return null;
716: }
717:
718:
719: 720: 721: 722: 723: 724: 725:
726: private function getRuleType($resource, $role, $privilege)
727: {
728: if (!$rules = $this->getRules($resource, $role)) {
729: return null;
730: }
731:
732: if ($privilege === self::ALL) {
733: if (isset($rules['allPrivileges'])) {
734: $rule = $rules['allPrivileges'];
735: } else {
736: return null;
737: }
738: } elseif (!isset($rules['byPrivilege'][$privilege])) {
739: return null;
740:
741: } else {
742: $rule = $rules['byPrivilege'][$privilege];
743: }
744:
745: if ($rule['assert'] === null || Nette\Utils\Callback::invoke($rule['assert'], $this, $role, $resource, $privilege)) {
746: return $rule['type'];
747:
748: } elseif ($resource !== self::ALL || $role !== self::ALL || $privilege !== self::ALL) {
749: return null;
750:
751: } elseif ($rule['type'] === self::ALLOW) {
752: return self::DENY;
753:
754: } else {
755: return self::ALLOW;
756: }
757: }
758:
759:
760: 761: 762: 763: 764: 765: 766: 767:
768: private function &getRules($resource, $role, $create = false)
769: {
770: $null = null;
771: if ($resource === self::ALL) {
772: $visitor = &$this->rules['allResources'];
773: } else {
774: if (!isset($this->rules['byResource'][$resource])) {
775: if (!$create) {
776: return $null;
777: }
778: $this->rules['byResource'][$resource] = [];
779: }
780: $visitor = &$this->rules['byResource'][$resource];
781: }
782:
783: if ($role === self::ALL) {
784: if (!isset($visitor['allRoles'])) {
785: if (!$create) {
786: return $null;
787: }
788: $visitor['allRoles']['byPrivilege'] = [];
789: }
790: return $visitor['allRoles'];
791: }
792:
793: if (!isset($visitor['byRole'][$role])) {
794: if (!$create) {
795: return $null;
796: }
797: $visitor['byRole'][$role]['byPrivilege'] = [];
798: }
799:
800: return $visitor['byRole'][$role];
801: }
802: }
803: