PermsHelper::isAllowed()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 2
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2021 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace App\View\Helper;
14
15
use BEdita\WebTools\ApiClientProvider;
16
use Cake\Utility\Hash;
17
use Cake\View\Helper;
18
19
/**
20
 * Helper class to handle permissions on modules.
21
 */
22
class PermsHelper extends Helper
23
{
24
    /**
25
     * API methods allowed in current module
26
     *
27
     * @var array
28
     */
29
    protected $current = [];
30
31
    /**
32
     * API methods allowed in all modules
33
     *
34
     * @var array
35
     */
36
    protected $allowed = [];
37
38
    /**
39
     * Permissions on folders enabled flag
40
     *
41
     * @var bool
42
     */
43
    protected $permissionsOnFolders = false;
44
45
    /**
46
     * {@inheritDoc}
47
     *
48
     * Init API and WebAPP base URL
49
     *
50
     * @return  void
51
     */
52
    public function initialize(array $config): void
53
    {
54
        $modules = (array)$this->_View->get('modules');
55
        // using foreach instead of Hash::combine
56
        // to avoid RuntimeError "Hash::combine() needs an equal number of keys + values"
57
        foreach ($modules as $name => $module) {
58
            if (Hash::check($module, 'hints.allow')) {
59
                $this->allowed[$name] = Hash::get($module, 'hints.allow');
60
            }
61
        }
62
        $currentModule = (array)$this->_View->get('currentModule');
63
        $this->current = (array)Hash::get($currentModule, 'hints.allow');
64
        $schema = (array)$this->_View->get('foldersSchema');
65
        $this->permissionsOnFolders = in_array('Permissions', (array)Hash::get($schema, 'associations'));
66
    }
67
68
    /**
69
     * Check lock/unlock permission.
70
     *
71
     * @return bool
72
     */
73
    public function canLock(): bool
74
    {
75
        return $this->userIsAdmin();
76
    }
77
78
    /**
79
     * Check create permission.
80
     *
81
     * @param string|null $module Module name
82
     * @return bool
83
     */
84
    public function canCreate(?string $module = null): bool
85
    {
86
        return $this->isAllowed('POST', $module) && $this->userIsAllowed($module);
87
    }
88
89
    /**
90
     * Return modules that can be created by the authenticated user.
91
     *
92
     * @return array
93
     */
94
    public function canCreateModules(): array
95
    {
96
        $modules = array_keys((array)$this->_View->get('modules'));
97
98
        return array_values(
99
            array_filter(
100
                $modules,
101
                function ($module) {
102
                    return $this->canCreate($module);
103
                }
104
            )
105
        );
106
    }
107
108
    /**
109
     * Check delete permission.
110
     *
111
     * @param array $object The object
112
     * @return bool
113
     */
114
    public function canDelete(array $object): bool
115
    {
116
        $locked = (bool)Hash::get($object, 'meta.locked', false);
117
        if ($locked === false) {
118
            $locked = $this->isLockedByParents((string)Hash::get($object, 'id'));
119
        }
120
        $module = (string)Hash::get($object, 'type');
121
122
        return !$locked && $this->isAllowed('DELETE', $module) && $this->userIsAllowed($module);
123
    }
124
125
    /**
126
     * Check save permission.
127
     *
128
     * @param string|null $module Module name
129
     * @return bool
130
     */
131
    public function canSave(?string $module = null): bool
132
    {
133
        return $this->isAllowed('PATCH', $module) && $this->userIsAllowed($module);
134
    }
135
136
    /**
137
     * Check read permission.
138
     *
139
     * @param string|null $module Module name
140
     * @return bool
141
     */
142
    public function canRead(?string $module = null): bool
143
    {
144
        return $this->isAllowed('GET', $module);
145
    }
146
147
    /**
148
     * Check if a method is allowed on a module.
149
     *
150
     * @param string $method Method to check
151
     * @param string|null $module Module name, if missing or null current module is used.
152
     * @return bool
153
     */
154
    protected function isAllowed(string $method, ?string $module = null): bool
155
    {
156
        if (empty($module)) {
157
            if (empty($this->current)) {
158
                return true;
159
            }
160
161
            return in_array($method, $this->current);
162
        }
163
164
        $allowed = (array)Hash::get($this->allowed, $module);
165
166
        return in_array($method, $allowed);
167
    }
168
169
    /**
170
     * Access string (can be 'read', 'write', 'hidden') per role and module.
171
     *
172
     * @param array $accessControl The access control array
173
     * @param string $roleName The role name
174
     * @param string $moduleName The module name
175
     * @return string
176
     */
177
    public function access(array $accessControl, string $roleName, string $moduleName): string
178
    {
179
        $roleAccesses = Hash::get($accessControl, $roleName, []);
180
        if (empty($roleAccesses)) {
181
            return 'write';
182
        }
183
        $hiddenModules = Hash::get($roleAccesses, 'hidden', []);
184
        if (in_array($moduleName, $hiddenModules)) {
185
            return 'hidden';
186
        }
187
        $readonlyModules = Hash::get($roleAccesses, 'readonly', []);
188
189
        return in_array($moduleName, $readonlyModules) ? 'read' : 'write';
190
    }
191
192
    /**
193
     * Return true if authenticated user has role admin
194
     *
195
     * @return bool
196
     */
197
    public function userIsAdmin(): bool
198
    {
199
        return in_array('admin', $this->userRoles());
200
    }
201
202
    /**
203
     * Check permissions for user if object is a folder.
204
     *
205
     * @param string|null $module The module, if passed.
206
     * @return bool
207
     */
208
    public function userIsAllowed(?string $module): bool
209
    {
210
        $objectType = !empty($module) ? $module : $this->_View->get('objectType');
211
        if ($this->permissionsOnFolders === false || $objectType !== 'folders' || $this->userIsAdmin()) {
212
            return true;
213
        }
214
215
        $object = $this->_View->get('object');
216
        $permsRoles = (array)Hash::get((array)$object, 'meta.perms.roles');
217
        if (empty($permsRoles)) {
218
            return true;
219
        }
220
221
        return !empty(array_intersect($permsRoles, $this->userRoles()));
222
    }
223
224
    /**
225
     * Return authenticated user roles
226
     *
227
     * @return array
228
     */
229
    public function userRoles(): array
230
    {
231
        /** @var \Authentication\Identity|null $identity */
232
        $identity = $this->_View->get('user');
233
234
        return empty($identity) ? [] : (array)$identity->get('roles');
235
    }
236
237
    /**
238
     * Return true if object is locked by parents.
239
     * When user is admin, return false.
240
     * When user is not admin, return true if at least one parent is locked for user.
241
     * Return false otherwise
242
     *
243
     * @param string $id The object id
244
     * @return bool
245
     */
246
    public function isLockedByParents(string $id): bool
247
    {
248
        if ($this->permissionsOnFolders === false || $this->userIsAdmin()) {
249
            return false;
250
        }
251
        $apiClient = ApiClientProvider::getApiClient();
252
        $response = (array)$apiClient->get(sprintf('/objects/%s?include=parents', $id));
253
        $included = (array)Hash::get($response, 'included', []);
254
        if (empty($included)) {
255
            return false;
256
        }
257
        $roles = $this->userRoles();
258
        foreach ($included as $data) {
259
            $metaPermsRoles = (array)Hash::get($data, 'meta.perms.roles');
260
            if (empty($metaPermsRoles)) {
261
                continue;
262
            }
263
            if (count(array_intersect($roles, $metaPermsRoles)) === 0) {
264
                return true;
265
            }
266
        }
267
268
        return false;
269
    }
270
}
271