Passed
Push — master ( 52cf02...273cba )
by Alexander
18:21 queued 14:33
created

DbManager::assign()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1.0023

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 2
dl 0
loc 17
ccs 13
cts 15
cp 0.8667
crap 1.0023
rs 9.8666
c 0
b 0
f 0
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\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 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
658 125
        return unserialize($data);
659
    }
660
661
    /**
662
     * {@inheritdoc}
663
     */
664 25
    public function getRules()
665
    {
666 25
        if ($this->rules !== null) {
667
            return $this->rules;
668
        }
669
670 25
        $query = (new Query())->from($this->ruleTable);
671
672 25
        $rules = [];
673 25
        foreach ($query->all($this->db) as $row) {
674 15
            $data = $row['data'];
675 15
            if (is_resource($data)) {
676 6
                $data = stream_get_contents($data);
677
            }
678 15
            $rules[$row['name']] = unserialize($data);
679
        }
680
681 25
        return $rules;
682
    }
683
684
    /**
685
     * {@inheritdoc}
686
     */
687 15
    public function getAssignment($roleName, $userId)
688
    {
689 15
        if ($this->isEmptyUserId($userId)) {
690 5
            return null;
691
        }
692
693 10
        $row = (new Query())->from($this->assignmentTable)
694 10
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
695 10
            ->one($this->db);
696
697 10
        if ($row === false) {
698
            return null;
699
        }
700
701 10
        return new Assignment([
702 10
            'userId' => $row['user_id'],
703 10
            'roleName' => $row['item_name'],
704 10
            'createdAt' => $row['created_at'],
705
        ]);
706
    }
707
708
    /**
709
     * {@inheritdoc}
710
     */
711 50
    public function getAssignments($userId)
712
    {
713 50
        if ($this->isEmptyUserId($userId)) {
714 5
            return [];
715
        }
716
717 45
        $query = (new Query())
718 45
            ->from($this->assignmentTable)
719 45
            ->where(['user_id' => (string) $userId]);
720
721 45
        $assignments = [];
722 45
        foreach ($query->all($this->db) as $row) {
723 35
            $assignments[$row['item_name']] = new Assignment([
724 35
                'userId' => $row['user_id'],
725 35
                'roleName' => $row['item_name'],
726 35
                'createdAt' => $row['created_at'],
727
            ]);
728
        }
729
730 45
        return $assignments;
731
    }
732
733
    /**
734
     * {@inheritdoc}
735
     * @since 2.0.8
736
     */
737 5
    public function canAddChild($parent, $child)
738
    {
739 5
        return !$this->detectLoop($parent, $child);
740
    }
741
742
    /**
743
     * {@inheritdoc}
744
     */
745 105
    public function addChild($parent, $child)
746
    {
747 105
        if ($parent->name === $child->name) {
748
            throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
749
        }
750
751 105
        if ($parent instanceof Permission && $child instanceof Role) {
752
            throw new InvalidArgumentException('Cannot add a role as a child of a permission.');
753
        }
754
755 105
        if ($this->detectLoop($parent, $child)) {
756
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
757
        }
758
759 105
        $this->db->createCommand()
760 105
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
761 105
            ->execute();
762
763 105
        $this->invalidateCache();
764
765 105
        return true;
766
    }
767
768
    /**
769
     * {@inheritdoc}
770
     */
771
    public function removeChild($parent, $child)
772
    {
773
        $result = $this->db->createCommand()
774
            ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
775
            ->execute() > 0;
776
777
        $this->invalidateCache();
778
779
        return $result;
780
    }
781
782
    /**
783
     * {@inheritdoc}
784
     */
785
    public function removeChildren($parent)
786
    {
787
        $result = $this->db->createCommand()
788
            ->delete($this->itemChildTable, ['parent' => $parent->name])
789
            ->execute() > 0;
790
791
        $this->invalidateCache();
792
793
        return $result;
794
    }
795
796
    /**
797
     * {@inheritdoc}
798
     */
799
    public function hasChild($parent, $child)
800
    {
801
        return (new Query())
802
            ->from($this->itemChildTable)
803
            ->where(['parent' => $parent->name, 'child' => $child->name])
804
            ->one($this->db) !== false;
805
    }
806
807
    /**
808
     * {@inheritdoc}
809
     */
810 105
    public function getChildren($name)
811
    {
812 105
        $query = (new Query())
813 105
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
814 105
            ->from([$this->itemTable, $this->itemChildTable])
815 105
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
816
817 105
        $children = [];
818 105
        foreach ($query->all($this->db) as $row) {
819 105
            $children[$row['name']] = $this->populateItem($row);
820
        }
821
822 105
        return $children;
823
    }
824
825
    /**
826
     * Checks whether there is a loop in the authorization item hierarchy.
827
     * @param Item $parent the parent item
828
     * @param Item $child the child item to be added to the hierarchy
829
     * @return bool whether a loop exists
830
     */
831 105
    protected function detectLoop($parent, $child)
832
    {
833 105
        if ($child->name === $parent->name) {
834 5
            return true;
835
        }
836 105
        foreach ($this->getChildren($child->name) as $grandchild) {
837 100
            if ($this->detectLoop($parent, $grandchild)) {
838 100
                return true;
839
            }
840
        }
841
842 105
        return false;
843
    }
844
845
    /**
846
     * {@inheritdoc}
847
     */
848 210
    public function assign($role, $userId)
849
    {
850 210
        $assignment = new Assignment([
851 210
            'userId' => $userId,
852 210
            'roleName' => $role->name,
853 210
            'createdAt' => time(),
854
        ]);
855
856 210
        $this->db->createCommand()
857 210
            ->insert($this->assignmentTable, [
858 210
                'user_id' => $assignment->userId,
859 210
                'item_name' => $assignment->roleName,
860 210
                'created_at' => $assignment->createdAt,
861 210
            ])->execute();
862
863 210
        unset($this->checkAccessAssignments[(string) $userId]);
864 210
        return $assignment;
865
    }
866
867
    /**
868
     * {@inheritdoc}
869
     */
870 30
    public function revoke($role, $userId)
871
    {
872 30
        if ($this->isEmptyUserId($userId)) {
873 5
            return false;
874
        }
875
876 25
        unset($this->checkAccessAssignments[(string) $userId]);
877 25
        return $this->db->createCommand()
878 25
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
879 25
            ->execute() > 0;
880
    }
881
882
    /**
883
     * {@inheritdoc}
884
     */
885 20
    public function revokeAll($userId)
886
    {
887 20
        if ($this->isEmptyUserId($userId)) {
888 5
            return false;
889
        }
890
891 15
        unset($this->checkAccessAssignments[(string) $userId]);
892 15
        return $this->db->createCommand()
893 15
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
894 15
            ->execute() > 0;
895
    }
896
897
    /**
898
     * {@inheritdoc}
899
     */
900 245
    public function removeAll()
901
    {
902 245
        $this->removeAllAssignments();
903 245
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
904 245
        $this->db->createCommand()->delete($this->itemTable)->execute();
905 245
        $this->db->createCommand()->delete($this->ruleTable)->execute();
906 245
        $this->invalidateCache();
907 245
    }
908
909
    /**
910
     * {@inheritdoc}
911
     */
912 5
    public function removeAllPermissions()
913
    {
914 5
        $this->removeAllItems(Item::TYPE_PERMISSION);
915 5
    }
916
917
    /**
918
     * {@inheritdoc}
919
     */
920 5
    public function removeAllRoles()
921
    {
922 5
        $this->removeAllItems(Item::TYPE_ROLE);
923 5
    }
924
925
    /**
926
     * Removes all auth items of the specified type.
927
     * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
928
     */
929 10
    protected function removeAllItems($type)
930
    {
931 10
        if (!$this->supportsCascadeUpdate()) {
932 2
            $names = (new Query())
933 2
                ->select(['name'])
934 2
                ->from($this->itemTable)
935 2
                ->where(['type' => $type])
936 2
                ->column($this->db);
937 2
            if (empty($names)) {
938
                return;
939
            }
940 2
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
941 2
            $this->db->createCommand()
942 2
                ->delete($this->itemChildTable, [$key => $names])
943 2
                ->execute();
944 2
            $this->db->createCommand()
945 2
                ->delete($this->assignmentTable, ['item_name' => $names])
946 2
                ->execute();
947
        }
948 10
        $this->db->createCommand()
949 10
            ->delete($this->itemTable, ['type' => $type])
950 10
            ->execute();
951
952 10
        $this->invalidateCache();
953 10
    }
954
955
    /**
956
     * {@inheritdoc}
957
     */
958 5
    public function removeAllRules()
959
    {
960 5
        if (!$this->supportsCascadeUpdate()) {
961 1
            $this->db->createCommand()
962 1
                ->update($this->itemTable, ['rule_name' => null])
963 1
                ->execute();
964
        }
965
966 5
        $this->db->createCommand()->delete($this->ruleTable)->execute();
967
968 5
        $this->invalidateCache();
969 5
    }
970
971
    /**
972
     * {@inheritdoc}
973
     */
974 245
    public function removeAllAssignments()
975
    {
976 245
        $this->checkAccessAssignments = [];
977 245
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
978 245
    }
979
980 245
    public function invalidateCache()
981
    {
982 245
        if ($this->cache !== null) {
983 101
            $this->cache->delete($this->cacheKey);
984 101
            $this->items = null;
985 101
            $this->rules = null;
986 101
            $this->parents = null;
987
        }
988 245
        $this->checkAccessAssignments = [];
989 245
    }
990
991 30
    public function loadFromCache()
992
    {
993 30
        if ($this->items !== null || !$this->cache instanceof CacheInterface) {
994 30
            return;
995
        }
996
997 15
        $data = $this->cache->get($this->cacheKey);
998 15
        if (is_array($data) && isset($data[0], $data[1], $data[2])) {
999
            list($this->items, $this->rules, $this->parents) = $data;
1000
            return;
1001
        }
1002
1003 15
        $query = (new Query())->from($this->itemTable);
1004 15
        $this->items = [];
1005 15
        foreach ($query->all($this->db) as $row) {
1006 15
            $this->items[$row['name']] = $this->populateItem($row);
1007
        }
1008
1009 15
        $query = (new Query())->from($this->ruleTable);
1010 15
        $this->rules = [];
1011 15
        foreach ($query->all($this->db) as $row) {
1012 15
            $data = $row['data'];
1013 15
            if (is_resource($data)) {
1014 7
                $data = stream_get_contents($data);
1015
            }
1016 15
            $this->rules[$row['name']] = unserialize($data);
1017
        }
1018
1019 15
        $query = (new Query())->from($this->itemChildTable);
1020 15
        $this->parents = [];
1021 15
        foreach ($query->all($this->db) as $row) {
1022 7
            if (isset($this->items[$row['child']])) {
1023 7
                $this->parents[$row['child']][] = $row['parent'];
1024
            }
1025
        }
1026
1027 15
        $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
1028 15
    }
1029
1030
    /**
1031
     * Returns all role assignment information for the specified role.
1032
     * @param string $roleName
1033
     * @return string[] the ids. An empty array will be
1034
     * returned if role is not assigned to any user.
1035
     * @since 2.0.7
1036
     */
1037 5
    public function getUserIdsByRole($roleName)
1038
    {
1039 5
        if (empty($roleName)) {
1040
            return [];
1041
        }
1042
1043 5
        return (new Query())->select('[[user_id]]')
1044 5
            ->from($this->assignmentTable)
1045 5
            ->where(['item_name' => $roleName])->column($this->db);
1046
    }
1047
1048
    /**
1049
     * Check whether $userId is empty.
1050
     * @param mixed $userId
1051
     * @return bool
1052
     * @since 2.0.26
1053
     */
1054 140
    protected function isEmptyUserId($userId)
1055
    {
1056 140
        return !isset($userId) || $userId === '';
1057
    }
1058
}
1059