Passed
Push — master ( bc171c...73bfc2 )
by Adrien
02:59
created

Acl::isCurrentUserAllowed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 1
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 5
    public function __construct()
40
    {
41 5
        $this->debugAcl = new DebugAcl();
42
    }
43
44 5
    public function addRole($role, $parents = null)
45
    {
46 5
        $this->debugAcl->addRole($role, $parents);
47
48 5
        return parent::addRole($role, $parents);
49
    }
50
51 5
    public function addResource($resource, $parent = null)
52
    {
53 5
        $this->debugAcl->addResource($resource, $parent);
54
55 5
        return parent::addResource($resource, $parent);
56
    }
57
58 5
    public function allow($roles = null, $resources = null, $privileges = null, ?AssertionInterface $assert = null)
59
    {
60 5
        $this->debugAcl->allow($roles, $resources, $privileges, $assert);
61
62 5
        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
1 ignored issue
show
Bug introduced by
The type Ecodev\Felix\Acl\ModelResource was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 RoleInterface|string $role
84
     * @param ResourceInterface|string $resource
85
     * @param ?string $privilege
86
     */
87 3
    public function isAllowed($role = null, $resource = null, $privilege = null): bool
88
    {
89
        // Normalize roles
90 3
        if ($role instanceof MultipleRoles) {
91 1
            $roles = $role->getRoles();
92
        } else {
93 2
            $roles = [$role];
94
        }
95
96
        // If at least one role is allowed, then return early
97 3
        foreach ($roles as $role) {
98 3
            if (parent::isAllowed($role, $resource, $privilege)) {
99 1
                return true;
100
            }
101
        }
102
103 3
        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 3
    public function isCurrentUserAllowed(Model $model, string $privilege): bool
112
    {
113 3
        $resource = new ModelResource($this->getClass($model), $model);
114 3
        $role = $this->getCurrentRole();
115 3
        $this->reasons = [];
116
117 3
        $isAllowed = $this->isAllowed($role, $resource, $privilege);
118
119 3
        $this->message = $this->buildMessage($resource, $privilege, $role, $isAllowed);
120
121 3
        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 2
    public function reject(string $reason): bool
133
    {
134 2
        $this->reasons[] = $reason;
135
136 2
        return false;
137
    }
138
139 3
    private function getClass(Model $resource): string
140
    {
141 3
        return ClassUtils::getRealClass($resource::class);
142
    }
143
144 3
    private function getCurrentRole(): MultipleRoles|string
145
    {
146 3
        $user = CurrentUser::get();
147 3
        if (!$user) {
148 1
            return 'anonymous';
149
        }
150
151 3
        return $user->getRole();
152
    }
153
154 3
    private function buildMessage(ModelResource $resource, ?string $privilege, MultipleRoles|string $role, bool $isAllowed): ?string
155
    {
156 3
        if ($isAllowed) {
157 1
            return null;
158
        }
159
160 3
        $resource = $resource->getName();
161
162 3
        $user = CurrentUser::get();
163 3
        $userName = $user ? 'User "' . $user->getLogin() . '"' : 'Non-logged user';
164 3
        $privilege ??= 'NULL';
165
166 3
        $message = "$userName with role $role is not allowed on resource \"$resource\" with privilege \"$privilege\"";
167
168 3
        $count = count($this->reasons);
169 3
        if ($count === 1) {
170 1
            $message .= ' because ' . $this->reasons[0];
171 3
        } elseif ($count) {
172 1
            $list = array_map(fn ($reason) => '- ' . $reason, $this->reasons);
173 1
            $message .= ' because:' . PHP_EOL . PHP_EOL . implode(PHP_EOL, $list);
174
        }
175
176 3
        return $message;
177
    }
178
179
    /**
180
     * Returns the message explaining the last denial, if any.
181
     */
182 3
    public function getLastDenialMessage(): ?string
183
    {
184 3
        return $this->message;
185
    }
186
187
    /**
188
     * @return array<array{resource:string, privileges: array<int, array{privilege:null|string, allowed: bool, allowIf: string[], denyIf: string[]}>}>
189
     */
190 2
    public function show(MultipleRoles|string $role, bool $useTranslations = true): array
191
    {
192 2
        $result = [];
193
        /** @var string[] $resources */
194 2
        $resources = $this->getResources();
195 2
        sort($resources);
196
197 2
        foreach ($resources as $resource) {
198 2
            $privileges = [];
199 2
            foreach ($this->debugAcl->getPrivileges() as $privilege) {
200 2
                $privileges[] = $this->debugAcl->show($role, $resource, $privilege);
201
            }
202
203 2
            $result[] = [
204
                'resource' => $resource,
205
                'privileges' => $privileges,
206
            ];
207
        }
208
209 2
        ksort($result);
210
211 2
        if ($useTranslations && ($this->resourceTranslations || $this->privilegeTranslations)) {
212 2
            foreach ($result as &$resource) {
213 2
                $resource['resource'] = $this->translate($this->resourceTranslations, $resource['resource']);
214 2
                foreach ($resource['privileges'] as &$privilege) {
215 2
                    $privilege['privilege'] = $this->translate($this->privilegeTranslations, $privilege['privilege'] ?? '');
216
                }
217
            }
218
        }
219
220 1
        return $result;
221
    }
222
223
    /**
224
     * Configure the translations for all resources and all privileges for the `show()` method.
225
     *
226
     * If this is set but one translation is missing, then `show()` will throw an exception when called.
227
     *
228
     * To disable translation altogether you can pass two empty lists.
229
     *
230
     * @param array<string, string> $resourceTranslations
231
     * @param array<string, string> $privilegeTranslations
232
     */
233 2
    public function setTranslations(array $resourceTranslations, array $privilegeTranslations): void
234
    {
235 2
        $this->resourceTranslations = $resourceTranslations;
236 2
        $this->privilegeTranslations = $privilegeTranslations;
237
    }
238
239
    /**
240
     * @param array<string, string> $translations
241
     */
242 2
    private function translate(array $translations, string $message): string
243
    {
244 2
        return $translations[$message] ?? throw new Exception('Was not marked as translatable: ' . $message);
245
    }
246
}
247