DbManager::getChildren()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 13
ccs 0
cts 9
cp 0
crap 6
rs 10
c 0
b 0
f 0
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
    public function init()
120
    {
121
        parent::init();
122
        $this->db = Instance::ensure($this->db, Connection::class);
123
        if ($this->cache !== null) {
124
            $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
125
        }
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function checkAccess($userId, $permissionName, $params = [])
132
    {
133
        if (isset($this->checkAccessAssignments[(string) $userId])) {
134
            $assignments = $this->checkAccessAssignments[(string) $userId];
135
        } else {
136
            $assignments = $this->getAssignments($userId);
137
            $this->checkAccessAssignments[(string) $userId] = $assignments;
138
        }
139
140
        if ($this->hasNoAssignments($assignments)) {
141
            return false;
142
        }
143
144
        $this->loadFromCache();
145
        if ($this->items !== null) {
146
            return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
147
        }
148
149
        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
    protected function checkAccessFromCache($user, $itemName, $params, $assignments)
166
    {
167
        if (!isset($this->items[$itemName])) {
168
            return false;
169
        }
170
171
        $item = $this->items[$itemName];
172
173
        Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
174
175
        if (!$this->executeRule($user, $item, $params)) {
176
            return false;
177
        }
178
179
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
180
            return true;
181
        }
182
183
        if (!empty($this->parents[$itemName])) {
184
            foreach ($this->parents[$itemName] as $parent) {
185
                if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
186
                    return true;
187
                }
188
            }
189
        }
190
191
        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
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
207
    {
208
        if (($item = $this->getItem($itemName)) === null) {
209
            return false;
210
        }
211
212
        Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
213
214
        if (!$this->executeRule($user, $item, $params)) {
215
            return false;
216
        }
217
218
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
219
            return true;
220
        }
221
222
        $query = new Query();
223
        $parents = $query->select(['parent'])
224
            ->from($this->itemChildTable)
225
            ->where(['child' => $itemName])
226
            ->column($this->db);
227
        foreach ($parents as $parent) {
228
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
229
                return true;
230
            }
231
        }
232
233
        return false;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    protected function getItem($name)
240
    {
241
        if (empty($name)) {
242
            return null;
243
        }
244
245
        if (!empty($this->items[$name])) {
246
            return $this->items[$name];
247
        }
248
249
        $row = (new Query())->from($this->itemTable)
250
            ->where(['name' => $name])
251
            ->one($this->db);
252
253
        if ($row === false) {
254
            return null;
255
        }
256
257
        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
    protected function supportsCascadeUpdate()
266
    {
267
        return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    protected function addItem($item)
274
    {
275
        $time = time();
276
        if ($item->createdAt === null) {
277
            $item->createdAt = $time;
278
        }
279
        if ($item->updatedAt === null) {
280
            $item->updatedAt = $time;
281
        }
282
        $this->db->createCommand()
283
            ->insert($this->itemTable, [
284
                'name' => $item->name,
285
                'type' => $item->type,
286
                'description' => $item->description,
287
                'rule_name' => $item->ruleName,
288
                'data' => $item->data === null ? null : serialize($item->data),
289
                'created_at' => $item->createdAt,
290
                'updated_at' => $item->updatedAt,
291
            ])->execute();
292
293
        $this->invalidateCache();
294
295
        return true;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    protected function removeItem($item)
302
    {
303
        if (!$this->supportsCascadeUpdate()) {
304
            $this->db->createCommand()
305
                ->delete($this->itemChildTable, ['or', '[[parent]]=:parent', '[[child]]=:child'], [':parent' => $item->name, ':child' => $item->name])
306
                ->execute();
307
            $this->db->createCommand()
308
                ->delete($this->assignmentTable, ['item_name' => $item->name])
309
                ->execute();
310
        }
311
312
        $this->db->createCommand()
313
            ->delete($this->itemTable, ['name' => $item->name])
314
            ->execute();
315
316
        $this->invalidateCache();
317
318
        return true;
319
    }
320
321
    /**
322
     * {@inheritdoc}
323
     */
324
    protected function updateItem($name, $item)
325
    {
326
        if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
327
            $this->db->createCommand()
328
                ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
329
                ->execute();
330
            $this->db->createCommand()
331
                ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
332
                ->execute();
333
            $this->db->createCommand()
334
                ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
335
                ->execute();
336
        }
337
338
        $item->updatedAt = time();
339
340
        $this->db->createCommand()
341
            ->update($this->itemTable, [
342
                'name' => $item->name,
343
                'description' => $item->description,
344
                'rule_name' => $item->ruleName,
345
                'data' => $item->data === null ? null : serialize($item->data),
346
                'updated_at' => $item->updatedAt,
347
            ], [
348
                'name' => $name,
349
            ])->execute();
350
351
        $this->invalidateCache();
352
353
        return true;
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359
    protected function addRule($rule)
360
    {
361
        $time = time();
362
        if ($rule->createdAt === null) {
363
            $rule->createdAt = $time;
364
        }
365
        if ($rule->updatedAt === null) {
366
            $rule->updatedAt = $time;
367
        }
368
        $this->db->createCommand()
369
            ->insert($this->ruleTable, [
370
                'name' => $rule->name,
371
                'data' => serialize($rule),
372
                'created_at' => $rule->createdAt,
373
                'updated_at' => $rule->updatedAt,
374
            ])->execute();
375
376
        $this->invalidateCache();
377
378
        return true;
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384
    protected function updateRule($name, $rule)
385
    {
386
        if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
387
            $this->db->createCommand()
388
                ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
389
                ->execute();
390
        }
391
392
        $rule->updatedAt = time();
393
394
        $this->db->createCommand()
395
            ->update($this->ruleTable, [
396
                'name' => $rule->name,
397
                'data' => serialize($rule),
398
                'updated_at' => $rule->updatedAt,
399
            ], [
400
                'name' => $name,
401
            ])->execute();
402
403
        $this->invalidateCache();
404
405
        return true;
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411
    protected function removeRule($rule)
412
    {
413
        if (!$this->supportsCascadeUpdate()) {
414
            $this->db->createCommand()
415
                ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
416
                ->execute();
417
        }
418
419
        $this->db->createCommand()
420
            ->delete($this->ruleTable, ['name' => $rule->name])
421
            ->execute();
422
423
        $this->invalidateCache();
424
425
        return true;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431
    protected function getItems($type)
432
    {
433
        $query = (new Query())
434
            ->from($this->itemTable)
435
            ->where(['type' => $type]);
436
437
        $items = [];
438
        foreach ($query->all($this->db) as $row) {
439
            $items[$row['name']] = $this->populateItem($row);
440
        }
441
442
        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
    protected function populateItem($row)
451
    {
452
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::class : Role::class;
453
454
        if (!isset($row['data']) || ($data = @unserialize(is_resource($row['data']) ? stream_get_contents($row['data']) : $row['data'])) === false) {
455
            $data = null;
456
        }
457
458
        return new $class([
459
            'name' => $row['name'],
460
            'type' => $row['type'],
461
            'description' => $row['description'],
462
            'ruleName' => $row['rule_name'] ?: null,
463
            'data' => $data,
464
            'createdAt' => $row['created_at'],
465
            'updatedAt' => $row['updated_at'],
466
        ]);
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     * The roles returned by this method include the roles assigned via [[$defaultRoles]].
472
     */
473
    public function getRolesByUser($userId)
474
    {
475
        if ($this->isEmptyUserId($userId)) {
476
            return [];
477
        }
478
479
        if ($this->cache !== null) {
480
            $data = $this->cache->get($this->getUserRolesCacheKey($userId));
481
482
            if ($data !== false) {
483
                return $data;
484
            }
485
        }
486
487
        $query = (new Query())->select('b.*')
488
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
489
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
490
            ->andWhere(['a.user_id' => (string) $userId])
491
            ->andWhere(['b.type' => Item::TYPE_ROLE]);
492
493
        $roles = $this->getDefaultRoleInstances();
494
        foreach ($query->all($this->db) as $row) {
495
            $roles[$row['name']] = $this->populateItem($row);
496
        }
497
498
        if ($this->cache !== null) {
499
            $this->cacheUserRolesData($userId, $roles);
500
        }
501
502
        return $roles;
503
    }
504
505
    /**
506
     * {@inheritdoc}
507
     */
508
    public function getChildRoles($roleName)
509
    {
510
        $role = $this->getRole($roleName);
511
512
        if ($role === null) {
513
            throw new InvalidArgumentException("Role \"$roleName\" not found.");
514
        }
515
516
        $result = [];
517
        $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);
518
519
        $roles = [$roleName => $role];
520
521
        $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
522
            return array_key_exists($roleItem->name, $result);
523
        });
524
525
        return $roles;
526
    }
527
528
    /**
529
     * {@inheritdoc}
530
     */
531
    public function getPermissionsByRole($roleName)
532
    {
533
        $childrenList = $this->getChildrenList();
534
        $result = [];
535
        $this->getChildrenRecursive($roleName, $childrenList, $result);
536
        if (empty($result)) {
537
            return [];
538
        }
539
        $query = (new Query())->from($this->itemTable)->where([
540
            'type' => Item::TYPE_PERMISSION,
541
            'name' => array_keys($result),
542
        ]);
543
        $permissions = [];
544
        foreach ($query->all($this->db) as $row) {
545
            $permissions[$row['name']] = $this->populateItem($row);
546
        }
547
548
        return $permissions;
549
    }
550
551
    /**
552
     * {@inheritdoc}
553
     */
554
    public function getPermissionsByUser($userId)
555
    {
556
        if ($this->isEmptyUserId($userId)) {
557
            return [];
558
        }
559
560
        $directPermission = $this->getDirectPermissionsByUser($userId);
561
        $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
562
563
        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
    protected function getDirectPermissionsByUser($userId)
573
    {
574
        $query = (new Query())->select('b.*')
575
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
576
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
577
            ->andWhere(['a.user_id' => (string) $userId])
578
            ->andWhere(['b.type' => Item::TYPE_PERMISSION]);
579
580
        $permissions = [];
581
        foreach ($query->all($this->db) as $row) {
582
            $permissions[$row['name']] = $this->populateItem($row);
583
        }
584
585
        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
    protected function getInheritedPermissionsByUser($userId)
595
    {
596
        $query = (new Query())->select('item_name')
597
            ->from($this->assignmentTable)
598
            ->where(['user_id' => (string) $userId]);
599
600
        $childrenList = $this->getChildrenList();
601
        $result = [];
602
        foreach ($query->column($this->db) as $roleName) {
603
            $this->getChildrenRecursive($roleName, $childrenList, $result);
604
        }
605
606
        if (empty($result)) {
607
            return [];
608
        }
609
610
        $query = (new Query())->from($this->itemTable)->where([
611
            'type' => Item::TYPE_PERMISSION,
612
            'name' => array_keys($result),
613
        ]);
614
        $permissions = [];
615
        foreach ($query->all($this->db) as $row) {
616
            $permissions[$row['name']] = $this->populateItem($row);
617
        }
618
619
        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
    protected function getChildrenList()
628
    {
629
        $query = (new Query())->from($this->itemChildTable);
630
        $parents = [];
631
        foreach ($query->all($this->db) as $row) {
632
            $parents[$row['parent']][] = $row['child'];
633
        }
634
635
        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
    protected function getChildrenRecursive($name, $childrenList, &$result)
645
    {
646
        if (isset($childrenList[$name])) {
647
            foreach ($childrenList[$name] as $child) {
648
                $result[$child] = true;
649
                $this->getChildrenRecursive($child, $childrenList, $result);
650
            }
651
        }
652
    }
653
654
    /**
655
     * {@inheritdoc}
656
     */
657
    public function getRule($name)
658
    {
659
        if ($this->rules !== null) {
660
            return isset($this->rules[$name]) ? $this->rules[$name] : null;
661
        }
662
663
        $row = (new Query())->select(['data'])
664
            ->from($this->ruleTable)
665
            ->where(['name' => $name])
666
            ->one($this->db);
667
        if ($row === false) {
668
            return null;
669
        }
670
        $data = $row['data'];
671
        if (is_resource($data)) {
672
            $data = stream_get_contents($data);
673
        }
674
        if (!$data) {
675
            return null;
676
        }
677
        return unserialize($data);
678
    }
679
680
    /**
681
     * {@inheritdoc}
682
     */
683
    public function getRules()
684
    {
685
        if ($this->rules !== null) {
686
            return $this->rules;
687
        }
688
689
        $query = (new Query())->from($this->ruleTable);
690
691
        $rules = [];
692
        foreach ($query->all($this->db) as $row) {
693
            $data = $row['data'];
694
            if (is_resource($data)) {
695
                $data = stream_get_contents($data);
696
            }
697
            if ($data) {
698
                $rules[$row['name']] = unserialize($data);
699
            }
700
        }
701
702
        return $rules;
703
    }
704
705
    /**
706
     * {@inheritdoc}
707
     */
708
    public function getAssignment($roleName, $userId)
709
    {
710
        if ($this->isEmptyUserId($userId)) {
711
            return null;
712
        }
713
714
        $row = (new Query())->from($this->assignmentTable)
715
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
716
            ->one($this->db);
717
718
        if ($row === false) {
719
            return null;
720
        }
721
722
        return new Assignment([
723
            'userId' => $row['user_id'],
724
            'roleName' => $row['item_name'],
725
            'createdAt' => $row['created_at'],
726
        ]);
727
    }
728
729
    /**
730
     * {@inheritdoc}
731
     */
732
    public function getAssignments($userId)
733
    {
734
        if ($this->isEmptyUserId($userId)) {
735
            return [];
736
        }
737
738
        $query = (new Query())
739
            ->from($this->assignmentTable)
740
            ->where(['user_id' => (string) $userId]);
741
742
        $assignments = [];
743
        foreach ($query->all($this->db) as $row) {
744
            $assignments[$row['item_name']] = new Assignment([
745
                'userId' => $row['user_id'],
746
                'roleName' => $row['item_name'],
747
                'createdAt' => $row['created_at'],
748
            ]);
749
        }
750
751
        return $assignments;
752
    }
753
754
    /**
755
     * {@inheritdoc}
756
     * @since 2.0.8
757
     */
758
    public function canAddChild($parent, $child)
759
    {
760
        return !$this->detectLoop($parent, $child);
761
    }
762
763
    /**
764
     * {@inheritdoc}
765
     */
766
    public function addChild($parent, $child)
767
    {
768
        if ($parent->name === $child->name) {
769
            throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
770
        }
771
772
        if ($parent instanceof Permission && $child instanceof Role) {
773
            throw new InvalidArgumentException('Cannot add a role as a child of a permission.');
774
        }
775
776
        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
        $this->db->createCommand()
781
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
782
            ->execute();
783
784
        $this->invalidateCache();
785
786
        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
    public function getChildren($name)
832
    {
833
        $query = (new Query())
834
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
835
            ->from([$this->itemTable, $this->itemChildTable])
836
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
837
838
        $children = [];
839
        foreach ($query->all($this->db) as $row) {
840
            $children[$row['name']] = $this->populateItem($row);
841
        }
842
843
        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
    protected function detectLoop($parent, $child)
853
    {
854
        if ($child->name === $parent->name) {
855
            return true;
856
        }
857
        foreach ($this->getChildren($child->name) as $grandchild) {
858
            if ($this->detectLoop($parent, $grandchild)) {
859
                return true;
860
            }
861
        }
862
863
        return false;
864
    }
865
866
    /**
867
     * {@inheritdoc}
868
     */
869
    public function assign($role, $userId)
870
    {
871
        $assignment = new Assignment([
872
            'userId' => $userId,
873
            'roleName' => $role->name,
874
            'createdAt' => time(),
875
        ]);
876
877
        $this->db->createCommand()
878
            ->insert($this->assignmentTable, [
879
                'user_id' => $assignment->userId,
880
                'item_name' => $assignment->roleName,
881
                'created_at' => $assignment->createdAt,
882
            ])->execute();
883
884
        unset($this->checkAccessAssignments[(string) $userId]);
885
886
        $this->invalidateCache();
887
888
        return $assignment;
889
    }
890
891
    /**
892
     * {@inheritdoc}
893
     */
894
    public function revoke($role, $userId)
895
    {
896
        if ($this->isEmptyUserId($userId)) {
897
            return false;
898
        }
899
900
        unset($this->checkAccessAssignments[(string) $userId]);
901
        $result = $this->db->createCommand()
902
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
903
            ->execute() > 0;
904
905
        $this->invalidateCache();
906
907
        return $result;
908
    }
909
910
    /**
911
     * {@inheritdoc}
912
     */
913
    public function revokeAll($userId)
914
    {
915
        if ($this->isEmptyUserId($userId)) {
916
            return false;
917
        }
918
919
        unset($this->checkAccessAssignments[(string) $userId]);
920
        $result = $this->db->createCommand()
921
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
922
            ->execute() > 0;
923
924
        $this->invalidateCache();
925
926
        return $result;
927
    }
928
929
    /**
930
     * {@inheritdoc}
931
     */
932
    public function removeAll()
933
    {
934
        $this->removeAllAssignments();
935
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
936
        $this->db->createCommand()->delete($this->itemTable)->execute();
937
        $this->db->createCommand()->delete($this->ruleTable)->execute();
938
        $this->invalidateCache();
939
    }
940
941
    /**
942
     * {@inheritdoc}
943
     */
944
    public function removeAllPermissions()
945
    {
946
        $this->removeAllItems(Item::TYPE_PERMISSION);
947
    }
948
949
    /**
950
     * {@inheritdoc}
951
     */
952
    public function removeAllRoles()
953
    {
954
        $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
    protected function removeAllItems($type)
962
    {
963
        if (!$this->supportsCascadeUpdate()) {
964
            $names = (new Query())
965
                ->select(['name'])
966
                ->from($this->itemTable)
967
                ->where(['type' => $type])
968
                ->column($this->db);
969
            if (empty($names)) {
970
                return;
971
            }
972
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
973
            $this->db->createCommand()
974
                ->delete($this->itemChildTable, [$key => $names])
975
                ->execute();
976
            $this->db->createCommand()
977
                ->delete($this->assignmentTable, ['item_name' => $names])
978
                ->execute();
979
        }
980
        $this->db->createCommand()
981
            ->delete($this->itemTable, ['type' => $type])
982
            ->execute();
983
984
        $this->invalidateCache();
985
    }
986
987
    /**
988
     * {@inheritdoc}
989
     */
990
    public function removeAllRules()
991
    {
992
        if (!$this->supportsCascadeUpdate()) {
993
            $this->db->createCommand()
994
                ->update($this->itemTable, ['rule_name' => null])
995
                ->execute();
996
        }
997
998
        $this->db->createCommand()->delete($this->ruleTable)->execute();
999
1000
        $this->invalidateCache();
1001
    }
1002
1003
    /**
1004
     * {@inheritdoc}
1005
     */
1006
    public function removeAllAssignments()
1007
    {
1008
        $this->checkAccessAssignments = [];
1009
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
1010
    }
1011
1012
    public function invalidateCache()
1013
    {
1014
        if ($this->cache !== null) {
1015
            $this->cache->delete($this->cacheKey);
1016
            $this->items = null;
1017
            $this->rules = null;
1018
            $this->parents = null;
1019
1020
            $cachedUserIds = $this->cache->get($this->getUserRolesCachedSetKey());
1021
1022
            if ($cachedUserIds !== false) {
1023
                foreach ($cachedUserIds as $userId) {
1024
                    $this->cache->delete($this->getUserRolesCacheKey($userId));
1025
                }
1026
1027
                $this->cache->delete($this->getUserRolesCachedSetKey());
1028
            }
1029
        }
1030
        $this->checkAccessAssignments = [];
1031
    }
1032
1033
    public function loadFromCache()
1034
    {
1035
        if ($this->items !== null || !$this->cache instanceof CacheInterface) {
1036
            return;
1037
        }
1038
1039
        $data = $this->cache->get($this->cacheKey);
1040
        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
        $query = (new Query())->from($this->itemTable);
1046
        $this->items = [];
1047
        foreach ($query->all($this->db) as $row) {
1048
            $this->items[$row['name']] = $this->populateItem($row);
1049
        }
1050
1051
        $query = (new Query())->from($this->ruleTable);
1052
        $this->rules = [];
1053
        foreach ($query->all($this->db) as $row) {
1054
            $data = $row['data'];
1055
            if (is_resource($data)) {
1056
                $data = stream_get_contents($data);
1057
            }
1058
            if ($data) {
1059
                $this->rules[$row['name']] = unserialize($data);
1060
            }
1061
        }
1062
1063
        $query = (new Query())->from($this->itemChildTable);
1064
        $this->parents = [];
1065
        foreach ($query->all($this->db) as $row) {
1066
            if (isset($this->items[$row['child']])) {
1067
                $this->parents[$row['child']][] = $row['parent'];
1068
            }
1069
        }
1070
1071
        $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
    public function getUserIdsByRole($roleName)
1082
    {
1083
        if (empty($roleName)) {
1084
            return [];
1085
        }
1086
1087
        return (new Query())->select('[[user_id]]')
1088
            ->from($this->assignmentTable)
1089
            ->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
    protected function isEmptyUserId($userId)
1099
    {
1100
        return !isset($userId) || $userId === '';
1101
    }
1102
1103
    private function getUserRolesCacheKey($userId)
1104
    {
1105
        return $this->cacheKey . $this->rolesCacheSuffix . $userId;
1106
    }
1107
1108
    private function getUserRolesCachedSetKey()
1109
    {
1110
        return $this->cacheKey . $this->rolesCacheSuffix;
1111
    }
1112
1113
    private function cacheUserRolesData($userId, $roles)
1114
    {
1115
        $cachedUserIds = $this->cache->get($this->getUserRolesCachedSetKey());
0 ignored issues
show
Bug introduced by
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
        if ($cachedUserIds === false) {
1118
            $cachedUserIds = [];
1119
        }
1120
1121
        $cachedUserIds[] = $userId;
1122
1123
        $this->cache->set($this->getUserRolesCacheKey($userId), $roles);
1124
        $this->cache->set($this->getUserRolesCachedSetKey(), $cachedUserIds);
1125
    }
1126
}
1127