Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
256 / 256
100.00% covered (success)
100.00%
37 / 37
CRAP
100.00% covered (success)
100.00%
1 / 1
Acl
100.00% covered (success)
100.00%
256 / 256
100.00% covered (success)
100.00%
37 / 37
173
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 getRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addRole
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 addRoles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getResource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasResource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addResource
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getResources
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addResources
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setStrict
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isStrict
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMultiStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isMultiStrict
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allow
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 removeAllowRule
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
12
 deny
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 removeDenyRule
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
12
 isAllowed
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
21
 isAllowedMulti
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
 isAllowedMultiStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isDenied
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
16
 isDeniedMulti
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 isDeniedMultiStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createAssertion
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 deleteAssertion
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasAssertionKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getAssertionKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 addPolicy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 hasPolicies
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 evaluatePolicies
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
19
 evaluatePolicy
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 verifyRole
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 verifyResource
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 generateAssertionKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 traverseChildren
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Acl;
15
16use Pop\Acl\Assertion\AssertionInterface;
17use InvalidArgumentException;
18
19/**
20 * ACL class
21 *
22 * @category   Pop
23 * @package    Pop\Acl
24 * @author     Nick Sagona, III <dev@nolainteractive.com>
25 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
26 * @license    http://www.popphp.org/license     New BSD License
27 * @version    4.0.0
28 */
29class Acl
30{
31
32    /**
33     * Array of roles
34     * @var array
35     */
36    protected array $roles = [];
37
38    /**
39     * Array of resources
40     * @var array
41     */
42    protected array $resources = [];
43
44    /**
45     * Array of allowed roles, resources and permissions
46     * @var array
47     */
48    protected array $allowed = [];
49
50    /**
51     * Array of denied roles, resources and permissions
52     * @var array
53     */
54    protected array $denied = [];
55
56    /**
57     * Array of assertions
58     * @var array
59     */
60    protected array $assertions = [
61        'allowed' => [],
62        'denied'  => []
63    ];
64
65    /**
66     * Array of policies
67     * @var array
68     */
69    protected array $policies = [];
70
71    /**
72     * Strict flag
73     * @var bool
74     */
75    protected bool $strict = false;
76
77    /**
78     * Multi strict flag
79     * @var bool
80     */
81    protected bool $multiStrict = false;
82
83    /**
84     * Constructor
85     *
86     * Instantiate the ACL object
87     */
88    public function __construct()
89    {
90        $args = func_get_args();
91
92        foreach ($args as $arg) {
93            if (is_array($arg)) {
94                foreach ($arg as $a) {
95                    if ($a instanceof AclRole) {
96                        $this->addRole($a);
97                    } else if ($a instanceof AclResource) {
98                        $this->addResource($a);
99                    }
100                }
101            } else if ($arg instanceof AclRole) {
102                $this->addRole($arg);
103            } else if ($arg instanceof AclResource) {
104                $this->addResource($arg);
105            }
106        }
107    }
108
109    /**
110     * Get a role
111     *
112     * @param  string $role
113     * @return AclRole|null
114     */
115    public function getRole(string $role): AclRole|null
116    {
117        return $this->roles[$role] ?? null;
118    }
119
120    /**
121     * Get roles
122     *
123     * @return array
124     */
125    public function getRoles(): array
126    {
127        return $this->roles;
128    }
129
130    /**
131     * See if a role has been added
132     *
133     * @param  string $role
134     * @return bool
135     */
136    public function hasRole(string $role): bool
137    {
138        return (isset($this->roles[$role]));
139    }
140
141    /**
142     * Add a role
143     *
144     * @param  AclRole $role
145     * @return Acl
146     */
147    public function addRole(AclRole $role): Acl
148    {
149        if (!isset($this->roles[$role->getName()])) {
150            $this->roles[$role->getName()] = $role;
151
152            // Traverse up if role has parents
153            while ($role->hasParent()) {
154                $role = $role->getParent();
155                $this->roles[$role->getName()] = $role;
156            }
157
158            // Traverse down if the role has children
159            if ($role->hasChildren()) {
160                $this->traverseChildren($role->getChildren());
161            }
162        }
163        return $this;
164    }
165
166    /**
167     * Add roles
168     *
169     * @param  array $roles
170     * @return Acl
171     */
172    public function addRoles(array $roles): Acl
173    {
174        foreach ($roles as $role) {
175            $this->addRole($role);
176        }
177
178        return $this;
179    }
180
181    /**
182     * Get a resource
183     *
184     * @param  string $resource
185     * @return AclResource|null
186     */
187    public function getResource(string $resource): AclResource|null
188    {
189        return $this->resources[$resource] ?? null;
190    }
191
192    /**
193     * See if a resource has been added
194     *
195     * @param  string $resource
196     * @return bool
197     */
198    public function hasResource(string $resource): bool
199    {
200        return (isset($this->resources[$resource]));
201    }
202
203    /**
204     * Add a resource
205     *
206     * @param AclResource $resource
207     * @return Acl
208     */
209    public function addResource(AclResource $resource): Acl
210    {
211        $this->resources[$resource->getName()] = $resource;
212        return $this;
213    }
214
215    /**
216     * Get resources
217     *
218     * @return array
219     */
220    public function getResources(): array
221    {
222        return $this->resources;
223    }
224
225    /**
226     * Add resources
227     *
228     * @param  array $resources
229     * @return Acl
230     */
231    public function addResources(array $resources): Acl
232    {
233        foreach ($resources as $resource) {
234            $this->addResource($resource);
235        }
236
237        return $this;
238    }
239
240    /**
241     * Set strict
242     *
243     * @param  bool      $strict
244     * @param  bool|null $multiStrict
245     * @return Acl
246     */
247    public function setStrict(bool $strict = true, bool|null $multiStrict = null): Acl
248    {
249        $this->strict = $strict;
250        if ($multiStrict !== null) {
251            $this->multiStrict = $multiStrict;
252        }
253        return $this;
254    }
255
256    /**
257     * See if ACL object is set to strict
258     *
259     * @return bool
260     */
261    public function isStrict(): bool
262    {
263        return $this->strict;
264    }
265
266    /**
267     * Set multi strict
268     *
269     * @param  bool $multiStrict
270     * @return Acl
271     */
272    public function setMultiStrict(bool $multiStrict = true): Acl
273    {
274        $this->multiStrict = $multiStrict;
275        return $this;
276    }
277
278    /**
279     * See if ACL object is set to multi strict
280     *
281     * @return bool
282     */
283    public function isMultiStrict(): bool
284    {
285        return $this->multiStrict;
286    }
287
288    /**
289     * Allow a role permission to a resource or resources
290     *
291     * @param  mixed               $role
292     * @param  mixed               $resource
293     * @param  mixed               $permission
294     * @param  ?AssertionInterface $assertion
295     * @throws Exception
296     * @return Acl
297     */
298    public function allow(mixed $role, mixed $resource = null, mixed $permission = null, ?AssertionInterface $assertion = null): Acl
299    {
300        if ($this->verifyRole($role)) {
301            $role = $this->roles[(string)$role];
302
303            if (!isset($this->allowed[(string)$role])) {
304                $this->allowed[(string)$role] = [];
305            }
306
307            if (($resource !== null) && ($this->verifyResource($resource))) {
308                $resource = $this->resources[(string)$resource];
309
310                if (!isset($this->allowed[(string)$role][(string)$resource])) {
311                    $this->allowed[(string)$role][(string)$resource] = [];
312                }
313                if ($permission !== null) {
314                    $this->allowed[(string)$role][(string)$resource][] = $permission;
315                }
316            }
317
318            // If an assertion has been passed
319            if ($assertion !== null) {
320                $this->createAssertion($assertion, 'allowed', $role, $resource, $permission);
321            }
322        }
323
324        return $this;
325    }
326
327    /**
328     * Remove an allow rule
329     *
330     * @param  mixed $role
331     * @param  mixed $resource
332     * @param  mixed $permission
333     * @throws Exception
334     * @return Acl
335     */
336    public function removeAllowRule(mixed $role, mixed $resource = null, mixed $permission = null): Acl
337    {
338        if (($this->verifyRole($role)) && isset($this->allowed[(string)$role])) {
339            // If only role passed
340            if (($resource === null) && ($permission === null)) {
341                unset($this->allowed[(string)$role]);
342            // If role & resource passed
343            } else if (($resource !== null) && ($permission === null) && ($this->verifyResource($resource)) &&
344                isset($this->allowed[(string)$role][(string)$resource])) {
345                unset($this->allowed[(string)$role][(string)$resource]);
346            // If role, resource & permission passed
347            } else {
348                if (($this->verifyResource($resource)) && isset($this->allowed[(string)$role][(string)$resource]) &&
349                    in_array($permission, $this->allowed[(string)$role][(string)$resource])) {
350                    $key = array_search($permission, $this->allowed[(string)$role][(string)$resource]);
351                    unset($this->allowed[(string)$role][(string)$resource][$key]);
352                }
353            }
354
355            $this->deleteAssertion('allowed', $role, $resource, $permission);
356        }
357
358        return $this;
359    }
360
361    /**
362     * Deny a role permission to a resource or resources
363     *
364     * @param  mixed               $role
365     * @param  mixed               $resource
366     * @param  mixed               $permission
367     * @param  ?AssertionInterface $assertion
368     * @throws Exception
369     * @return Acl
370     */
371    public function deny(mixed $role, mixed $resource = null, mixed $permission = null, ?AssertionInterface $assertion = null): Acl
372    {
373        if ($this->verifyRole($role)) {
374            $role = $this->roles[(string)$role];
375
376            if (!isset($this->denied[(string)$role])) {
377                $this->denied[(string)$role] = [];
378            }
379
380            if (($resource !== null) && ($this->verifyResource($resource))) {
381                $resource = $this->resources[(string)$resource];
382
383                if (!isset($this->denied[(string)$role][(string)$resource])) {
384                    $this->denied[(string)$role][(string)$resource] = [];
385                }
386                if ($permission !== null) {
387                    $this->denied[(string)$role][(string)$resource][] = $permission;
388                }
389            }
390
391            // If an assertion has been passed
392            if ($assertion !== null) {
393                $this->createAssertion($assertion, 'denied', $role, $resource, $permission);
394            }
395        }
396
397        return $this;
398    }
399
400    /**
401     * Remove a deny rule
402     *
403     * @param  mixed $role
404     * @param  mixed $resource
405     * @param  mixed $permission
406     * @throws Exception
407     * @return Acl
408     */
409    public function removeDenyRule(mixed $role, mixed $resource = null, mixed $permission = null): Acl
410    {
411        if (($this->verifyRole($role)) && isset($this->denied[(string)$role])) {
412            // If only role passed
413            if (($resource === null) && ($permission === null)) {
414                unset($this->denied[(string)$role]);
415            // If role & resource passed
416            } else if (($resource !== null) && ($permission === null) && ($this->verifyResource($resource)) &&
417                isset($this->denied[(string)$role][(string)$resource])) {
418                unset($this->denied[(string)$role][(string)$resource]);
419            // If role, resource & permission passed
420            } else {
421                if (($this->verifyResource($resource)) && isset($this->denied[(string)$role][(string)$resource]) &&
422                    in_array($permission, $this->denied[(string)$role][(string)$resource])) {
423                    $key = array_search($permission, $this->denied[(string)$role][(string)$resource]);
424                    unset($this->denied[(string)$role][(string)$resource][$key]);
425                }
426            }
427
428            $this->deleteAssertion('denied', $role, $resource, $permission);
429        }
430
431        return $this;
432    }
433
434    /**
435     * Determine if the role is allowed
436     *
437     * @param  mixed $role
438     * @param  mixed $resource
439     * @param  mixed $permission
440     * @throws Exception
441     * @return bool
442     */
443    public function isAllowed(mixed $role, mixed $resource = null, mixed $permission = null): bool
444    {
445        $result = false;
446
447        if ($this->verifyRole($role)) {
448            if ($resource !== null) {
449                $this->verifyResource($resource);
450            }
451
452            // If is not denied
453            if (!$this->isDenied($role, $resource, $permission)) {
454                // If not strict, pass
455                if ((!$this->strict) && (!$this->multiStrict)) {
456                    $result = true;
457                // If strict, check for explicit allow rule
458                } else {
459                    $roleToCheck = $this->roles[(string)$role];
460                    while ($roleToCheck !== null) {
461                        if (isset($this->allowed[(string)$roleToCheck])) {
462                            // No explicit resources or permissions
463                            if (count($this->allowed[(string)$roleToCheck]) == 0) {
464                                $result = true;
465                                // Resource set, but no explicit permissions
466                            } else if (($resource !== null) && isset($this->allowed[(string)$roleToCheck][(string)$resource]) &&
467                                (count($this->allowed[(string)$roleToCheck][(string)$resource]) == 0)) {
468                                $result = true;
469                                // Else, has resource and permissions set
470                            } else if (($resource !== null) && ($permission !== null) &&
471                                isset($this->allowed[(string)$roleToCheck][(string)$resource]) &&
472                                (count($this->allowed[(string)$roleToCheck][(string)$resource]) > 0)) {
473                                $permissions        = (!is_array($permission)) ? [$permission] : $permission;
474                                $allowedPermissions = array_intersect(
475                                    $permissions, $this->allowed[(string)$roleToCheck][(string)$resource]
476                                );
477
478                                $result = (count($allowedPermissions) == count($permissions));
479                            }
480                        }
481                        $roleToCheck = $roleToCheck->getParent();
482                    }
483                }
484            }
485        }
486
487        // Check for assertions
488        if (($result) && ($this->hasAssertionKey('allowed', $role, $resource, $permission))) {
489            $assertionKey      = $this->getAssertionKey('allowed', $role, $resource, $permission);
490            $assertionRole     = $this->roles[(string)$role];
491            $assertionResource = ($resource !== null) ?  $this->resources[(string)$resource] : null;
492            $result            =
493                $this->assertions['allowed'][$assertionKey]->assert($this, $assertionRole, $assertionResource, $permission);
494        }
495
496        // Check for policies
497        if ($this->hasPolicies()) {
498            $result = $this->evaluatePolicies($role, $resource, $permission);
499        }
500
501        return $result;
502    }
503
504    /**
505     * Determine if multiple roles are allowed
506     *
507     * @param  array $roles
508     * @param  mixed $resource
509     * @param  mixed $permission
510     * @throws Exception
511     * @return bool
512     */
513    public function isAllowedMulti(array $roles, mixed $resource = null, mixed $permission = null): bool
514    {
515        // If strict, all roles must be allowed
516        if ($this->multiStrict) {
517            $result = true;
518            foreach ($roles as $role) {
519                if (!$this->isAllowed($role, $resource, $permission)) {
520                    $result = false;
521                    break;
522                }
523            }
524        // Else, evaluate loosely
525        } else {
526            // Check for any explicitly set denied rules for any of the roles
527            foreach ($roles as $role) {
528                if ($this->isDenied($role, $resource, $permission)) {
529                    return false;
530                }
531            }
532
533            // Else, evaluate if any of the roles are allowed
534            $result = false;
535            foreach ($roles as $role) {
536                if ($this->isAllowed($role, $resource, $permission)) {
537                    $result = true;
538                    break;
539                }
540            }
541        }
542
543        return $result;
544    }
545
546    /**
547     * Determine if multiple roles are allowed using the strict parameter
548     * All of the roles must be allowed to return true, otherwise it will return false
549     *
550     * @param  array $roles
551     * @param  mixed $resource
552     * @param  mixed $permission
553     * @throws Exception
554     * @return bool
555     */
556    public function isAllowedMultiStrict(array $roles, mixed $resource = null, mixed $permission = null): bool
557    {
558        $this->multiStrict = true;
559        return $this->isAllowedMulti($roles, $resource, $permission);
560    }
561
562    /**
563     * Determine if a role is denied
564     *
565     * @param  mixed $role
566     * @param  mixed $resource
567     * @param  mixed $permission
568     * @throws Exception
569     * @return bool
570     */
571    public function isDenied(mixed $role, mixed $resource = null, mixed $permission = null): bool
572    {
573        $result = false;
574
575        if ($this->verifyRole($role)) {
576            if ($resource !== null) {
577                $this->verifyResource($resource);
578            }
579
580            // Check if the user, resource and/or permission is denied
581            $roleToCheck = $this->roles[(string)$role];
582            while ($roleToCheck !== null) {
583                if (isset($this->denied[(string)$roleToCheck])) {
584                    if (count($this->denied[(string)$roleToCheck]) > 0) {
585                        if (($resource !== null) && array_key_exists((string)$resource, $this->denied[(string)$roleToCheck])) {
586                            if (count($this->denied[(string)$roleToCheck][(string)$resource]) > 0) {
587                                if ($permission !== null) {
588                                    $permissions = (!is_array($permission)) ? [$permission] : $permission;
589                                    foreach ($permissions as $p) {
590                                        if (in_array($p, $this->denied[(string)$roleToCheck][(string)$resource])) {
591                                            $result = true;
592                                        }
593                                    }
594                                }
595                            } else {
596                                $result = true;
597                            }
598                        }
599                    } else {
600                        $result = true;
601                    }
602                }
603                $roleToCheck = $roleToCheck->getParent();
604            }
605        }
606
607        // Check for assertions
608        if ($this->hasAssertionKey('denied', $role, $resource, $permission)) {
609            $assertionKey      = $this->getAssertionKey('denied', $role, $resource, $permission);
610            $assertionRole     = $this->roles[(string)$role];
611            $assertionResource = ($resource !== null) ?  $this->resources[(string)$resource] : null;
612            $result            =
613                $this->assertions['denied'][$assertionKey]->assert($this, $assertionRole, $assertionResource, $permission);
614        }
615
616        // Check for policies
617        if ($this->hasPolicies()) {
618            $result = (!$this->evaluatePolicies($role, $resource, $permission));
619        }
620
621        return $result;
622    }
623
624    /**
625     * Determine if multiple roles are denied
626     *
627     * @param  array $roles
628     * @param  mixed $resource
629     * @param  mixed $permission
630     * @throws Exception
631     * @return bool
632     */
633    public function isDeniedMulti(array $roles, mixed $resource = null, mixed $permission = null): bool
634    {
635        // If strict, all roles must be denied
636        if ($this->multiStrict) {
637            $result = true;
638            foreach ($roles as $role) {
639                if (!$this->isDenied($role, $resource, $permission)) {
640                    $result = false;
641                    break;
642                }
643            }
644        // Else, evaluate loosely
645        } else {
646            // Else, evaluate if any of the roles are denied
647            $result = false;
648            foreach ($roles as $role) {
649                if ($this->isDenied($role, $resource, $permission)) {
650                    $result = true;
651                    break;
652                }
653            }
654        }
655
656        return $result;
657    }
658
659    /**
660     * Determine if multiple roles are denied using the strict parameter
661     *  All of the roles must be denied to return true, otherwise it will return false
662     *
663     * @param  array $roles
664     * @param  mixed $resource
665     * @param  mixed $permission
666     * @return bool
667     *@throws Exception
668     */
669    public function isDeniedMultiStrict(array $roles, mixed $resource = null, mixed $permission = null): bool
670    {
671        $this->multiStrict = true;
672        return $this->isDeniedMulti($roles, $resource, $permission);
673    }
674
675    /**
676     * Create assertion
677     *
678     * @param  AssertionInterface $assertion
679     * @param  string             $type
680     * @param  mixed              $role
681     * @param  mixed              $resource
682     * @param  ?string            $permission
683     * @throws InvalidArgumentException
684     * @return void
685     */
686    public function createAssertion(AssertionInterface $assertion, string $type, mixed $role, mixed $resource = null, ?string $permission = null): void
687    {
688        $key = $this->generateAssertionKey($role, $resource, $permission);
689
690        if (($type != 'allowed') && ($type != 'denied')) {
691            throw new InvalidArgumentException("Error: The assertion type must be either 'allowed' or 'denied'.");
692        }
693        $this->assertions[$type][$key] = $assertion;
694    }
695
696    /**
697     * Delete assertion
698     *
699     * @param  string  $type
700     * @param  mixed   $role
701     * @param  mixed   $resource
702     * @param  ?string $permission
703     * @return void
704     */
705    public function deleteAssertion(string $type, mixed $role, mixed $resource = null, ?string $permission = null): void
706    {
707        $key = $this->generateAssertionKey($role, $resource, $permission);
708
709        if (isset($this->assertions[$type][$key])) {
710            unset($this->assertions[$type][$key]);
711        }
712    }
713
714    /**
715     * Has assertion key
716     *
717     * @param  string  $type
718     * @param  mixed   $role
719     * @param  mixed   $resource
720     * @param  ?string $permission
721     * @throws InvalidArgumentException
722     * @return bool
723     */
724    public function hasAssertionKey(string $type, mixed $role, mixed $resource = null, ?string $permission = null): bool
725    {
726        $key = $this->generateAssertionKey($role, $resource, $permission);
727
728        if (($type != 'allowed') && ($type != 'denied')) {
729            throw new InvalidArgumentException("Error: The assertion type must be either 'allowed' or 'denied'.");
730        }
731
732        return (isset($this->assertions[$type][$key]));
733    }
734
735    /**
736     * Get assertion key
737     *
738     * @param  string  $type
739     * @param  mixed   $role
740     * @param  mixed   $resource
741     * @param  ?string $permission
742     * @throws InvalidArgumentException
743     * @return string|null
744     */
745    public function getAssertionKey(string $type, mixed $role, mixed $resource = null, ?string $permission = null): string|null
746    {
747        $key = $this->generateAssertionKey($role, $resource, $permission);
748
749        if (($type != 'allowed') && ($type != 'denied')) {
750            throw new InvalidArgumentException("Error: The assertion type must be either 'allowed' or 'denied'.");
751        }
752
753        return (isset($this->assertions[$type][$key])) ? $key : null;
754    }
755
756    /**
757     * Add policy
758     *
759     * @param  string $method
760     * @param  mixed  $role
761     * @param  mixed  $resource
762     * @return Acl
763     */
764    public function addPolicy(string $method, mixed $role, mixed $resource = null): Acl
765    {
766        $this->policies[] = [
767            'method'   => $method,
768            'role'     => $role,
769            'resource' => $resource
770        ];
771
772        return $this;
773    }
774
775    /**
776     * Has policies
777     *
778     * @return bool
779     */
780    public function hasPolicies(): bool
781    {
782        return (count($this->policies) > 0);
783    }
784
785    /**
786     * Evaluate policies
787     *
788     * @param  mixed $role
789     * @param  mixed $resource
790     * @param  mixed $permission
791     * @throws Exception
792     * @return bool|null
793     */
794    public function evaluatePolicies(mixed $role = null, mixed $resource = null, mixed $permission = null): bool|null
795    {
796        $result = null;
797
798        if (($role === null) && ($resource === null) && ($permission === null)) {
799            foreach ($this->policies as $policy) {
800                $result = $this->evaluatePolicy($policy['method'], $policy['role'], $policy['resource']);
801                if ($result === false) {
802                    return false;
803                }
804            }
805        } else {
806            $policyRole     = null;
807            $policyResource = null;
808            $policyMethod   = ($permission !== null) ? $permission : null;
809
810            if ($role !== null) {
811                $this->verifyRole($role);
812                $policyRole = ($role instanceof AclRole) ? $role->getName() : $role;
813            }
814            if ($resource !== null) {
815                $this->verifyResource($resource);
816                $policyResource = ($resource instanceof AclResource) ? $resource->getName() : $resource;
817            }
818
819            foreach ($this->policies as $policy) {
820                if ((($policyRole === null) || ($policyRole == $policy['role'])) &&
821                    (($policyResource === null) || ($policyResource == $policy['resource'])) &&
822                    (($policyMethod === null) || ($policyMethod == $policy['method']))) {
823                    $result = $this->evaluatePolicy($policy['method'], $policy['role'], $policy['resource']);
824                    if ($result === false) {
825                        return false;
826                    }
827                }
828            }
829        }
830
831        return $result;
832    }
833
834    /**
835     * Evaluate policy
836     *
837     * @param  string $method
838     * @param  mixed  $role
839     * @param  mixed  $resource
840     * @throws Exception
841     * @return bool|null
842     */
843    public function evaluatePolicy(string $method, mixed $role, mixed $resource = null): bool|null
844    {
845        if (is_string($role) && ($this->verifyRole($role))) {
846            $role = $this->roles[(string)$role];
847        }
848
849        if (!in_array('Pop\Acl\Policy\PolicyTrait', class_uses($role))) {
850            throw new Exception('Error: The role must use Pop\Acl\Policy\PolicyTrait.');
851        }
852
853        if ($resource !== null) {
854            $this->verifyResource($resource);
855            $resource = $this->resources[(string)$resource];
856        }
857
858        return $role->can($method, $resource);
859    }
860
861    /**
862     * Verify role
863     *
864     * @param  mixed $role
865     * @throws Exception
866     * @return bool
867     */
868    protected function verifyRole(mixed $role): bool
869    {
870        if (!is_string($role) && !($role instanceof AclRole)) {
871            throw new \InvalidArgumentException('Error: The role must be a string or an instance of Role.');
872        }
873        if (!isset($this->roles[(string)$role])) {
874            throw new Exception("Error: The role '" . (string)$role . "' has not been added.");
875        }
876
877        return true;
878    }
879
880    /**
881     * Verify resource
882     *
883     * @param  mixed $resource
884     * @throws Exception
885     * @return bool
886     */
887    protected function verifyResource(mixed $resource): bool
888    {
889        if (!is_string($resource) && !($resource instanceof AclResource)) {
890            throw new \InvalidArgumentException('Error: The resource must be a string or an instance of Resource.');
891        }
892        if (!isset($this->resources[(string)$resource])) {
893            throw new Exception("Error: The resource '" . (string)$resource . "' has not been added.");
894        }
895
896        return true;
897    }
898
899    /**
900     * Generate assertion key
901     *
902     * @param  mixed   $role
903     * @param  mixed   $resource
904     * @param  ?string $permission
905     * @return string
906     */
907    protected function generateAssertionKey(mixed $role, mixed $resource = null, ?string $permission = null): string
908    {
909        $key = (string)$role;
910
911        if ($resource !== null) {
912            $key .= '-' . (string)$resource;
913        }
914        if ($permission !== null) {
915            $key .= '-' . (string)$permission;
916        }
917
918        return $key;
919    }
920
921    /**
922     * Traverse child roles to add them to the ACL object
923     *
924     * @param  array $roles
925     * @return void
926     */
927    protected function traverseChildren(array $roles): void
928    {
929        foreach ($roles as $role) {
930            $this->addRole($role);
931            if ($role->hasChildren()) {
932                $this->traverseChildren($role->getChildren());
933            }
934        }
935    }
936
937}