Completed
Push — master ( f7942c...711498 )
by Carsten
12:24
created

DbManager::checkAccess()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.0119

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 10
cts 11
cp 0.9091
rs 9.2
c 0
b 0
f 0
cc 4
eloc 12
nc 6
nop 3
crap 4.0119
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\rbac;
9
10
use Yii;
11
use yii\base\InvalidCallException;
12
use yii\base\InvalidParamException;
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 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
    /**
92
     * @var Item[] all auth items (name => Item)
93
     */
94
    protected $items;
95
    /**
96
     * @var Rule[] all auth rules (name => Rule)
97
     */
98
    protected $rules;
99
    /**
100
     * @var array auth item parent-child relationships (childName => list of parents)
101
     */
102
    protected $parents;
103
104
105
    /**
106
     * Initializes the application component.
107
     * This method overrides the parent implementation by establishing the database connection.
108
     */
109 235
    public function init()
110
    {
111 235
        parent::init();
112 235
        $this->db = Instance::ensure($this->db, Connection::className());
113 235
        if ($this->cache !== null) {
114 94
            $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
115
        }
116 235
    }
117
118
    private $_checkAccessAssignments = [];
119
120
    /**
121
     * @inheritdoc
122
     */
123 30
    public function checkAccess($userId, $permissionName, $params = [])
124
    {
125 30
        if (isset($this->_checkAccessAssignments[(string) $userId])) {
126 30
            $assignments = $this->_checkAccessAssignments[(string) $userId];
127
        } else {
128 30
            $assignments = $this->getAssignments($userId);
129 30
            $this->_checkAccessAssignments[(string) $userId] = $assignments;
130
        }
131
132 30
        if ($this->hasNoAssignments($assignments)) {
133
            return false;
134
        }
135
136 30
        $this->loadFromCache();
137 30
        if ($this->items !== null) {
138 15
            return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
139
        }
140
141 15
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
142
    }
143
144
    /**
145
     * Performs access check for the specified user based on the data loaded from cache.
146
     * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
147
     * @param string|int $user the user ID. This should can be either an integer or a string representing
148
     * the unique identifier of a user. See [[\yii\web\User::id]].
149
     * @param string $itemName the name of the operation that need access check
150
     * @param array $params name-value pairs that would be passed to rules associated
151
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
152
     * which holds the value of `$userId`.
153
     * @param Assignment[] $assignments the assignments to the specified user
154
     * @return bool whether the operations can be performed by the user.
155
     * @since 2.0.3
156
     */
157 15
    protected function checkAccessFromCache($user, $itemName, $params, $assignments)
158
    {
159 15
        if (!isset($this->items[$itemName])) {
160 2
            return false;
161
        }
162
163 15
        $item = $this->items[$itemName];
164
165 15
        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
166
167 15
        if (!$this->executeRule($user, $item, $params)) {
168 10
            return false;
169
        }
170
171 15
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
172 11
            return true;
173
        }
174
175 11
        if (!empty($this->parents[$itemName])) {
176 7
            foreach ($this->parents[$itemName] as $parent) {
177 7
                if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
178 7
                    return true;
179
                }
180
            }
181
        }
182
183 11
        return false;
184
    }
185
186
    /**
187
     * Performs access check for the specified user.
188
     * This method is internally called by [[checkAccess()]].
189
     * @param string|int $user the user ID. This should can be either an integer or a string representing
190
     * the unique identifier of a user. See [[\yii\web\User::id]].
191
     * @param string $itemName the name of the operation that need access check
192
     * @param array $params name-value pairs that would be passed to rules associated
193
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
194
     * which holds the value of `$userId`.
195
     * @param Assignment[] $assignments the assignments to the specified user
196
     * @return bool whether the operations can be performed by the user.
197
     */
198 15
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
199
    {
200 15
        if (($item = $this->getItem($itemName)) === null) {
201 3
            return false;
202
        }
203
204 15
        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
205
206 15
        if (!$this->executeRule($user, $item, $params)) {
207 15
            return false;
208
        }
209
210 15
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
211 9
            return true;
212
        }
213
214 9
        $query = new Query();
215 9
        $parents = $query->select(['parent'])
216 9
            ->from($this->itemChildTable)
217 9
            ->where(['child' => $itemName])
218 9
            ->column($this->db);
219 9
        foreach ($parents as $parent) {
220 3
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
221 3
                return true;
222
            }
223
        }
224
225 9
        return false;
226
    }
227
228
    /**
229
     * @inheritdoc
230
     */
231 84
    protected function getItem($name)
232
    {
233 84
        if (empty($name)) {
234 3
            return null;
235
        }
236
237 84
        if (!empty($this->items[$name])) {
238 9
            return $this->items[$name];
239
        }
240
241 75
        $row = (new Query())->from($this->itemTable)
242 75
            ->where(['name' => $name])
243 75
            ->one($this->db);
244
245 75
        if ($row === false) {
246 13
            return null;
247
        }
248
249 75
        return $this->populateItem($row);
250
    }
251
252
    /**
253
     * Returns a value indicating whether the database supports cascading update and delete.
254
     * The default implementation will return false for SQLite database and true for all other databases.
255
     * @return bool whether the database supports cascading update and delete.
256
     */
257 35
    protected function supportsCascadeUpdate()
258
    {
259 35
        return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
260
    }
261
262
    /**
263
     * @inheritdoc
264
     */
265 220
    protected function addItem($item)
266
    {
267 220
        $time = time();
268 220
        if ($item->createdAt === null) {
269 220
            $item->createdAt = $time;
270
        }
271 220
        if ($item->updatedAt === null) {
272 220
            $item->updatedAt = $time;
273
        }
274 220
        $this->db->createCommand()
275 220
            ->insert($this->itemTable, [
276 220
                'name' => $item->name,
277 220
                'type' => $item->type,
278 220
                'description' => $item->description,
279 220
                'rule_name' => $item->ruleName,
280 220
                'data' => $item->data === null ? null : serialize($item->data),
281 220
                'created_at' => $item->createdAt,
282 220
                'updated_at' => $item->updatedAt,
283 220
            ])->execute();
284
285 220
        $this->invalidateCache();
286
287 220
        return true;
288
    }
289
290
    /**
291
     * @inheritdoc
292
     */
293 5
    protected function removeItem($item)
294
    {
295 5
        if (!$this->supportsCascadeUpdate()) {
296 1
            $this->db->createCommand()
297 1
                ->delete($this->itemChildTable, ['or', '[[parent]]=:name', '[[child]]=:name'], [':name' => $item->name])
298 1
                ->execute();
299 1
            $this->db->createCommand()
300 1
                ->delete($this->assignmentTable, ['item_name' => $item->name])
301 1
                ->execute();
302
        }
303
304 5
        $this->db->createCommand()
305 5
            ->delete($this->itemTable, ['name' => $item->name])
306 5
            ->execute();
307
308 5
        $this->invalidateCache();
309
310 5
        return true;
311
    }
312
313
    /**
314
     * @inheritdoc
315
     */
316 15
    protected function updateItem($name, $item)
317
    {
318 15
        if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
319 3
            $this->db->createCommand()
320 3
                ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
321 3
                ->execute();
322 3
            $this->db->createCommand()
323 3
                ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
324 3
                ->execute();
325 3
            $this->db->createCommand()
326 3
                ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
327 3
                ->execute();
328
        }
329
330 15
        $item->updatedAt = time();
331
332 15
        $this->db->createCommand()
333 15
            ->update($this->itemTable, [
334 15
                'name' => $item->name,
335 15
                'description' => $item->description,
336 15
                'rule_name' => $item->ruleName,
337 15
                'data' => $item->data === null ? null : serialize($item->data),
338 15
                'updated_at' => $item->updatedAt,
339
            ], [
340 15
                'name' => $name,
341 15
            ])->execute();
342
343 15
        $this->invalidateCache();
344
345 15
        return true;
346
    }
347
348
    /**
349
     * @inheritdoc
350
     */
351 130
    protected function addRule($rule)
352
    {
353 130
        $time = time();
354 130
        if ($rule->createdAt === null) {
355 130
            $rule->createdAt = $time;
356
        }
357 130
        if ($rule->updatedAt === null) {
358 130
            $rule->updatedAt = $time;
359
        }
360 130
        $this->db->createCommand()
361 130
            ->insert($this->ruleTable, [
362 130
                'name' => $rule->name,
363 130
                'data' => serialize($rule),
364 130
                'created_at' => $rule->createdAt,
365 130
                'updated_at' => $rule->updatedAt,
366 130
            ])->execute();
367
368 130
        $this->invalidateCache();
369
370 130
        return true;
371
    }
372
373
    /**
374
     * @inheritdoc
375
     */
376 5
    protected function updateRule($name, $rule)
377
    {
378 5
        if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
379 1
            $this->db->createCommand()
380 1
                ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
381 1
                ->execute();
382
        }
383
384 5
        $rule->updatedAt = time();
385
386 5
        $this->db->createCommand()
387 5
            ->update($this->ruleTable, [
388 5
                'name' => $rule->name,
389 5
                'data' => serialize($rule),
390 5
                'updated_at' => $rule->updatedAt,
391
            ], [
392 5
                'name' => $name,
393 5
            ])->execute();
394
395 5
        $this->invalidateCache();
396
397 5
        return true;
398
    }
399
400
    /**
401
     * @inheritdoc
402
     */
403 5
    protected function removeRule($rule)
404
    {
405 5
        if (!$this->supportsCascadeUpdate()) {
406 1
            $this->db->createCommand()
407 1
                ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
408 1
                ->execute();
409
        }
410
411 5
        $this->db->createCommand()
412 5
            ->delete($this->ruleTable, ['name' => $rule->name])
413 5
            ->execute();
414
415 5
        $this->invalidateCache();
416
417 5
        return true;
418
    }
419
420
    /**
421
     * @inheritdoc
422
     */
423 20
    protected function getItems($type)
424
    {
425 20
        $query = (new Query())
426 20
            ->from($this->itemTable)
427 20
            ->where(['type' => $type]);
428
429 20
        $items = [];
430 20
        foreach ($query->all($this->db) as $row) {
431 20
            $items[$row['name']] = $this->populateItem($row);
432
        }
433
434 20
        return $items;
435
    }
436
437
    /**
438
     * Populates an auth item with the data fetched from database.
439
     * @param array $row the data from the auth item table
440
     * @return Item the populated auth item instance (either Role or Permission)
441
     */
442 160
    protected function populateItem($row)
443
    {
444 160
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
445
446 160
        if (!isset($row['data']) || ($data = @unserialize(is_resource($row['data']) ? stream_get_contents($row['data']) : $row['data'])) === false) {
447 160
            $data = null;
448
        }
449
450 160
        return new $class([
451 160
            'name' => $row['name'],
452 160
            'type' => $row['type'],
453 160
            'description' => $row['description'],
454 160
            'ruleName' => $row['rule_name'],
455 160
            'data' => $data,
456 160
            'createdAt' => $row['created_at'],
457 160
            'updatedAt' => $row['updated_at'],
458
        ]);
459
    }
460
461
    /**
462
     * @inheritdoc
463
     * The roles returned by this method include the roles assigned via [[$defaultRoles]].
464
     */
465 25
    public function getRolesByUser($userId)
466
    {
467 25
        if ($this->isEmptyUserId($userId)) {
468 5
            return [];
469
        }
470
471 20
        $query = (new Query())->select('b.*')
472 20
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
473 20
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
474 20
            ->andWhere(['a.user_id' => (string) $userId])
475 20
            ->andWhere(['b.type' => Item::TYPE_ROLE]);
476
477 20
        $roles = $this->getDefaultRoleInstances();
478 20
        foreach ($query->all($this->db) as $row) {
479 20
            $roles[$row['name']] = $this->populateItem($row);
480
        }
481
482 20
        return $roles;
483
    }
484
485
    /**
486
     * @inheritdoc
487
     */
488 5
    public function getChildRoles($roleName)
489
    {
490 5
        $role = $this->getRole($roleName);
491
492 5
        if ($role === null) {
493
            throw new InvalidParamException("Role \"$roleName\" not found.");
494
        }
495
496 5
        $result = [];
497 5
        $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);
498
499 5
        $roles = [$roleName => $role];
500
501 5
        $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
502 5
            return array_key_exists($roleItem->name, $result);
503 5
        });
504
505 5
        return $roles;
506
    }
507
508
    /**
509
     * @inheritdoc
510
     */
511 5
    public function getPermissionsByRole($roleName)
512
    {
513 5
        $childrenList = $this->getChildrenList();
514 5
        $result = [];
515 5
        $this->getChildrenRecursive($roleName, $childrenList, $result);
516 5
        if (empty($result)) {
517
            return [];
518
        }
519 5
        $query = (new Query())->from($this->itemTable)->where([
520 5
            'type' => Item::TYPE_PERMISSION,
521 5
            'name' => array_keys($result),
522
        ]);
523 5
        $permissions = [];
524 5
        foreach ($query->all($this->db) as $row) {
525 5
            $permissions[$row['name']] = $this->populateItem($row);
526
        }
527
528 5
        return $permissions;
529
    }
530
531
    /**
532
     * @inheritdoc
533
     */
534 20
    public function getPermissionsByUser($userId)
535
    {
536 20
        if ($this->isEmptyUserId($userId)) {
537 5
            return [];
538
        }
539
540 15
        $directPermission = $this->getDirectPermissionsByUser($userId);
541 15
        $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
542
543 15
        return array_merge($directPermission, $inheritedPermission);
544
    }
545
546
    /**
547
     * Returns all permissions that are directly assigned to user.
548
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
549
     * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
550
     * @since 2.0.7
551
     */
552 15
    protected function getDirectPermissionsByUser($userId)
553
    {
554 15
        $query = (new Query())->select('b.*')
555 15
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
556 15
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
557 15
            ->andWhere(['a.user_id' => (string) $userId])
558 15
            ->andWhere(['b.type' => Item::TYPE_PERMISSION]);
559
560 15
        $permissions = [];
561 15
        foreach ($query->all($this->db) as $row) {
562 15
            $permissions[$row['name']] = $this->populateItem($row);
563
        }
564
565 15
        return $permissions;
566
    }
567
568
    /**
569
     * Returns all permissions that the user inherits from the roles assigned to him.
570
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
571
     * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
572
     * @since 2.0.7
573
     */
574 15
    protected function getInheritedPermissionsByUser($userId)
575
    {
576 15
        $query = (new Query())->select('item_name')
577 15
            ->from($this->assignmentTable)
578 15
            ->where(['user_id' => (string) $userId]);
579
580 15
        $childrenList = $this->getChildrenList();
581 15
        $result = [];
582 15
        foreach ($query->column($this->db) as $roleName) {
583 15
            $this->getChildrenRecursive($roleName, $childrenList, $result);
584
        }
585
586 15
        if (empty($result)) {
587 10
            return [];
588
        }
589
590 5
        $query = (new Query())->from($this->itemTable)->where([
591 5
            'type' => Item::TYPE_PERMISSION,
592 5
            'name' => array_keys($result),
593
        ]);
594 5
        $permissions = [];
595 5
        foreach ($query->all($this->db) as $row) {
596 5
            $permissions[$row['name']] = $this->populateItem($row);
597
        }
598
599 5
        return $permissions;
600
    }
601
602
    /**
603
     * Returns the children for every parent.
604
     * @return array the children list. Each array key is a parent item name,
605
     * and the corresponding array value is a list of child item names.
606
     */
607 25
    protected function getChildrenList()
608
    {
609 25
        $query = (new Query())->from($this->itemChildTable);
610 25
        $parents = [];
611 25
        foreach ($query->all($this->db) as $row) {
612 15
            $parents[$row['parent']][] = $row['child'];
613
        }
614
615 25
        return $parents;
616
    }
617
618
    /**
619
     * Recursively finds all children and grand children of the specified item.
620
     * @param string $name the name of the item whose children are to be looked for.
621
     * @param array $childrenList the child list built via [[getChildrenList()]]
622
     * @param array $result the children and grand children (in array keys)
623
     */
624 25
    protected function getChildrenRecursive($name, $childrenList, &$result)
625
    {
626 25
        if (isset($childrenList[$name])) {
627 15
            foreach ($childrenList[$name] as $child) {
628 15
                $result[$child] = true;
629 15
                $this->getChildrenRecursive($child, $childrenList, $result);
630
            }
631
        }
632 25
    }
633
634
    /**
635
     * @inheritdoc
636
     */
637 125
    public function getRule($name)
638
    {
639 125
        if ($this->rules !== null) {
640 10
            return isset($this->rules[$name]) ? $this->rules[$name] : null;
641
        }
642
643 125
        $row = (new Query())->select(['data'])
644 125
            ->from($this->ruleTable)
645 125
            ->where(['name' => $name])
646 125
            ->one($this->db);
647 125
        if ($row === false) {
648 20
            return null;
649
        }
650 125
        $data = $row['data'];
651 125
        if (is_resource($data)) {
652 50
            $data = stream_get_contents($data);
653
        }
654
655 125
        return unserialize($data);
656
    }
657
658
    /**
659
     * @inheritdoc
660
     */
661 25
    public function getRules()
662
    {
663 25
        if ($this->rules !== null) {
664
            return $this->rules;
665
        }
666
667 25
        $query = (new Query())->from($this->ruleTable);
668
669 25
        $rules = [];
670 25
        foreach ($query->all($this->db) as $row) {
671 15
            $data = $row['data'];
672 15
            if (is_resource($data)) {
673 6
                $data = stream_get_contents($data);
674
            }
675 15
            $rules[$row['name']] = unserialize($data);
676
        }
677
678 25
        return $rules;
679
    }
680
681
    /**
682
     * @inheritdoc
683
     */
684 15
    public function getAssignment($roleName, $userId)
685
    {
686 15
        if ($this->isEmptyUserId($userId)) {
687 5
            return null;
688
        }
689
690 10
        $row = (new Query())->from($this->assignmentTable)
691 10
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
692 10
            ->one($this->db);
693
694 10
        if ($row === false) {
695
            return null;
696
        }
697
698 10
        return new Assignment([
699 10
            'userId' => $row['user_id'],
700 10
            'roleName' => $row['item_name'],
701 10
            'createdAt' => $row['created_at'],
702
        ]);
703
    }
704
705
    /**
706
     * @inheritdoc
707
     */
708 50
    public function getAssignments($userId)
709
    {
710 50
        if ($this->isEmptyUserId($userId)) {
711 5
            return [];
712
        }
713
714 45
        $query = (new Query())
715 45
            ->from($this->assignmentTable)
716 45
            ->where(['user_id' => (string) $userId]);
717
718 45
        $assignments = [];
719 45
        foreach ($query->all($this->db) as $row) {
720 35
            $assignments[$row['item_name']] = new Assignment([
721 35
                'userId' => $row['user_id'],
722 35
                'roleName' => $row['item_name'],
723 35
                'createdAt' => $row['created_at'],
724
            ]);
725
        }
726
727 45
        return $assignments;
728
    }
729
730
    /**
731
     * @inheritdoc
732
     * @since 2.0.8
733
     */
734 5
    public function canAddChild($parent, $child)
735
    {
736 5
        return !$this->detectLoop($parent, $child);
737
    }
738
739
    /**
740
     * @inheritdoc
741
     */
742 105
    public function addChild($parent, $child)
743
    {
744 105
        if ($parent->name === $child->name) {
745
            throw new InvalidParamException("Cannot add '{$parent->name}' as a child of itself.");
746
        }
747
748 105
        if ($parent instanceof Permission && $child instanceof Role) {
749
            throw new InvalidParamException('Cannot add a role as a child of a permission.');
750
        }
751
752 105
        if ($this->detectLoop($parent, $child)) {
753
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
754
        }
755
756 105
        $this->db->createCommand()
757 105
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
758 105
            ->execute();
759
760 105
        $this->invalidateCache();
761
762 105
        return true;
763
    }
764
765
    /**
766
     * @inheritdoc
767
     */
768
    public function removeChild($parent, $child)
769
    {
770
        $result = $this->db->createCommand()
771
            ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
772
            ->execute() > 0;
773
774
        $this->invalidateCache();
775
776
        return $result;
777
    }
778
779
    /**
780
     * @inheritdoc
781
     */
782
    public function removeChildren($parent)
783
    {
784
        $result = $this->db->createCommand()
785
            ->delete($this->itemChildTable, ['parent' => $parent->name])
786
            ->execute() > 0;
787
788
        $this->invalidateCache();
789
790
        return $result;
791
    }
792
793
    /**
794
     * @inheritdoc
795
     */
796
    public function hasChild($parent, $child)
797
    {
798
        return (new Query())
799
            ->from($this->itemChildTable)
800
            ->where(['parent' => $parent->name, 'child' => $child->name])
801
            ->one($this->db) !== false;
802
    }
803
804
    /**
805
     * @inheritdoc
806
     */
807 105
    public function getChildren($name)
808
    {
809 105
        $query = (new Query())
810 105
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
811 105
            ->from([$this->itemTable, $this->itemChildTable])
812 105
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
813
814 105
        $children = [];
815 105
        foreach ($query->all($this->db) as $row) {
816 105
            $children[$row['name']] = $this->populateItem($row);
817
        }
818
819 105
        return $children;
820
    }
821
822
    /**
823
     * Checks whether there is a loop in the authorization item hierarchy.
824
     * @param Item $parent the parent item
825
     * @param Item $child the child item to be added to the hierarchy
826
     * @return bool whether a loop exists
827
     */
828 105
    protected function detectLoop($parent, $child)
829
    {
830 105
        if ($child->name === $parent->name) {
831 5
            return true;
832
        }
833 105
        foreach ($this->getChildren($child->name) as $grandchild) {
834 100
            if ($this->detectLoop($parent, $grandchild)) {
835 100
                return true;
836
            }
837
        }
838
839 105
        return false;
840
    }
841
842
    /**
843
     * @inheritdoc
844
     */
845 210
    public function assign($role, $userId)
846
    {
847 210
        $assignment = new Assignment([
848 210
            'userId' => $userId,
849 210
            'roleName' => $role->name,
850 210
            'createdAt' => time(),
851
        ]);
852
853 210
        $this->db->createCommand()
854 210
            ->insert($this->assignmentTable, [
855 210
                'user_id' => $assignment->userId,
856 210
                'item_name' => $assignment->roleName,
857 210
                'created_at' => $assignment->createdAt,
858 210
            ])->execute();
859
860 210
        unset($this->_checkAccessAssignments[(string) $userId]);
861 210
        return $assignment;
862
    }
863
864
    /**
865
     * @inheritdoc
866
     */
867 30
    public function revoke($role, $userId)
868
    {
869 30
        if ($this->isEmptyUserId($userId)) {
870 5
            return false;
871
        }
872
873 25
        unset($this->_checkAccessAssignments[(string) $userId]);
874 25
        return $this->db->createCommand()
875 25
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
876 25
            ->execute() > 0;
877
    }
878
879
    /**
880
     * @inheritdoc
881
     */
882 20
    public function revokeAll($userId)
883
    {
884 20
        if ($this->isEmptyUserId($userId)) {
885 5
            return false;
886
        }
887
888 15
        unset($this->_checkAccessAssignments[(string) $userId]);
889 15
        return $this->db->createCommand()
890 15
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
891 15
            ->execute() > 0;
892
    }
893
894
    /**
895
     * @inheritdoc
896
     */
897 235
    public function removeAll()
898
    {
899 235
        $this->removeAllAssignments();
900 235
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
901 235
        $this->db->createCommand()->delete($this->itemTable)->execute();
902 235
        $this->db->createCommand()->delete($this->ruleTable)->execute();
903 235
        $this->invalidateCache();
904 235
    }
905
906
    /**
907
     * @inheritdoc
908
     */
909 5
    public function removeAllPermissions()
910
    {
911 5
        $this->removeAllItems(Item::TYPE_PERMISSION);
912 5
    }
913
914
    /**
915
     * @inheritdoc
916
     */
917 5
    public function removeAllRoles()
918
    {
919 5
        $this->removeAllItems(Item::TYPE_ROLE);
920 5
    }
921
922
    /**
923
     * Removes all auth items of the specified type.
924
     * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
925
     */
926 10
    protected function removeAllItems($type)
927
    {
928 10
        if (!$this->supportsCascadeUpdate()) {
929 2
            $names = (new Query())
930 2
                ->select(['name'])
931 2
                ->from($this->itemTable)
932 2
                ->where(['type' => $type])
933 2
                ->column($this->db);
934 2
            if (empty($names)) {
935
                return;
936
            }
937 2
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
938 2
            $this->db->createCommand()
939 2
                ->delete($this->itemChildTable, [$key => $names])
940 2
                ->execute();
941 2
            $this->db->createCommand()
942 2
                ->delete($this->assignmentTable, ['item_name' => $names])
943 2
                ->execute();
944
        }
945 10
        $this->db->createCommand()
946 10
            ->delete($this->itemTable, ['type' => $type])
947 10
            ->execute();
948
949 10
        $this->invalidateCache();
950 10
    }
951
952
    /**
953
     * @inheritdoc
954
     */
955 5
    public function removeAllRules()
956
    {
957 5
        if (!$this->supportsCascadeUpdate()) {
958 1
            $this->db->createCommand()
959 1
                ->update($this->itemTable, ['rule_name' => null])
960 1
                ->execute();
961
        }
962
963 5
        $this->db->createCommand()->delete($this->ruleTable)->execute();
964
965 5
        $this->invalidateCache();
966 5
    }
967
968
    /**
969
     * @inheritdoc
970
     */
971 235
    public function removeAllAssignments()
972
    {
973 235
        $this->_checkAccessAssignments = [];
974 235
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
975 235
    }
976
977 235
    public function invalidateCache()
978
    {
979 235
        if ($this->cache !== null) {
980 97
            $this->cache->delete($this->cacheKey);
981 97
            $this->items = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<yii\rbac\Item>> of property $items.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
982 97
            $this->rules = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<yii\rbac\Rule>> of property $rules.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
983 97
            $this->parents = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $parents.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
984
        }
985 235
        $this->_checkAccessAssignments = [];
986 235
    }
987
988 30
    public function loadFromCache()
989
    {
990 30
        if ($this->items !== null || !$this->cache instanceof CacheInterface) {
991 30
            return;
992
        }
993
994 15
        $data = $this->cache->get($this->cacheKey);
995 15
        if (is_array($data) && isset($data[0], $data[1], $data[2])) {
996
            list($this->items, $this->rules, $this->parents) = $data;
997
            return;
998
        }
999
1000 15
        $query = (new Query())->from($this->itemTable);
1001 15
        $this->items = [];
1002 15
        foreach ($query->all($this->db) as $row) {
1003 15
            $this->items[$row['name']] = $this->populateItem($row);
1004
        }
1005
1006 15
        $query = (new Query())->from($this->ruleTable);
1007 15
        $this->rules = [];
1008 15
        foreach ($query->all($this->db) as $row) {
1009 15
            $data = $row['data'];
1010 15
            if (is_resource($data)) {
1011 7
                $data = stream_get_contents($data);
1012
            }
1013 15
            $this->rules[$row['name']] = unserialize($data);
1014
        }
1015
1016 15
        $query = (new Query())->from($this->itemChildTable);
1017 15
        $this->parents = [];
1018 15
        foreach ($query->all($this->db) as $row) {
1019 7
            if (isset($this->items[$row['child']])) {
1020 7
                $this->parents[$row['child']][] = $row['parent'];
1021
            }
1022
        }
1023
1024 15
        $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
1025 15
    }
1026
1027
    /**
1028
     * Returns all role assignment information for the specified role.
1029
     * @param string $roleName
1030
     * @return string[] the ids. An empty array will be
1031
     * returned if role is not assigned to any user.
1032
     * @since 2.0.7
1033
     */
1034 5
    public function getUserIdsByRole($roleName)
1035
    {
1036 5
        if (empty($roleName)) {
1037
            return [];
1038
        }
1039
1040 5
        return (new Query())->select('[[user_id]]')
1041 5
            ->from($this->assignmentTable)
1042 5
            ->where(['item_name' => $roleName])->column($this->db);
1043
    }
1044
1045
    /**
1046
     * Check whether $userId is empty.
1047
     * @param mixed $userId
1048
     * @return bool
1049
     */
1050 140
    private function isEmptyUserId($userId)
1051
    {
1052 140
        return !isset($userId) || $userId === '';
1053
    }
1054
}
1055