Issues (910)

framework/rbac/DbManager.php (1 issue)

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());
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();
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