Completed
Push — master ( e2ba94...1ee29c )
by Alexander
41:45 queued 38:17
created

DbManager::isEmptyUserId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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