Acl::getPrivilegesByResource()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Acl;
6
7
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
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(DefaultProxyClassNameResolver::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 5
    private function getCurrentRole(): MultipleRoles|string
138
    {
139 5
        $user = CurrentUser::get();
140 5
        if (!$user) {
141 2
            return 'anonymous';
142
        }
143
144 4
        return $user->getRole();
145
    }
146
147 5
    private function buildMessage(ModelResource|string $resource, ?string $privilege, MultipleRoles|string $role, bool $isAllowed): ?string
148
    {
149 5
        if ($isAllowed) {
150 1
            return null;
151
        }
152
153 5
        if ($resource instanceof ModelResource) {
154 4
            $resource = $resource->getName();
155
        }
156
157 5
        $user = CurrentUser::get();
158 5
        $userName = $user ? 'User "' . $user->getLogin() . '"' : 'Non-logged user';
159 5
        $privilege ??= 'NULL';
160
161 5
        $message = "$userName with role $role is not allowed on resource \"$resource\" with privilege \"$privilege\"";
162
163 5
        $this->reasons = array_unique($this->reasons);
164 5
        $count = count($this->reasons);
165 5
        if ($count === 1) {
166 3
            $message .= ' because ' . $this->reasons[0];
167 3
        } elseif ($count) {
168 1
            $list = array_map(fn ($reason) => '- ' . $reason, $this->reasons);
169 1
            $message .= ' because:' . PHP_EOL . PHP_EOL . implode(PHP_EOL, $list);
170
        }
171
172 5
        return $message;
173
    }
174
175
    /**
176
     * Returns the message explaining the last denial, if any.
177
     */
178 5
    public function getLastDenialMessage(): ?string
179
    {
180 5
        return $this->message;
181
    }
182
183
    /**
184
     * Show the ACL configuration for the given role in a structured array.
185
     *
186
     * @return array<array{resource:string, privileges: array<int, array{privilege:null|string, allowed: bool, allowIf: string[], denyIf: string[]}>}>
187
     */
188 2
    public function show(MultipleRoles|string $role, bool $useTranslations = true): array
189
    {
190 2
        $result = [];
191
        /** @var string[] $resources */
192 2
        $resources = $this->getResources();
193 2
        sort($resources);
194
195 2
        foreach ($resources as $resource) {
196 2
            $privileges = [];
197 2
            foreach ($this->debugAcl->getPrivileges() as $privilege) {
198 2
                $privileges[] = $this->debugAcl->show($role, $resource, $privilege);
199
            }
200
201 2
            $result[] = [
202 2
                'resource' => $resource,
203 2
                'privileges' => $privileges,
204 2
            ];
205
        }
206
207 2
        ksort($result);
208
209 2
        if ($useTranslations && ($this->resourceTranslations || $this->privilegeTranslations)) {
210 2
            foreach ($result as &$resource) {
211 2
                $resource['resource'] = $this->translate($this->resourceTranslations, $resource['resource']);
212 2
                foreach ($resource['privileges'] as &$privilege) {
213 2
                    $privilege['privilege'] = $this->translate($this->privilegeTranslations, $privilege['privilege'] ?? '');
214
                }
215
            }
216
        }
217
218 1
        return $result;
219
    }
220
221
    /**
222
     * Configure the translations for all resources and all privileges for the `show()` method.
223
     *
224
     * If this is set but one translation is missing, then `show()` will throw an exception when called.
225
     *
226
     * To disable translation altogether you can pass two empty lists.
227
     *
228
     * @param array<string, string> $resourceTranslations
229
     * @param array<string, string> $privilegeTranslations
230
     */
231 2
    public function setTranslations(array $resourceTranslations, array $privilegeTranslations): void
232
    {
233 2
        $this->resourceTranslations = $resourceTranslations;
234 2
        $this->privilegeTranslations = $privilegeTranslations;
235
    }
236
237
    /**
238
     * @param array<string, string> $translations
239
     */
240 2
    private function translate(array $translations, string $message): string
241
    {
242 2
        return $translations[$message] ?? throw new Exception('Was not marked as translatable: ' . $message);
243
    }
244
245
    /**
246
     * Returns all non-null privileges indexed by all non-null resources.
247
     *
248
     * @return array<string, array<string>>
249
     */
250 1
    public function getPrivilegesByResource(): array
251
    {
252 1
        return $this->debugAcl->getPrivilegesByResource();
253
    }
254
}
255