Passed
Pull Request — master (#19513)
by Alexander
08:39
created

DbManager::getUserRolesCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
     */
93
    public $rolesCacheSuffix = 'roles';
94
95
    /**
96
     * @var Item[] all auth items (name => Item)
97
     */
98
    protected $items;
99
    /**
100
     * @var Rule[] all auth rules (name => Rule)
101
     */
102
    protected $rules;
103
    /**
104
     * @var array auth item parent-child relationships (childName => list of parents)
105
     */
106
    protected $parents;
107
    /**
108
     * @var array user assignments (user id => Assignment[])
109
     * @since `protected` since 2.0.38
110
     */
111
    protected $checkAccessAssignments = [];
112
113
114
    /**
115
     * Initializes the application component.
116
     * This method overrides the parent implementation by establishing the database connection.
117
     */
118 245
    public function init()
119
    {
120 245
        parent::init();
121 245
        $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

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

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