Passed
Push — upd-cach ( de12cf...517209 )
by
unknown
16:21 queued 08:11
created

DbManager::getRules()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
c 1
b 0
f 0
nc 6
nop 0
dl 0
loc 20
ccs 11
cts 12
cp 0.9167
crap 5.0144
rs 9.6111
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
    /**
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
     * @var array user assignments (user id => Assignment[])
105
     * @since `protected` since 2.0.38
106
     */
107
    protected $checkAccessAssignments = [];
108
109
110
    /**
111
     * Initializes the application component.
112
     * This method overrides the parent implementation by establishing the database connection.
113
     */
114 245
    public function init()
115
    {
116 245
        parent::init();
117 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

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

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