Passed
Push — master ( 73bfc2...a878a7 )
by Adrien
13:45 queued 10:48
created

Acl   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Test Coverage

Coverage 96.43%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 76
c 1
b 0
f 0
dl 0
loc 244
ccs 81
cts 84
cp 0.9643
rs 9.68
wmc 34

17 Methods

Rating   Name   Duplication   Size   Complexity  
A reject() 0 5 1
A allow() 0 5 1
A addResource() 0 5 1
A getCurrentRole() 0 8 2
A addRole() 0 5 1
A __construct() 0 3 1
A isAllowed() 0 17 4
A createModelResource() 0 6 1
A getClass() 0 3 1
A deny() 0 5 1
A buildMessage() 0 25 6
A isCurrentUserAllowed() 0 11 2
A getLastDenialMessage() 0 3 1
A setTranslations() 0 4 1
A translate() 0 3 1
A getPrivilegesByResource() 0 3 1
B show() 0 31 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Acl;
6
7
use Doctrine\Common\Util\ClassUtils;
8
use Ecodev\Felix\Model\CurrentUser;
9
use Ecodev\Felix\Model\Model;
10
use Exception;
11
use Laminas\Permissions\Acl\Assertion\AssertionInterface;
12
use Laminas\Permissions\Acl\Resource\ResourceInterface;
13
use Laminas\Permissions\Acl\Role\RoleInterface;
14
15
class Acl extends \Laminas\Permissions\Acl\Acl
16
{
17
    /**
18
     * The message explaining the last denial.
19
     */
20
    private ?string $message = null;
21
22
    /**
23
     * @var string[]
24
     */
25
    private array $reasons = [];
26
27
    private DebugAcl $debugAcl;
28
29
    /**
30
     * @var array<string, string>
31
     */
32
    private array $resourceTranslations = [];
33
34
    /**
35
     * @var array<string, string>
36
     */
37
    private array $privilegeTranslations = [];
38
39 7
    public function __construct()
40
    {
41 7
        $this->debugAcl = new DebugAcl();
42
    }
43
44 7
    public function addRole($role, $parents = null)
45
    {
46 7
        $this->debugAcl->addRole($role, $parents);
47
48 7
        return parent::addRole($role, $parents);
49
    }
50
51 7
    public function addResource($resource, $parent = null)
52
    {
53 7
        $this->debugAcl->addResource($resource, $parent);
54
55 7
        return parent::addResource($resource, $parent);
56
    }
57
58 7
    public function allow($roles = null, $resources = null, $privileges = null, ?AssertionInterface $assert = null)
59
    {
60 7
        $this->debugAcl->allow($roles, $resources, $privileges, $assert);
61
62 7
        return parent::allow($roles, $resources, $privileges, $assert);
63
    }
64
65
    public function deny($roles = null, $resources = null, $privileges = null, ?AssertionInterface $assert = null)
66
    {
67
        $this->debugAcl->deny($roles, $resources, $privileges, $assert);
68
69
        return parent::deny($roles, $resources, $privileges, $assert);
70
    }
71
72 3
    protected function createModelResource(string $class): ModelResource
73
    {
74 3
        $resource = new ModelResource($class);
75 3
        $this->addResource($resource);
76
77 3
        return $resource;
78
    }
79
80
    /**
81
     * Override parent to provide compatibility with MultipleRoles.
82
     *
83
     * @param null|RoleInterface|string $role
84
     * @param null|ResourceInterface|string $resource
85
     * @param null|string $privilege
86
     */
87 4
    public function isAllowed($role = null, $resource = null, $privilege = null): bool
88
    {
89
        // Normalize roles
90 4
        if ($role instanceof MultipleRoles) {
91 1
            $roles = $role->getRoles();
92
        } else {
93 3
            $roles = [$role];
94
        }
95
96
        // If at least one role is allowed, then return early
97 4
        foreach ($roles as $role) {
98 4
            if (parent::isAllowed($role, $resource, $privilege)) {
99 1
                return true;
100
            }
101
        }
102
103 4
        return false;
104
    }
105
106
    /**
107
     * Return whether the current user is allowed to do something.
108
     *
109
     * This should be the main method to do all ACL checks.
110
     */
111 4
    public function isCurrentUserAllowed(Model|string $modelOrResource, string $privilege): bool
112
    {
113 4
        $resource = is_string($modelOrResource) ? $modelOrResource : new ModelResource($this->getClass($modelOrResource), $modelOrResource);
114 4
        $role = $this->getCurrentRole();
115 4
        $this->reasons = [];
116
117 4
        $isAllowed = $this->isAllowed($role, $resource, $privilege);
118
119 4
        $this->message = $this->buildMessage($resource, $privilege, $role, $isAllowed);
120
121 4
        return $isAllowed;
122
    }
123
124
    /**
125
     * Set the reason for rejection that will be shown to end-user.
126
     *
127
     * This method always return false for usage convenience and should be used by all assertions,
128
     * instead of only return false themselves.
129
     *
130
     * @return false
131
     */
132 3
    public function reject(string $reason): bool
133
    {
134 3
        $this->reasons[] = $reason;
135
136 3
        return false;
137
    }
138
139 3
    private function getClass(Model $resource): string
140
    {
141 3
        return ClassUtils::getRealClass($resource::class);
142
    }
143
144 4
    private function getCurrentRole(): MultipleRoles|string
145
    {
146 4
        $user = CurrentUser::get();
147 4
        if (!$user) {
148 2
            return 'anonymous';
149
        }
150
151 3
        return $user->getRole();
152
    }
153
154 4
    private function buildMessage(ModelResource|string $resource, ?string $privilege, MultipleRoles|string $role, bool $isAllowed): ?string
155
    {
156 4
        if ($isAllowed) {
157 1
            return null;
158
        }
159
160 4
        if ($resource instanceof ModelResource) {
161 3
            $resource = $resource->getName();
162
        }
163
164 4
        $user = CurrentUser::get();
165 4
        $userName = $user ? 'User "' . $user->getLogin() . '"' : 'Non-logged user';
166 4
        $privilege ??= 'NULL';
167
168 4
        $message = "$userName with role $role is not allowed on resource \"$resource\" with privilege \"$privilege\"";
169
170 4
        $count = count($this->reasons);
171 4
        if ($count === 1) {
172 2
            $message .= ' because ' . $this->reasons[0];
173 3
        } elseif ($count) {
174 1
            $list = array_map(fn ($reason) => '- ' . $reason, $this->reasons);
175 1
            $message .= ' because:' . PHP_EOL . PHP_EOL . implode(PHP_EOL, $list);
176
        }
177
178 4
        return $message;
179
    }
180
181
    /**
182
     * Returns the message explaining the last denial, if any.
183
     */
184 4
    public function getLastDenialMessage(): ?string
185
    {
186 4
        return $this->message;
187
    }
188
189
    /**
190
     * Show the ACL configuration for the given role in a structured array.
191
     *
192
     * @return array<array{resource:string, privileges: array<int, array{privilege:null|string, allowed: bool, allowIf: string[], denyIf: string[]}>}>
193
     */
194 2
    public function show(MultipleRoles|string $role, bool $useTranslations = true): array
195
    {
196 2
        $result = [];
197
        /** @var string[] $resources */
198 2
        $resources = $this->getResources();
199 2
        sort($resources);
200
201 2
        foreach ($resources as $resource) {
202 2
            $privileges = [];
203 2
            foreach ($this->debugAcl->getPrivileges() as $privilege) {
204 2
                $privileges[] = $this->debugAcl->show($role, $resource, $privilege);
205
            }
206
207 2
            $result[] = [
208
                'resource' => $resource,
209
                'privileges' => $privileges,
210
            ];
211
        }
212
213 2
        ksort($result);
214
215 2
        if ($useTranslations && ($this->resourceTranslations || $this->privilegeTranslations)) {
216 2
            foreach ($result as &$resource) {
217 2
                $resource['resource'] = $this->translate($this->resourceTranslations, $resource['resource']);
218 2
                foreach ($resource['privileges'] as &$privilege) {
219 2
                    $privilege['privilege'] = $this->translate($this->privilegeTranslations, $privilege['privilege'] ?? '');
220
                }
221
            }
222
        }
223
224 1
        return $result;
225
    }
226
227
    /**
228
     * Configure the translations for all resources and all privileges for the `show()` method.
229
     *
230
     * If this is set but one translation is missing, then `show()` will throw an exception when called.
231
     *
232
     * To disable translation altogether you can pass two empty lists.
233
     *
234
     * @param array<string, string> $resourceTranslations
235
     * @param array<string, string> $privilegeTranslations
236
     */
237 2
    public function setTranslations(array $resourceTranslations, array $privilegeTranslations): void
238
    {
239 2
        $this->resourceTranslations = $resourceTranslations;
240 2
        $this->privilegeTranslations = $privilegeTranslations;
241
    }
242
243
    /**
244
     * @param array<string, string> $translations
245
     */
246 2
    private function translate(array $translations, string $message): string
247
    {
248 2
        return $translations[$message] ?? throw new Exception('Was not marked as translatable: ' . $message);
249
    }
250
251
    /**
252
     * Returns all non-null privileges indexed by all non-null resources.
253
     *
254
     * @return array<string, array<string>>
255
     */
256 1
    public function getPrivilegesByResource(): array
257
    {
258 1
        return $this->debugAcl->getPrivilegesByResource();
259
    }
260
}
261