Passed
Pull Request — master (#17)
by Adrien
13:27
created

Acl::deny()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 4
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
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 8
    public function __construct()
40
    {
41 8
        $this->debugAcl = new DebugAcl();
42
    }
43
44 8
    public function addRole($role, $parents = null)
45
    {
46 8
        $this->debugAcl->addRole($role, $parents);
47
48 8
        return parent::addRole($role, $parents);
49
    }
50
51 8
    public function addResource($resource, $parent = null)
52
    {
53 8
        $this->debugAcl->addResource($resource, $parent);
54
55 8
        return parent::addResource($resource, $parent);
56
    }
57
58 8
    public function allow($roles = null, $resources = null, $privileges = null, ?AssertionInterface $assert = null)
59
    {
60 8
        $this->debugAcl->allow($roles, $resources, $privileges, $assert);
61
62 8
        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 4
    protected function createModelResource(string $class): ModelResource
73
    {
74 4
        $resource = new ModelResource($class);
75 4
        $this->addResource($resource);
76
77 4
        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 5
    public function isAllowed($role = null, $resource = null, $privilege = null): bool
88
    {
89
        // Normalize roles
90 5
        if ($role instanceof MultipleRoles) {
91 1
            $roles = $role->getRoles();
92
        } else {
93 4
            $roles = [$role];
94
        }
95
96
        // If at least one role is allowed, then return early
97 5
        foreach ($roles as $role) {
98 5
            if (parent::isAllowed($role, $resource, $privilege)) {
99 1
                return true;
100
            }
101
        }
102
103 5
        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 5
    public function isCurrentUserAllowed(Model|string $modelOrResource, string $privilege): bool
112
    {
113 5
        $resource = is_string($modelOrResource) ? $modelOrResource : new ModelResource($this->getClass($modelOrResource), $modelOrResource);
114 5
        $role = $this->getCurrentRole();
115 5
        $this->reasons = [];
116
117 5
        $isAllowed = $this->isAllowed($role, $resource, $privilege);
118
119 5
        $this->message = $this->buildMessage($resource, $privilege, $role, $isAllowed);
120
121 5
        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 4
    public function reject(string $reason): false
131
    {
132 4
        $this->reasons[] = $reason;
133
134 4
        return false;
135
    }
136
137 4
    private function getClass(Model $resource): string
138
    {
139 4
        return ClassUtils::getRealClass($resource::class);
140
    }
141
142 5
    private function getCurrentRole(): MultipleRoles|string
143
    {
144 5
        $user = CurrentUser::get();
145 5
        if (!$user) {
146 2
            return 'anonymous';
147
        }
148
149 4
        return $user->getRole();
150
    }
151
152 5
    private function buildMessage(ModelResource|string $resource, ?string $privilege, MultipleRoles|string $role, bool $isAllowed): ?string
153
    {
154 5
        if ($isAllowed) {
155 1
            return null;
156
        }
157
158 5
        if ($resource instanceof ModelResource) {
159 4
            $resource = $resource->getName();
160
        }
161
162 5
        $user = CurrentUser::get();
163 5
        $userName = $user ? 'User "' . $user->getLogin() . '"' : 'Non-logged user';
164 5
        $privilege ??= 'NULL';
165
166 5
        $message = "$userName with role $role is not allowed on resource \"$resource\" with privilege \"$privilege\"";
167
168 5
        $this->reasons = array_unique($this->reasons);
169 5
        $count = count($this->reasons);
170 5
        if ($count === 1) {
171 3
            $message .= ' because ' . $this->reasons[0];
172 3
        } elseif ($count) {
173 1
            $list = array_map(fn ($reason) => '- ' . $reason, $this->reasons);
174 1
            $message .= ' because:' . PHP_EOL . PHP_EOL . implode(PHP_EOL, $list);
175
        }
176
177 5
        return $message;
178
    }
179
180
    /**
181
     * Returns the message explaining the last denial, if any.
182
     */
183 5
    public function getLastDenialMessage(): ?string
184
    {
185 5
        return $this->message;
186
    }
187
188
    /**
189
     * Show the ACL configuration for the given role in a structured array.
190
     *
191
     * @return array<array{resource:string, privileges: array<int, array{privilege:null|string, allowed: bool, allowIf: string[], denyIf: string[]}>}>
192
     */
193 2
    public function show(MultipleRoles|string $role, bool $useTranslations = true): array
194
    {
195 2
        $result = [];
196
        /** @var string[] $resources */
197 2
        $resources = $this->getResources();
198 2
        sort($resources);
199
200 2
        foreach ($resources as $resource) {
201 2
            $privileges = [];
202 2
            foreach ($this->debugAcl->getPrivileges() as $privilege) {
203 2
                $privileges[] = $this->debugAcl->show($role, $resource, $privilege);
204
            }
205
206 2
            $result[] = [
207 2
                'resource' => $resource,
208 2
                'privileges' => $privileges,
209 2
            ];
210
        }
211
212 2
        ksort($result);
213
214 2
        if ($useTranslations && ($this->resourceTranslations || $this->privilegeTranslations)) {
215 2
            foreach ($result as &$resource) {
216 2
                $resource['resource'] = $this->translate($this->resourceTranslations, $resource['resource']);
217 2
                foreach ($resource['privileges'] as &$privilege) {
218 2
                    $privilege['privilege'] = $this->translate($this->privilegeTranslations, $privilege['privilege'] ?? '');
219
                }
220
            }
221
        }
222
223 1
        return $result;
224
    }
225
226
    /**
227
     * Configure the translations for all resources and all privileges for the `show()` method.
228
     *
229
     * If this is set but one translation is missing, then `show()` will throw an exception when called.
230
     *
231
     * To disable translation altogether you can pass two empty lists.
232
     *
233
     * @param array<string, string> $resourceTranslations
234
     * @param array<string, string> $privilegeTranslations
235
     */
236 2
    public function setTranslations(array $resourceTranslations, array $privilegeTranslations): void
237
    {
238 2
        $this->resourceTranslations = $resourceTranslations;
239 2
        $this->privilegeTranslations = $privilegeTranslations;
240
    }
241
242
    /**
243
     * @param array<string, string> $translations
244
     */
245 2
    private function translate(array $translations, string $message): string
246
    {
247 2
        return $translations[$message] ?? throw new Exception('Was not marked as translatable: ' . $message);
248
    }
249
250
    /**
251
     * Returns all non-null privileges indexed by all non-null resources.
252
     *
253
     * @return array<string, array<string>>
254
     */
255 1
    public function getPrivilegesByResource(): array
256
    {
257 1
        return $this->debugAcl->getPrivilegesByResource();
258
    }
259
}
260