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