Issues (257)

src/Services/UserSystem/PermissionManager.php (2 issues)

1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\Services\UserSystem;
24
25
use App\Configuration\PermissionsConfiguration;
26
use App\Entity\UserSystem\Group;
27
use App\Entity\UserSystem\User;
28
use App\Security\Interfaces\HasPermissionsInterface;
29
use InvalidArgumentException;
30
use Symfony\Component\Config\ConfigCache;
31
use Symfony\Component\Config\Definition\Processor;
32
use Symfony\Component\Config\Resource\FileResource;
33
use Symfony\Component\Yaml\Yaml;
34
35
/**
36
 * This class manages the permissions of users and groups.
37
 * Permissions are defined in the config/permissions.yaml file, and are parsed and resolved by this class using the
38
 * user and hierachical group PermissionData information.
39
 */
40
class PermissionManager
41
{
42
    protected $permission_structure;
43
44
    protected bool $is_debug;
45
    protected string $cache_file;
46
47
    /**
48
     * PermissionResolver constructor.
49
     */
50
    public function __construct(bool $kernel_debug, string $kernel_cache_dir)
51
    {
52
        $cache_dir = $kernel_cache_dir;
53
        //Here the cached structure will be saved.
54
        $this->cache_file = $cache_dir.'/permissions.php.cache';
55
        $this->is_debug = $kernel_debug;
56
57
        $this->permission_structure = $this->generatePermissionStructure();
58
    }
59
60
    public function getPermissionStructure(): array
61
    {
62
        return $this->permission_structure;
63
    }
64
65
    /**
66
     * Check if a user/group is allowed to do the specified operation for the permission.
67
     *
68
     * See permissions.yaml for valid permission operation combinations.
69
     * This function does not check, if the permission is valid!
70
     *
71
     * @param HasPermissionsInterface $user       the user/group for which the operation should be checked
72
     * @param string                  $permission the name of the permission for which should be checked
73
     * @param string                  $operation  the name of the operation for which should be checked
74
     *
75
     * @return bool|null true, if the user is allowed to do the operation (ALLOW), false if not (DISALLOW), and null,
76
     *                   if the value is set to inherit
77
     */
78
    public function dontInherit(HasPermissionsInterface $user, string $permission, string $operation): ?bool
79
    {
80
        //Check that the permission/operation combination is valid
81
        if (! $this->isValidOperation($permission, $operation)) {
82
            throw new InvalidArgumentException('The permission/operation combination "'.$permission.'/'.$operation.'" is not valid!');
83
        }
84
85
        //Get the permissions from the user
86
        return $user->getPermissions()->getPermissionValue($permission, $operation);
87
    }
88
89
    /**
90
     * Checks if a user is allowed to do the specified operation for the permission.
91
     * In contrast to dontInherit() it tries to resolve to inherit values, of the user, by going upwards in the
92
     * hierarchy (user -> group -> parent group -> so on). But even in this case it is possible, that to inherit value
93
     * could be resolved, and this function returns null.
94
     *
95
     * In that case the voter should set it manually to false by using ?? false.
96
     *
97
     * @param User   $user       the user for which the operation should be checked
98
     * @param string $permission the name of the permission for which should be checked
99
     * @param string $operation  the name of the operation for which should be checked
100
     *
101
     * @return bool|null true, if the user is allowed to do the operation (ALLOW), false if not (DISALLOW), and null,
102
     *                   if the value is set to inherit
103
     */
104
    public function inherit(User $user, string $permission, string $operation): ?bool
105
    {
106
        //Check if we need to inherit
107
        $allowed = $this->dontInherit($user, $permission, $operation);
108
109
        if (null !== $allowed) {
110
            //Just return the value of the user.
111
            return $allowed;
112
        }
113
114
        /** @var Group $parent */
115
        $parent = $user->getGroup();
116
        while (null !== $parent) { //The top group, has parent == null
117
            //Check if our current element gives a info about disallow/allow
118
            $allowed = $this->dontInherit($parent, $permission, $operation);
0 ignored issues
show
It seems like $parent can also be of type App\Entity\Base\AbstractStructuralDBElement; however, parameter $user of App\Services\UserSystem\...nManager::dontInherit() does only seem to accept App\Security\Interfaces\HasPermissionsInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

118
            $allowed = $this->dontInherit(/** @scrutinizer ignore-type */ $parent, $permission, $operation);
Loading history...
119
            if (null !== $allowed) {
120
                return $allowed;
121
            }
122
            //Else go up in the hierachy.
123
            $parent = $parent->getParent();
124
        }
125
126
        return null; //The inherited value is never resolved. Should be treat as false, in Voters.
127
    }
128
129
    /**
130
     * Sets the new value for the operation.
131
     *
132
     * @param HasPermissionsInterface $user       the user or group for which the value should be changed
133
     * @param string                  $permission the name of the permission that should be changed
134
     * @param string                  $operation  the name of the operation that should be changed
135
     * @param bool|null               $new_val    The new value for the permission. true = ALLOW, false = DISALLOW, null = INHERIT
136
     */
137
    public function setPermission(HasPermissionsInterface $user, string $permission, string $operation, ?bool $new_val): void
138
    {
139
        //Get the permissions from the user
140
        $perm_list = $user->getPermissions();
141
142
        //Check if the permission/operation combination is valid
143
        if (! $this->isValidOperation($permission, $operation)) {
144
            throw new InvalidArgumentException(sprintf('The permission/operation combination "%s.%s" is not valid!', $permission, $operation));
145
        }
146
147
        $perm_list->setPermissionValue($permission, $operation, $new_val);
148
    }
149
150
    /**
151
     * Lists the names of all operations that is supported for the given permission.
152
     *
153
     * If the Permission is not existing at all, a exception is thrown.
154
     *
155
     * This function is useful for the support() function of the voters.
156
     *
157
     * @param string $permission The permission for which the
158
     *
159
     * @return string[] A list of all operations that are supported by the given
160
     */
161
    public function listOperationsForPermission(string $permission): array
162
    {
163
        if (!$this->isValidPermission($permission)) {
164
            throw new InvalidArgumentException(sprintf('A permission with that name is not existing! Got %s.', $permission));
165
        }
166
        $operations = $this->permission_structure['perms'][$permission]['operations'];
167
168
        return array_keys($operations);
169
    }
170
171
    /**
172
     * Checks if the permission with the given name is existing.
173
     *
174
     * @param string $permission the name of the permission which we want to check
175
     *
176
     * @return bool True if a perm with that name is existing. False if not.
177
     */
178
    public function isValidPermission(string $permission): bool
179
    {
180
        return isset($this->permission_structure['perms'][$permission]);
181
    }
182
183
    /**
184
     * Checks if the permission operation combination with the given names is existing.
185
     *
186
     * @param string $permission the name of the permission which should be checked
187
     * @param string $operation  the name of the operation which should be checked
188
     *
189
     * @return bool true if the given permission operation combination is existing
190
     */
191
    public function isValidOperation(string $permission, string $operation): bool
192
    {
193
        return $this->isValidPermission($permission) &&
194
            isset($this->permission_structure['perms'][$permission]['operations'][$operation]);
195
    }
196
197
    /**
198
     * This functions sets all operations mentioned in the alsoSet value of a permission, so that the structure is always valid.
199
     * @param  HasPermissionsInterface $user
200
     * @return void
201
     */
202
    public function ensureCorrectSetOperations(HasPermissionsInterface $user): void
203
    {
204
        //If we have changed anything on the permission structure due to the alsoSet value, this becomes true, so we
205
        //redo the whole process, to ensure that all alsoSet values are set recursively.
206
        $anything_changed = false;
0 ignored issues
show
The assignment to $anything_changed is dead and can be removed.
Loading history...
207
208
        do {
209
            $anything_changed = false; //Reset the variable for the next iteration
210
211
            //Check for each permission and operation, for an alsoSet attribute
212
            foreach ($this->permission_structure['perms'] as $perm_key => $permission) {
213
                foreach ($permission['operations'] as $op_key => $op) {
214
                    if (!empty($op['alsoSet']) &&
215
                        true === $this->dontInherit($user, $perm_key, $op_key)) {
216
                        //Set every op listed in also Set
217
                        foreach ($op['alsoSet'] as $set_also) {
218
                            //If the alsoSet value contains a dot then we set the operation of another permission
219
                            if (false !== strpos($set_also, '.')) {
220
                                [$set_perm, $set_op] = explode('.', $set_also);
221
                            } else {
222
                                //Else we set the operation of the same permission
223
                                [$set_perm, $set_op] = [$perm_key, $set_also];
224
                            }
225
226
                            //Check if we change the value of the permission
227
                            if ($this->dontInherit($user, $set_perm, $set_op) !== true) {
228
                                $this->setPermission($user, $set_perm, $set_op, true);
229
                                //Mark the change, so we redo the whole process
230
                                $anything_changed = true;
231
                            }
232
                        }
233
                    }
234
                }
235
            }
236
        } while($anything_changed);
237
    }
238
239
    /**
240
     * Sets all possible operations of all possible permissions of the given entity to the given value.
241
     * @param  HasPermissionsInterface  $perm_holder
242
     * @param  bool|null  $new_value
243
     * @return void
244
     */
245
    public function setAllPermissions(HasPermissionsInterface $perm_holder, ?bool $new_value): void
246
    {
247
        foreach ($this->permission_structure['perms'] as $perm_key => $permission) {
248
            foreach ($permission['operations'] as $op_key => $op) {
249
                $this->setPermission($perm_holder, $perm_key, $op_key, $new_value);
250
            }
251
        }
252
    }
253
254
    /**
255
     * Sets all operations of the given permissions to the given value.
256
     * Please note that you have to call ensureCorrectSetOperations() after this function, to ensure that all alsoSet values are set.
257
     *
258
     * @param  HasPermissionsInterface  $perm_holder
259
     * @param  string  $permission
260
     * @param  bool|null  $new_value
261
     * @return void
262
     */
263
    public function setAllOperationsOfPermission(HasPermissionsInterface $perm_holder, string $permission, ?bool $new_value): void
264
    {
265
        if (!$this->isValidPermission($permission)) {
266
            throw new InvalidArgumentException(sprintf('A permission with that name is not existing! Got %s.', $permission));
267
        }
268
269
        foreach ($this->permission_structure['perms'][$permission]['operations'] as $op_key => $op) {
270
            $this->setPermission($perm_holder, $permission, $op_key, $new_value);
271
        }
272
    }
273
274
    protected function generatePermissionStructure()
275
    {
276
        $cache = new ConfigCache($this->cache_file, $this->is_debug);
277
278
        //Check if the cache is fresh, else regenerate it.
279
        if (!$cache->isFresh()) {
280
            $permission_file = __DIR__.'/../../../config/permissions.yaml';
281
282
            //Read the permission config file...
283
            $config = Yaml::parse(
284
                file_get_contents($permission_file)
285
            );
286
287
            $configs = [$config];
288
289
            //... And parse it
290
            $processor = new Processor();
291
            $databaseConfiguration = new PermissionsConfiguration();
292
            $processedConfiguration = $processor->processConfiguration(
293
                $databaseConfiguration,
294
                $configs
295
            );
296
297
            //Permission file is our file resource (it is used to invalidate cache)
298
            $resources = [];
299
            $resources[] = new FileResource($permission_file);
300
301
            //Var export the structure and write it to cache file.
302
            $cache->write(
303
                sprintf('<?php return %s;', var_export($processedConfiguration, true)),
304
                $resources);
305
        }
306
307
        //In the most cases we just need to dump the cached PHP file.
308
        return require $this->cache_file;
309
    }
310
}
311