Issues (51)

src/Acl/DebugAcl.php (2 issues)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Acl;
6
7
use Ecodev\Felix\Acl\Assertion\NamedAssertion;
8
use Laminas\Permissions\Acl\Assertion\AssertionInterface;
9
use Laminas\Permissions\Acl\Assertion\CallbackAssertion;
10
use Laminas\Permissions\Acl\Resource\ResourceInterface;
11
use Laminas\Permissions\Acl\Role\RoleInterface;
12
13
/**
14
 * Debug ACL is used to mirror normal ACL but with assertions configured but disabled. This allows
15
 * to query the entire ACL configuration, including configured assertions, while still leveraging
16
 * the complex underlying logic of Laminas ACL.
17
 *
18
 * @internal
19
 */
20
final class DebugAcl extends \Laminas\Permissions\Acl\Acl
21
{
22
    /**
23
     * @var string[]
24
     */
25
    private array $usedAllowAssertions = [];
26
27
    /**
28
     * @var string[]
29
     */
30
    private array $usedDenyAssertions = [];
31
32
    /**
33
     * @var array<null|string> All possible privileges
34
     */
35
    private array $privileges = [];
36
37
    /**
38
     * @var array<string, array<string>> All possible privileges
39
     */
40
    private array $privilegesByResource = [];
41
42
    /**
43
     * @return ($assert is null ? null : AssertionInterface)
44
     */
45 12
    private function wrapAssertion(?AssertionInterface $assert, bool $isAllow): ?AssertionInterface
46
    {
47 12
        if (!$assert) {
48 7
            return null;
49
        }
50
51 6
        return new CallbackAssertion(function () use ($assert, $isAllow) {
52 2
            $name = $assert instanceof NamedAssertion ? $assert->getName() : 'unnamed assertion ' . $assert::class;
53
54 2
            if ($isAllow) {
55 1
                $this->usedAllowAssertions[] = $name;
56
            } else {
57 1
                $this->usedDenyAssertions[] = $name;
58
            }
59
60
            // hardcode response so other assertions from other roles are executed too
61 2
            return !$isAllow;
62 6
        });
63
    }
64
65 12
    public function allow($roles = null, $resources = null, $privileges = null, ?AssertionInterface $assert = null)
66
    {
67 12
        $this->storePrivileges($resources, $privileges);
68 12
        $assert = $this->wrapAssertion($assert, true);
69
70 12
        return parent::allow($roles, $resources, $privileges, $assert);
71
    }
72
73 2
    public function deny($roles = null, $resources = null, $privileges = null, ?AssertionInterface $assert = null)
74
    {
75 2
        $this->storePrivileges($resources, $privileges);
76 2
        $assert = $this->wrapAssertion($assert, false);
77
78 2
        return parent::deny($roles, $resources, $privileges, $assert);
79
    }
80
81 12
    private function storePrivileges(null|string|array|ResourceInterface $resource, null|string|array $privileges): void
82
    {
83 12
        if (!is_array($resource)) {
0 ignored issues
show
The condition is_array($resource) is always true.
Loading history...
84 8
            $resource = [$resource];
85
        }
86
87 12
        if (!is_array($privileges)) {
0 ignored issues
show
The condition is_array($privileges) is always true.
Loading history...
88 7
            $privileges = [$privileges];
89
        }
90
91 12
        $this->privileges = array_merge($this->privileges, $privileges);
92
93
        // Keep non-null privileges only
94 12
        $privileges = array_filter($privileges);
95 12
        if (!$privileges) {
96 3
            return;
97
        }
98
99 12
        foreach ($resource as $oneResource) {
100 12
            $oneResource = (string) $oneResource;
101 12
            if (!$oneResource) {
102 1
                continue;
103
            }
104
105 12
            if (!isset($this->privilegesByResource[$oneResource])) {
106 12
                $this->privilegesByResource[$oneResource] = [];
107
            }
108
109 12
            $this->privilegesByResource[$oneResource] = array_merge($this->privilegesByResource[$oneResource], $privileges);
110
        }
111
    }
112
113
    /**
114
     * Returns all non-null privileges indexed by all non-null resources.
115
     *
116
     * @return array<string, array<string>>
117
     */
118 3
    public function getPrivilegesByResource(): array
119
    {
120 3
        foreach ($this->privilegesByResource as &$privileges) {
121 3
            $privileges = array_unique($privileges);
122 3
            sort($privileges);
123
        }
124
125 3
        return $this->privilegesByResource;
126
    }
127
128
    /**
129
     * @return array<null|string>
130
     */
131 4
    public function getPrivileges(): array
132
    {
133
        // Keep most common privileges at the beginning, for convenience
134 4
        $mostCommon = [
135 4
            null,
136 4
            'create',
137 4
            'read',
138 4
            'update',
139 4
            'index',
140 4
            'view',
141 4
            'edit',
142 4
            'add',
143 4
            'delete',
144 4
            'deleteAll',
145 4
        ];
146
147 4
        $mostCommonThatExists = array_unique(array_intersect($mostCommon, $this->privileges));
148 4
        $result = array_unique(array_diff($this->privileges, $mostCommonThatExists));
149 4
        sort($result);
150
151 4
        $result = array_merge($mostCommonThatExists, $result);
152
153 4
        return $result;
154
    }
155
156
    /**
157
     * Override parent to provide compatibility with MultipleRoles.
158
     *
159
     * @param RoleInterface|string $role
160
     * @param ResourceInterface|string $resource
161
     * @param ?string $privilege
162
     */
163 5
    public function isAllowed($role = null, $resource = null, $privilege = null): bool
164
    {
165 5
        $this->usedAllowAssertions = [];
166 5
        $this->usedDenyAssertions = [];
167
168
        // Normalize roles
169 5
        if ($role instanceof MultipleRoles) {
170 2
            $roles = $role->getRoles();
171
        } else {
172 5
            $roles = [$role];
173
        }
174
175
        // If at least one role is allowed, then return early
176 5
        foreach ($roles as $role) {
177 5
            if (parent::isAllowed($role, $resource, $privilege)) {
178 3
                return true;
179
            }
180
        }
181
182 3
        return false;
183
    }
184
185
    /**
186
     * Returns whether the privilege is allowed and the assertions that were used to determine that.
187
     *
188
     * @return array{privilege: null|string, allowed: bool, allowIf: string[], denyIf: string[]}
189
     */
190 5
    public function show(null|RoleInterface|string $role, null|ResourceInterface|string $resource, null|string $privilege): array
191
    {
192 5
        $allowed = $this->isAllowed($role, $resource, $privilege);
193 5
        sort($this->usedAllowAssertions);
194 5
        sort($this->usedDenyAssertions);
195
196 5
        return [
197 5
            'privilege' => $privilege,
198 5
            'allowed' => $allowed,
199 5
            'allowIf' => array_unique($this->usedAllowAssertions),
200 5
            'denyIf' => array_unique($this->usedDenyAssertions),
201 5
        ];
202
    }
203
}
204