Issues (902)

framework/rbac/DbManager.php (3 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\rbac;
9
10
use Yii;
11
use yii\base\InvalidArgumentException;
12
use yii\base\InvalidCallException;
13
use yii\caching\CacheInterface;
14
use yii\db\Connection;
15
use yii\db\Expression;
16
use yii\db\Query;
17
use yii\di\Instance;
18
19
/**
20
 * DbManager represents an authorization manager that stores authorization information in database.
21
 *
22
 * The database connection is specified by [[db]]. The database schema could be initialized by applying migration:
23
 *
24
 * ```
25
 * yii migrate --migrationPath=@yii/rbac/migrations/
26
 * ```
27
 *
28
 * If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
29
 *
30
 * You may change the names of the tables used to store the authorization and rule data by setting [[itemTable]],
31
 * [[itemChildTable]], [[assignmentTable]] and [[ruleTable]].
32
 *
33
 * For more details and usage information on DbManager, see the [guide article on security authorization](guide:security-authorization).
34
 *
35
 * @author Qiang Xue <[email protected]>
36
 * @author Alexander Kochetov <[email protected]>
37
 * @since 2.0
38
 */
39
class DbManager extends BaseManager
40
{
41
    /**
42
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
43
     * After the DbManager object is created, if you want to change this property, you should only assign it
44
     * with a DB connection object.
45
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
46
     */
47
    public $db = 'db';
48
    /**
49
     * @var string the name of the table storing authorization items. Defaults to "auth_item".
50
     */
51
    public $itemTable = '{{%auth_item}}';
52
    /**
53
     * @var string the name of the table storing authorization item hierarchy. Defaults to "auth_item_child".
54
     */
55
    public $itemChildTable = '{{%auth_item_child}}';
56
    /**
57
     * @var string the name of the table storing authorization item assignments. Defaults to "auth_assignment".
58
     */
59
    public $assignmentTable = '{{%auth_assignment}}';
60
    /**
61
     * @var string the name of the table storing rules. Defaults to "auth_rule".
62
     */
63
    public $ruleTable = '{{%auth_rule}}';
64
    /**
65
     * @var CacheInterface|array|string|null the cache used to improve RBAC performance. This can be one of the following:
66
     *
67
     * - an application component ID (e.g. `cache`)
68
     * - a configuration array
69
     * - a [[\yii\caching\Cache]] object
70
     *
71
     * When this is not set, it means caching is not enabled.
72
     *
73
     * Note that by enabling RBAC cache, all auth items, rules and auth item parent-child relationships will
74
     * be cached and loaded into memory. This will improve the performance of RBAC permission check. However,
75
     * it does require extra memory and as a result may not be appropriate if your RBAC system contains too many
76
     * auth items. You should seek other RBAC implementations (e.g. RBAC based on Redis storage) in this case.
77
     *
78
     * Also note that if you modify RBAC items, rules or parent-child relationships from outside of this component,
79
     * you have to manually call [[invalidateCache()]] to ensure data consistency.
80
     *
81
     * @since 2.0.3
82
     */
83
    public $cache;
84
    /**
85
     * @var string the key used to store RBAC data in cache
86
     * @see cache
87
     * @since 2.0.3
88
     */
89
    public $cacheKey = 'rbac';
90
    /**
91
     * @var string the key used to store user RBAC roles in cache
92
     * @since 2.0.48
93
     */
94
    public $rolesCacheSuffix = 'roles';
95
96
    /**
97
     * @var Item[] all auth items (name => Item)
98
     */
99
    protected $items;
100
    /**
101
     * @var Rule[] all auth rules (name => Rule)
102
     */
103
    protected $rules;
104
    /**
105
     * @var array auth item parent-child relationships (childName => list of parents)
106
     */
107
    protected $parents;
108
    /**
109
     * @var array user assignments (user id => Assignment[])
110
     * @since `protected` since 2.0.38
111
     */
112
    protected $checkAccessAssignments = [];
113
114
115
    /**
116
     * Initializes the application component.
117
     * This method overrides the parent implementation by establishing the database connection.
118
     */
119 250
    public function init()
120
    {
121 250
        parent::init();
122 250
        $this->db = Instance::ensure($this->db, Connection::className());
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

122
        $this->db = Instance::ensure($this->db, /** @scrutinizer ignore-deprecated */ Connection::className());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
123 250
        if ($this->cache !== null) {
124 100
            $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
125
        }
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 30
    public function checkAccess($userId, $permissionName, $params = [])
132
    {
133 30
        if (isset($this->checkAccessAssignments[(string) $userId])) {
134 30
            $assignments = $this->checkAccessAssignments[(string) $userId];
135
        } else {
136 30
            $assignments = $this->getAssignments($userId);
137 30
            $this->checkAccessAssignments[(string) $userId] = $assignments;
138
        }
139
140 30
        if ($this->hasNoAssignments($assignments)) {
141
            return false;
142
        }
143
144 30
        $this->loadFromCache();
145 30
        if ($this->items !== null) {
146 15
            return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
147
        }
148
149 15
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
150
    }
151
152
    /**
153
     * Performs access check for the specified user based on the data loaded from cache.
154
     * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
155
     * @param string|int $user the user ID. This should can be either an integer or a string representing
156
     * the unique identifier of a user. See [[\yii\web\User::id]].
157
     * @param string $itemName the name of the operation that need access check
158
     * @param array $params name-value pairs that would be passed to rules associated
159
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
160
     * which holds the value of `$userId`.
161
     * @param Assignment[] $assignments the assignments to the specified user
162
     * @return bool whether the operations can be performed by the user.
163
     * @since 2.0.3
164
     */
165 15
    protected function checkAccessFromCache($user, $itemName, $params, $assignments)
166
    {
167 15
        if (!isset($this->items[$itemName])) {
168 2
            return false;
169
        }
170
171 15
        $item = $this->items[$itemName];
172
173 15
        Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
174
175 15
        if (!$this->executeRule($user, $item, $params)) {
176 10
            return false;
177
        }
178
179 15
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
180 11
            return true;
181
        }
182
183 11
        if (!empty($this->parents[$itemName])) {
184 7
            foreach ($this->parents[$itemName] as $parent) {
185 7
                if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
186 7
                    return true;
187
                }
188
            }
189
        }
190
191 11
        return false;
192
    }
193
194
    /**
195
     * Performs access check for the specified user.
196
     * This method is internally called by [[checkAccess()]].
197
     * @param string|int $user the user ID. This should can be either an integer or a string representing
198
     * the unique identifier of a user. See [[\yii\web\User::id]].
199
     * @param string $itemName the name of the operation that need access check
200
     * @param array $params name-value pairs that would be passed to rules associated
201
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
202
     * which holds the value of `$userId`.
203
     * @param Assignment[] $assignments the assignments to the specified user
204
     * @return bool whether the operations can be performed by the user.
205
     */
206 15
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
207
    {
208 15
        if (($item = $this->getItem($itemName)) === null) {
209 3
            return false;
210
        }
211
212 15
        Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
213
214 15
        if (!$this->executeRule($user, $item, $params)) {
215 15
            return false;
216
        }
217
218 15
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
219 9
            return true;
220
        }
221
222 9
        $query = new Query();
223 9
        $parents = $query->select(['parent'])
224 9
            ->from($this->itemChildTable)
225 9
            ->where(['child' => $itemName])
226 9
            ->column($this->db);
227 9
        foreach ($parents as $parent) {
228 3
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
229 3
                return true;
230
            }
231
        }
232
233 9
        return false;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 84
    protected function getItem($name)
240
    {
241 84
        if (empty($name)) {
242 3
            return null;
243
        }
244
245 84
        if (!empty($this->items[$name])) {
246 9
            return $this->items[$name];
247
        }
248
249 75
        $row = (new Query())->from($this->itemTable)
250 75
            ->where(['name' => $name])
251 75
            ->one($this->db);
252
253 75
        if ($row === false) {
254 13
            return null;
255
        }
256
257 75
        return $this->populateItem($row);
258
    }
259
260
    /**
261
     * Returns a value indicating whether the database supports cascading update and delete.
262
     * The default implementation will return false for SQLite database and true for all other databases.
263
     * @return bool whether the database supports cascading update and delete.
264
     */
265 35
    protected function supportsCascadeUpdate()
266
    {
267 35
        return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273 225
    protected function addItem($item)
274
    {
275 225
        $time = time();
276 225
        if ($item->createdAt === null) {
277 225
            $item->createdAt = $time;
278
        }
279 225
        if ($item->updatedAt === null) {
280 225
            $item->updatedAt = $time;
281
        }
282 225
        $this->db->createCommand()
283 225
            ->insert($this->itemTable, [
284 225
                'name' => $item->name,
285 225
                'type' => $item->type,
286 225
                'description' => $item->description,
287 225
                'rule_name' => $item->ruleName,
288 225
                'data' => $item->data === null ? null : serialize($item->data),
289 225
                'created_at' => $item->createdAt,
290 225
                'updated_at' => $item->updatedAt,
291 225
            ])->execute();
292
293 225
        $this->invalidateCache();
294
295 225
        return true;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301 5
    protected function removeItem($item)
302
    {
303 5
        if (!$this->supportsCascadeUpdate()) {
304 1
            $this->db->createCommand()
305 1
                ->delete($this->itemChildTable, ['or', '[[parent]]=:parent', '[[child]]=:child'], [':parent' => $item->name, ':child' => $item->name])
306 1
                ->execute();
307 1
            $this->db->createCommand()
308 1
                ->delete($this->assignmentTable, ['item_name' => $item->name])
309 1
                ->execute();
310
        }
311
312 5
        $this->db->createCommand()
313 5
            ->delete($this->itemTable, ['name' => $item->name])
314 5
            ->execute();
315
316 5
        $this->invalidateCache();
317
318 5
        return true;
319
    }
320
321
    /**
322
     * {@inheritdoc}
323
     */
324 15
    protected function updateItem($name, $item)
325
    {
326 15
        if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
327 3
            $this->db->createCommand()
328 3
                ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
329 3
                ->execute();
330 3
            $this->db->createCommand()
331 3
                ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
332 3
                ->execute();
333 3
            $this->db->createCommand()
334 3
                ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
335 3
                ->execute();
336
        }
337
338 15
        $item->updatedAt = time();
339
340 15
        $this->db->createCommand()
341 15
            ->update($this->itemTable, [
342 15
                'name' => $item->name,
343 15
                'description' => $item->description,
344 15
                'rule_name' => $item->ruleName,
345 15
                'data' => $item->data === null ? null : serialize($item->data),
346 15
                'updated_at' => $item->updatedAt,
347 15
            ], [
348 15
                'name' => $name,
349 15
            ])->execute();
350
351 15
        $this->invalidateCache();
352
353 15
        return true;
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 130
    protected function addRule($rule)
360
    {
361 130
        $time = time();
362 130
        if ($rule->createdAt === null) {
363 130
            $rule->createdAt = $time;
364
        }
365 130
        if ($rule->updatedAt === null) {
366 130
            $rule->updatedAt = $time;
367
        }
368 130
        $this->db->createCommand()
369 130
            ->insert($this->ruleTable, [
370 130
                'name' => $rule->name,
371 130
                'data' => serialize($rule),
372 130
                'created_at' => $rule->createdAt,
373 130
                'updated_at' => $rule->updatedAt,
374 130
            ])->execute();
375
376 130
        $this->invalidateCache();
377
378 130
        return true;
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384 5
    protected function updateRule($name, $rule)
385
    {
386 5
        if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
387 1
            $this->db->createCommand()
388 1
                ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
389 1
                ->execute();
390
        }
391
392 5
        $rule->updatedAt = time();
393
394 5
        $this->db->createCommand()
395 5
            ->update($this->ruleTable, [
396 5
                'name' => $rule->name,
397 5
                'data' => serialize($rule),
398 5
                'updated_at' => $rule->updatedAt,
399 5
            ], [
400 5
                'name' => $name,
401 5
            ])->execute();
402
403 5
        $this->invalidateCache();
404
405 5
        return true;
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411 5
    protected function removeRule($rule)
412
    {
413 5
        if (!$this->supportsCascadeUpdate()) {
414 1
            $this->db->createCommand()
415 1
                ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
416 1
                ->execute();
417
        }
418
419 5
        $this->db->createCommand()
420 5
            ->delete($this->ruleTable, ['name' => $rule->name])
421 5
            ->execute();
422
423 5
        $this->invalidateCache();
424
425 5
        return true;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 20
    protected function getItems($type)
432
    {
433 20
        $query = (new Query())
434 20
            ->from($this->itemTable)
435 20
            ->where(['type' => $type]);
436
437 20
        $items = [];
438 20
        foreach ($query->all($this->db) as $row) {
439 20
            $items[$row['name']] = $this->populateItem($row);
440
        }
441
442 20
        return $items;
443
    }
444
445
    /**
446
     * Populates an auth item with the data fetched from database.
447
     * @param array $row the data from the auth item table
448
     * @return Item the populated auth item instance (either Role or Permission)
449
     */
450 165
    protected function populateItem($row)
451
    {
452 165
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

452
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : /** @scrutinizer ignore-deprecated */ Role::className();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
453
454 165
        if (!isset($row['data']) || ($data = @unserialize(is_resource($row['data']) ? stream_get_contents($row['data']) : $row['data'])) === false) {
455 165
            $data = null;
456
        }
457
458 165
        return new $class([
459 165
            'name' => $row['name'],
460 165
            'type' => $row['type'],
461 165
            'description' => $row['description'],
462 165
            'ruleName' => $row['rule_name'] ?: null,
463 165
            'data' => $data,
464 165
            'createdAt' => $row['created_at'],
465 165
            'updatedAt' => $row['updated_at'],
466 165
        ]);
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     * The roles returned by this method include the roles assigned via [[$defaultRoles]].
472
     */
473 30
    public function getRolesByUser($userId)
474
    {
475 30
        if ($this->isEmptyUserId($userId)) {
476 5
            return [];
477
        }
478
479 25
        if ($this->cache !== null) {
480 13
            $data = $this->cache->get($this->getUserRolesCacheKey($userId));
481
482 13
            if ($data !== false) {
483
                return $data;
484
            }
485
        }
486
487 25
        $query = (new Query())->select('b.*')
488 25
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
489 25
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
490 25
            ->andWhere(['a.user_id' => (string) $userId])
491 25
            ->andWhere(['b.type' => Item::TYPE_ROLE]);
492
493 25
        $roles = $this->getDefaultRoleInstances();
494 25
        foreach ($query->all($this->db) as $row) {
495 25
            $roles[$row['name']] = $this->populateItem($row);
496
        }
497
498 25
        if ($this->cache !== null) {
499 13
            $this->cacheUserRolesData($userId, $roles);
500
        }
501
502 25
        return $roles;
503
    }
504
505
    /**
506
     * {@inheritdoc}
507
     */
508 5
    public function getChildRoles($roleName)
509
    {
510 5
        $role = $this->getRole($roleName);
511
512 5
        if ($role === null) {
513
            throw new InvalidArgumentException("Role \"$roleName\" not found.");
514
        }
515
516 5
        $result = [];
517 5
        $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);
518
519 5
        $roles = [$roleName => $role];
520
521 5
        $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
522 5
            return array_key_exists($roleItem->name, $result);
523 5
        });
524
525 5
        return $roles;
526
    }
527
528
    /**
529
     * {@inheritdoc}
530
     */
531 5
    public function getPermissionsByRole($roleName)
532
    {
533 5
        $childrenList = $this->getChildrenList();
534 5
        $result = [];
535 5
        $this->getChildrenRecursive($roleName, $childrenList, $result);
536 5
        if (empty($result)) {
537
            return [];
538
        }
539 5
        $query = (new Query())->from($this->itemTable)->where([
540 5
            'type' => Item::TYPE_PERMISSION,
541 5
            'name' => array_keys($result),
542 5
        ]);
543 5
        $permissions = [];
544 5
        foreach ($query->all($this->db) as $row) {
545 5
            $permissions[$row['name']] = $this->populateItem($row);
546
        }
547
548 5
        return $permissions;
549
    }
550
551
    /**
552
     * {@inheritdoc}
553
     */
554 20
    public function getPermissionsByUser($userId)
555
    {
556 20
        if ($this->isEmptyUserId($userId)) {
557 5
            return [];
558
        }
559
560 15
        $directPermission = $this->getDirectPermissionsByUser($userId);
561 15
        $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
562
563 15
        return array_merge($directPermission, $inheritedPermission);
564
    }
565
566
    /**
567
     * Returns all permissions that are directly assigned to user.
568
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
569
     * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
570
     * @since 2.0.7
571
     */
572 15
    protected function getDirectPermissionsByUser($userId)
573
    {
574 15
        $query = (new Query())->select('b.*')
575 15
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
576 15
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
577 15
            ->andWhere(['a.user_id' => (string) $userId])
578 15
            ->andWhere(['b.type' => Item::TYPE_PERMISSION]);
579
580 15
        $permissions = [];
581 15
        foreach ($query->all($this->db) as $row) {
582 15
            $permissions[$row['name']] = $this->populateItem($row);
583
        }
584
585 15
        return $permissions;
586
    }
587
588
    /**
589
     * Returns all permissions that the user inherits from the roles assigned to him.
590
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
591
     * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
592
     * @since 2.0.7
593
     */
594 15
    protected function getInheritedPermissionsByUser($userId)
595
    {
596 15
        $query = (new Query())->select('item_name')
597 15
            ->from($this->assignmentTable)
598 15
            ->where(['user_id' => (string) $userId]);
599
600 15
        $childrenList = $this->getChildrenList();
601 15
        $result = [];
602 15
        foreach ($query->column($this->db) as $roleName) {
603 15
            $this->getChildrenRecursive($roleName, $childrenList, $result);
604
        }
605
606 15
        if (empty($result)) {
607 10
            return [];
608
        }
609
610 5
        $query = (new Query())->from($this->itemTable)->where([
611 5
            'type' => Item::TYPE_PERMISSION,
612 5
            'name' => array_keys($result),
613 5
        ]);
614 5
        $permissions = [];
615 5
        foreach ($query->all($this->db) as $row) {
616 5
            $permissions[$row['name']] = $this->populateItem($row);
617
        }
618
619 5
        return $permissions;
620
    }
621
622
    /**
623
     * Returns the children for every parent.
624
     * @return array the children list. Each array key is a parent item name,
625
     * and the corresponding array value is a list of child item names.
626
     */
627 25
    protected function getChildrenList()
628
    {
629 25
        $query = (new Query())->from($this->itemChildTable);
630 25
        $parents = [];
631 25
        foreach ($query->all($this->db) as $row) {
632 15
            $parents[$row['parent']][] = $row['child'];
633
        }
634
635 25
        return $parents;
636
    }
637
638
    /**
639
     * Recursively finds all children and grand children of the specified item.
640
     * @param string $name the name of the item whose children are to be looked for.
641
     * @param array $childrenList the child list built via [[getChildrenList()]]
642
     * @param array $result the children and grand children (in array keys)
643
     */
644 25
    protected function getChildrenRecursive($name, $childrenList, &$result)
645
    {
646 25
        if (isset($childrenList[$name])) {
647 15
            foreach ($childrenList[$name] as $child) {
648 15
                $result[$child] = true;
649 15
                $this->getChildrenRecursive($child, $childrenList, $result);
650
            }
651
        }
652
    }
653
654
    /**
655
     * {@inheritdoc}
656
     */
657 125
    public function getRule($name)
658
    {
659 125
        if ($this->rules !== null) {
660 10
            return isset($this->rules[$name]) ? $this->rules[$name] : null;
661
        }
662
663 125
        $row = (new Query())->select(['data'])
664 125
            ->from($this->ruleTable)
665 125
            ->where(['name' => $name])
666 125
            ->one($this->db);
667 125
        if ($row === false) {
668 20
            return null;
669
        }
670 125
        $data = $row['data'];
671 125
        if (is_resource($data)) {
672 50
            $data = stream_get_contents($data);
673
        }
674 125
        if (!$data) {
675
            return null;
676
        }
677 125
        return unserialize($data);
678
    }
679
680
    /**
681
     * {@inheritdoc}
682
     */
683 25
    public function getRules()
684
    {
685 25
        if ($this->rules !== null) {
686
            return $this->rules;
687
        }
688
689 25
        $query = (new Query())->from($this->ruleTable);
690
691 25
        $rules = [];
692 25
        foreach ($query->all($this->db) as $row) {
693 15
            $data = $row['data'];
694 15
            if (is_resource($data)) {
695 6
                $data = stream_get_contents($data);
696
            }
697 15
            if ($data) {
698 15
                $rules[$row['name']] = unserialize($data);
699
            }
700
        }
701
702 25
        return $rules;
703
    }
704
705
    /**
706
     * {@inheritdoc}
707
     */
708 15
    public function getAssignment($roleName, $userId)
709
    {
710 15
        if ($this->isEmptyUserId($userId)) {
711 5
            return null;
712
        }
713
714 10
        $row = (new Query())->from($this->assignmentTable)
715 10
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
716 10
            ->one($this->db);
717
718 10
        if ($row === false) {
719
            return null;
720
        }
721
722 10
        return new Assignment([
723 10
            'userId' => $row['user_id'],
724 10
            'roleName' => $row['item_name'],
725 10
            'createdAt' => $row['created_at'],
726 10
        ]);
727
    }
728
729
    /**
730
     * {@inheritdoc}
731
     */
732 50
    public function getAssignments($userId)
733
    {
734 50
        if ($this->isEmptyUserId($userId)) {
735 5
            return [];
736
        }
737
738 45
        $query = (new Query())
739 45
            ->from($this->assignmentTable)
740 45
            ->where(['user_id' => (string) $userId]);
741
742 45
        $assignments = [];
743 45
        foreach ($query->all($this->db) as $row) {
744 35
            $assignments[$row['item_name']] = new Assignment([
745 35
                'userId' => $row['user_id'],
746 35
                'roleName' => $row['item_name'],
747 35
                'createdAt' => $row['created_at'],
748 35
            ]);
749
        }
750
751 45
        return $assignments;
752
    }
753
754
    /**
755
     * {@inheritdoc}
756
     * @since 2.0.8
757
     */
758 5
    public function canAddChild($parent, $child)
759
    {
760 5
        return !$this->detectLoop($parent, $child);
761
    }
762
763
    /**
764
     * {@inheritdoc}
765
     */
766 105
    public function addChild($parent, $child)
767
    {
768 105
        if ($parent->name === $child->name) {
769
            throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
770
        }
771
772 105
        if ($parent instanceof Permission && $child instanceof Role) {
773
            throw new InvalidArgumentException('Cannot add a role as a child of a permission.');
774
        }
775
776 105
        if ($this->detectLoop($parent, $child)) {
777
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
778
        }
779
780 105
        $this->db->createCommand()
781 105
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
782 105
            ->execute();
783
784 105
        $this->invalidateCache();
785
786 105
        return true;
787
    }
788
789
    /**
790
     * {@inheritdoc}
791
     */
792
    public function removeChild($parent, $child)
793
    {
794
        $result = $this->db->createCommand()
795
            ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
796
            ->execute() > 0;
797
798
        $this->invalidateCache();
799
800
        return $result;
801
    }
802
803
    /**
804
     * {@inheritdoc}
805
     */
806
    public function removeChildren($parent)
807
    {
808
        $result = $this->db->createCommand()
809
            ->delete($this->itemChildTable, ['parent' => $parent->name])
810
            ->execute() > 0;
811
812
        $this->invalidateCache();
813
814
        return $result;
815
    }
816
817
    /**
818
     * {@inheritdoc}
819
     */
820
    public function hasChild($parent, $child)
821
    {
822
        return (new Query())
823
            ->from($this->itemChildTable)
824
            ->where(['parent' => $parent->name, 'child' => $child->name])
825
            ->one($this->db) !== false;
826
    }
827
828
    /**
829
     * {@inheritdoc}
830
     */
831 105
    public function getChildren($name)
832
    {
833 105
        $query = (new Query())
834 105
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
835 105
            ->from([$this->itemTable, $this->itemChildTable])
836 105
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
837
838 105
        $children = [];
839 105
        foreach ($query->all($this->db) as $row) {
840 105
            $children[$row['name']] = $this->populateItem($row);
841
        }
842
843 105
        return $children;
844
    }
845
846
    /**
847
     * Checks whether there is a loop in the authorization item hierarchy.
848
     * @param Item $parent the parent item
849
     * @param Item $child the child item to be added to the hierarchy
850
     * @return bool whether a loop exists
851
     */
852 105
    protected function detectLoop($parent, $child)
853
    {
854 105
        if ($child->name === $parent->name) {
855 5
            return true;
856
        }
857 105
        foreach ($this->getChildren($child->name) as $grandchild) {
858 100
            if ($this->detectLoop($parent, $grandchild)) {
859 5
                return true;
860
            }
861
        }
862
863 105
        return false;
864
    }
865
866
    /**
867
     * {@inheritdoc}
868
     */
869 215
    public function assign($role, $userId)
870
    {
871 215
        $assignment = new Assignment([
872 215
            'userId' => $userId,
873 215
            'roleName' => $role->name,
874 215
            'createdAt' => time(),
875 215
        ]);
876
877 215
        $this->db->createCommand()
878 215
            ->insert($this->assignmentTable, [
879 215
                'user_id' => $assignment->userId,
880 215
                'item_name' => $assignment->roleName,
881 215
                'created_at' => $assignment->createdAt,
882 215
            ])->execute();
883
884 215
        unset($this->checkAccessAssignments[(string) $userId]);
885
886 215
        $this->invalidateCache();
887
888 215
        return $assignment;
889
    }
890
891
    /**
892
     * {@inheritdoc}
893
     */
894 30
    public function revoke($role, $userId)
895
    {
896 30
        if ($this->isEmptyUserId($userId)) {
897 5
            return false;
898
        }
899
900 25
        unset($this->checkAccessAssignments[(string) $userId]);
901 25
        $result = $this->db->createCommand()
902 25
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
903 25
            ->execute() > 0;
904
905 25
        $this->invalidateCache();
906
907 25
        return $result;
908
    }
909
910
    /**
911
     * {@inheritdoc}
912
     */
913 20
    public function revokeAll($userId)
914
    {
915 20
        if ($this->isEmptyUserId($userId)) {
916 5
            return false;
917
        }
918
919 15
        unset($this->checkAccessAssignments[(string) $userId]);
920 15
        $result = $this->db->createCommand()
921 15
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
922 15
            ->execute() > 0;
923
924 15
        $this->invalidateCache();
925
926 15
        return $result;
927
    }
928
929
    /**
930
     * {@inheritdoc}
931
     */
932 250
    public function removeAll()
933
    {
934 250
        $this->removeAllAssignments();
935 250
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
936 250
        $this->db->createCommand()->delete($this->itemTable)->execute();
937 250
        $this->db->createCommand()->delete($this->ruleTable)->execute();
938 250
        $this->invalidateCache();
939
    }
940
941
    /**
942
     * {@inheritdoc}
943
     */
944 5
    public function removeAllPermissions()
945
    {
946 5
        $this->removeAllItems(Item::TYPE_PERMISSION);
947
    }
948
949
    /**
950
     * {@inheritdoc}
951
     */
952 5
    public function removeAllRoles()
953
    {
954 5
        $this->removeAllItems(Item::TYPE_ROLE);
955
    }
956
957
    /**
958
     * Removes all auth items of the specified type.
959
     * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
960
     */
961 10
    protected function removeAllItems($type)
962
    {
963 10
        if (!$this->supportsCascadeUpdate()) {
964 2
            $names = (new Query())
965 2
                ->select(['name'])
966 2
                ->from($this->itemTable)
967 2
                ->where(['type' => $type])
968 2
                ->column($this->db);
969 2
            if (empty($names)) {
970
                return;
971
            }
972 2
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
973 2
            $this->db->createCommand()
974 2
                ->delete($this->itemChildTable, [$key => $names])
975 2
                ->execute();
976 2
            $this->db->createCommand()
977 2
                ->delete($this->assignmentTable, ['item_name' => $names])
978 2
                ->execute();
979
        }
980 10
        $this->db->createCommand()
981 10
            ->delete($this->itemTable, ['type' => $type])
982 10
            ->execute();
983
984 10
        $this->invalidateCache();
985
    }
986
987
    /**
988
     * {@inheritdoc}
989
     */
990 5
    public function removeAllRules()
991
    {
992 5
        if (!$this->supportsCascadeUpdate()) {
993 1
            $this->db->createCommand()
994 1
                ->update($this->itemTable, ['rule_name' => null])
995 1
                ->execute();
996
        }
997
998 5
        $this->db->createCommand()->delete($this->ruleTable)->execute();
999
1000 5
        $this->invalidateCache();
1001
    }
1002
1003
    /**
1004
     * {@inheritdoc}
1005
     */
1006 250
    public function removeAllAssignments()
1007
    {
1008 250
        $this->checkAccessAssignments = [];
1009 250
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
1010
    }
1011
1012 250
    public function invalidateCache()
1013
    {
1014 250
        if ($this->cache !== null) {
1015 106
            $this->cache->delete($this->cacheKey);
1016 106
            $this->items = null;
1017 106
            $this->rules = null;
1018 106
            $this->parents = null;
1019
1020 106
            $cachedUserIds = $this->cache->get($this->getUserRolesCachedSetKey());
1021
1022 106
            if ($cachedUserIds !== false) {
1023 13
                foreach ($cachedUserIds as $userId) {
1024 13
                    $this->cache->delete($this->getUserRolesCacheKey($userId));
1025
                }
1026
1027 13
                $this->cache->delete($this->getUserRolesCachedSetKey());
1028
            }
1029
        }
1030 250
        $this->checkAccessAssignments = [];
1031
    }
1032
1033 30
    public function loadFromCache()
1034
    {
1035 30
        if ($this->items !== null || !$this->cache instanceof CacheInterface) {
1036 30
            return;
1037
        }
1038
1039 15
        $data = $this->cache->get($this->cacheKey);
1040 15
        if (is_array($data) && isset($data[0], $data[1], $data[2])) {
1041
            list($this->items, $this->rules, $this->parents) = $data;
1042
            return;
1043
        }
1044
1045 15
        $query = (new Query())->from($this->itemTable);
1046 15
        $this->items = [];
1047 15
        foreach ($query->all($this->db) as $row) {
1048 15
            $this->items[$row['name']] = $this->populateItem($row);
1049
        }
1050
1051 15
        $query = (new Query())->from($this->ruleTable);
1052 15
        $this->rules = [];
1053 15
        foreach ($query->all($this->db) as $row) {
1054 15
            $data = $row['data'];
1055 15
            if (is_resource($data)) {
1056 7
                $data = stream_get_contents($data);
1057
            }
1058 15
            if ($data) {
1059 15
                $this->rules[$row['name']] = unserialize($data);
1060
            }
1061
        }
1062
1063 15
        $query = (new Query())->from($this->itemChildTable);
1064 15
        $this->parents = [];
1065 15
        foreach ($query->all($this->db) as $row) {
1066 7
            if (isset($this->items[$row['child']])) {
1067 7
                $this->parents[$row['child']][] = $row['parent'];
1068
            }
1069
        }
1070
1071 15
        $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
1072
    }
1073
1074
    /**
1075
     * Returns all role assignment information for the specified role.
1076
     * @param string $roleName
1077
     * @return string[] the ids. An empty array will be
1078
     * returned if role is not assigned to any user.
1079
     * @since 2.0.7
1080
     */
1081 5
    public function getUserIdsByRole($roleName)
1082
    {
1083 5
        if (empty($roleName)) {
1084
            return [];
1085
        }
1086
1087 5
        return (new Query())->select('[[user_id]]')
1088 5
            ->from($this->assignmentTable)
1089 5
            ->where(['item_name' => $roleName])->column($this->db);
1090
    }
1091
1092
    /**
1093
     * Check whether $userId is empty.
1094
     * @param mixed $userId
1095
     * @return bool
1096
     * @since 2.0.26
1097
     */
1098 145
    protected function isEmptyUserId($userId)
1099
    {
1100 145
        return !isset($userId) || $userId === '';
1101
    }
1102
1103 13
    private function getUserRolesCacheKey($userId)
1104
    {
1105 13
        return $this->cacheKey . $this->rolesCacheSuffix . $userId;
1106
    }
1107
1108 106
    private function getUserRolesCachedSetKey()
1109
    {
1110 106
        return $this->cacheKey . $this->rolesCacheSuffix;
1111
    }
1112
1113 13
    private function cacheUserRolesData($userId, $roles)
1114
    {
1115 13
        $cachedUserIds = $this->cache->get($this->getUserRolesCachedSetKey());
0 ignored issues
show
The method get() does not exist on null. ( Ignorable by Annotation )

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

1115
        /** @scrutinizer ignore-call */ 
1116
        $cachedUserIds = $this->cache->get($this->getUserRolesCachedSetKey());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1116
1117 13
        if ($cachedUserIds === false) {
1118 13
            $cachedUserIds = [];
1119
        }
1120
1121 13
        $cachedUserIds[] = $userId;
1122
1123 13
        $this->cache->set($this->getUserRolesCacheKey($userId), $roles);
1124 13
        $this->cache->set($this->getUserRolesCachedSetKey(), $cachedUserIds);
1125
    }
1126
}
1127