Manager::getRules()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.4285
cc 2
eloc 6
nc 2
nop 0
crap 2
1
<?php
2
/**
3
 * Manager.php
4
 *
5
 * PHP version 5.6+
6
 *
7
 * @author Philippe Gaultier <[email protected]>
8
 * @copyright 2010-2017 Philippe Gaultier
9
 * @license http://www.sweelix.net/license license
10
 * @version XXX
11
 * @link http://www.sweelix.net
12
 * @package sweelix\rbac\redis
13
 */
14
15
namespace sweelix\rbac\redis;
16
17
use sweelix\guid\Guid;
18
use Yii;
19
use yii\base\InvalidCallException;
20
use yii\base\InvalidParamException;
21
use yii\di\Instance;
22
use yii\rbac\Assignment;
23
use yii\rbac\BaseManager;
24
use yii\rbac\Permission;
25
use yii\rbac\Role;
26
use yii\rbac\Item;
27
use yii\rbac\Rule;
28
use yii\redis\Connection;
29
30
/**
31
 * REDIS Manager represents an authorization manager that stores
32
 * authorization information in REDIS database
33
 *
34
 *  database structure :
35
 *   * auth:users:<userId>:assignments : ZSET (string: roleName, score: createdAt)
36
 *   * auth:roles:<roleName>:assignments : ZSET (string|int: userId, score: createdAt)
37
 *   * auth:rules:<ruleName> : MAP (string: ruleName, string: data, integer: createdAt, integer: updatedAt)
38
 *   * auth:types:<typeId>:items : SET (string: itemName)
39
 *   * auth:items:<itemName> : MAP (string: itemName, int: typeId, string: description, integer: createdAt, integer: updatedAt, string: ruleName)
40
 *   * auth:rules:<ruleName>:items : SET (string: itemName)
41
 *   * auth:items:<itemName>:children : SET (string: itemName)
42
 *   * auth:items:<itemName>:parents : SET (string: itemName)
43
 *   * auth:mappings:items : MAP (string: itemName, string: guid)
44
 *   * auth:mappings:itemsguid : MAP (string: guid, string: itemName)
45
 *   * auth:mappings:rules : MAP (string: ruleName, string: guid)
46
 *   * auth:mappings:rulesguid : MAP (string: guid, string: ruleName)
47
 *
48
 * @author Philippe Gaultier <[email protected]>
49
 * @copyright 2010-2017 Philippe Gaultier
50
 * @license http://www.sweelix.net/license license
51
 * @version XXX
52
 * @link http://www.sweelix.net
53
 * @package application\controllers
54
 * @since XXX
55
 */
56
class Manager extends BaseManager
57
{
58
    /**
59
     * @var Connection|array|string the Redis DB connection object or the application component ID of the DB connection.
60
     */
61
    public $db = 'redis';
62
63
    /**
64
     * @var string
65
     */
66
    public $globalMatchKey = 'auth:*';
67
68
    /**
69
     * @var string
70
     */
71
    public $userAssignmentsKey = 'auth:users:{id}:assignments';
72
73
    /**
74
     * @var string
75
     */
76
    public $roleAssignmentsKey = 'auth:roles:{id}:assignments';
77
78
    /**
79
     * @var string
80
     */
81
    public $ruleKey = 'auth:rules:{id}';
82
83
    /**
84
     * @var string
85
     */
86
    public $typeItemsKey = 'auth:types:{id}:items';
87
88
    /**
89
     * @var string
90
     */
91
    public $itemKey = 'auth:items:{id}';
92
93
    /**
94
     * @var string
95
     */
96
    public $ruleItemsKey = 'auth:rules:{id}:items';
97
98
    /**
99
     * @var string
100
     */
101
    public $itemChildrenKey = 'auth:items:{id}:children';
102
103
    /**
104
     * @var string
105
     */
106
    public $itemParentsKey = 'auth:items:{id}:parents';
107
108
    /**
109
     * @var string
110
     */
111
    public $itemMappings = 'auth:mappings:items';
112
113
    /**
114
     * @var string
115
     */
116
    public $itemMappingsGuid = 'auth:mappings:itemsguid';
117
118
    /**
119
     * @var string
120
     */
121
    public $ruleMappings = 'auth:mappings:rules';
122
123
    /**
124
     * @var string
125
     */
126
    public $ruleMappingsGuid = 'auth:mappings:rulesguid';
127
128
129
    /**
130
     * @param string|integer $userId user id
131
     * @return string the user assignments key
132
     * @since XXX
133
     */
134 20
    public function getUserAssignmentsKey($userId)
135
    {
136 20
        return str_replace('{id}', $userId, $this->userAssignmentsKey);
137
    }
138
139
    /**
140
     * @param string $roleGuid role guid
141
     * @return string the rule assignments key
142
     * @since XXX
143
     */
144 20
    public function getRoleAssignmentsKey($roleGuid)
145
    {
146 20
        return str_replace('{id}', $roleGuid, $this->roleAssignmentsKey);
147
    }
148
149
    /**
150
     * @param string $ruleGuid rule guid
151
     * @return string the rule key
152
     * @since XXX
153
     */
154 22
    public function getRuleKey($ruleGuid)
155
    {
156 22
        return str_replace('{id}', $ruleGuid, $this->ruleKey);
157
    }
158
159
    /**
160
     * @param integer $typeId type id
161
     * @return string the type id key
162
     * @since XXX
163
     */
164 29
    public function getTypeItemsKey($typeId)
165
    {
166 29
        return str_replace('{id}', $typeId, $this->typeItemsKey);
167
    }
168
169
    /**
170
     * @param string $itemGuid item guid
171
     * @return string
172
     * @since XXX
173
     */
174 29
    public function getItemKey($itemGuid)
175
    {
176 29
        return str_replace('{id}', $itemGuid, $this->itemKey);
177
    }
178
179
    /**
180
     * @param string $ruleGuid rule guid
181
     * @return string the rule items key
182
     * @since XXX
183
     */
184 4
    public function getRuleItemsKey($ruleGuid)
185
    {
186 4
        return str_replace('{id}', $ruleGuid, $this->ruleItemsKey);
187
    }
188
189
    /**
190
     * @param string $itemGuid item guid
191
     * @return string the item children key
192
     * @since XXX
193
     */
194 24
    public function getItemChildrenKey($itemGuid)
195
    {
196 24
        return str_replace('{id}', $itemGuid, $this->itemChildrenKey);
197
    }
198
199
    /**
200
     * @param string $itemGuid item guid
201
     * @return string the item parents key
202
     * @since XXX
203
     */
204 24
    public function getItemParentsKey($itemGuid)
205
    {
206 24
        return str_replace('{id}', $itemGuid, $this->itemParentsKey);
207
    }
208
209
    /**
210
     * @return string the item mapping key
211
     * @since XXX
212
     */
213 29
    public function getItemMappingKey()
214
    {
215 29
        return $this->itemMappings;
216
    }
217
218
    /**
219
     * @return string the rule mapping key
220
     * @since XXX
221
     */
222 29
    public function getRuleMappingKey()
223
    {
224 29
        return $this->ruleMappings;
225
    }
226
227
    /**
228
     * @return string the item mapping key
229
     * @since XXX
230
     */
231 29
    public function getItemMappingGuidKey()
232
    {
233 29
        return $this->itemMappingsGuid;
234
    }
235
236
    /**
237
     * @return string the rule mapping key
238
     * @since XXX
239
     */
240 22
    public function getRuleMappingGuidKey()
241
    {
242 22
        return $this->ruleMappingsGuid;
243
    }
244
245
246
    /**
247
     * @inheritdoc
248
     */
249 31
    public function init()
250
    {
251 31
        parent::init();
252 31
        $this->db = Instance::ensure($this->db, Connection::className());
253 31
    }
254
255
    /**
256
     * @inheritdoc
257
     */
258 11
    protected function getItem($name)
259
    {
260 11
        $item = null;
261 11
        $guid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $name]);
262 11
        if ($guid !== null) {
263 11
            $item = $this->getItemByGuid($guid, $name);
264 11
        }
265 11
        return $item;
266
    }
267
268
    /**
269
     * @param string $guid item guid
270
     * @param string $name name if known to avoid additional call
271
     * @return mixed
272
     * @since XXX
273
     */
274 25
    protected function getItemByGuid($guid, $name = null)
275
    {
276 25
        if ($name === null) {
277 24
            $name = $this->db->executeCommand('HGET', [$this->getItemMappingGuidKey(), $guid]);
278 24
        }
279 25
        $data = $this->db->executeCommand('HGETALL', [$this->getItemKey($guid)]);
280 25
        $dataRow = ['name' => $name];
281 25
        $nbProps = count($data);
282 25
        for ($i = 0; $i < $nbProps; $i = $i + 2) {
283 25
            $dataRow[$data[$i]] = $data[($i + 1)];
284 25
        }
285 25
        if (isset($dataRow['ruleGuid']) === true) {
286 20
            $dataRow['ruleName'] = $this->db->executeCommand('HGET', [$this->getRuleMappingGuidKey(), $dataRow['ruleGuid']]);
287 20
            unset($dataRow['ruleGuid']);
288 25
        } elseif(isset($dataRow['ruleClass']) === true) {
289 1
            $dataRow['ruleName'] = $dataRow['ruleClass'];
290 1
            unset($dataRow['ruleClass']);
291 1
        }
292 25
        $item = $this->populateItem($dataRow);
293 25
        return $item;
294
    }
295
296
    /**
297
     * @inheritdoc
298
     */
299 3
    protected function getItems($type)
300
    {
301 3
        $itemGuids = $this->db->executeCommand('SMEMBERS', [$this->getTypeItemsKey($type)]);
302 3
        $items = [];
303 3
        foreach($itemGuids as $itemGuid) {
304 3
            $item = $this->getItemByGuid($itemGuid);
305 3
            $items[$item->name] = $item;
306 3
        }
307 3
        return $items;
308
    }
309
310
    /**
311
     * @inheritdoc
312
     */
313 29
    protected function addItem($item)
314
    {
315 29
        if (empty($item->name) === true) {
316 1
            throw new InvalidParamException("Item name must be defined");
317
        }
318 29
        $itemExists = (int)$this->db->executeCommand('HEXISTS', [$this->getItemMappingKey(), $item->name]);
319 29
        if ($itemExists === 1) {
320 1
            throw new DuplicateKeyException("Rule '{$item->name}' already defined");
321
        }
322 29
        $guid = Guid::v4();
323 29
        $time = time();
324 29
        if ($item->createdAt === null) {
325 29
            $item->createdAt = $time;
326 29
        }
327 29
        if ($item->updatedAt === null) {
328 29
            $item->updatedAt = $time;
329 29
        }
330
331 29
        list($insertInRedis, ) = $this->prepareRedisItem($item, $guid);
332 29
        $this->db->executeCommand('MULTI');
333
        // update mapping
334 29
        $this->db->executeCommand('HSET', [$this->getItemMappingKey(), $item->name, $guid]);
335 29
        $this->db->executeCommand('HSET', [$this->getItemMappingGuidKey(), $guid, $item->name]);
336
        // insert item
337 29
        $this->db->executeCommand('HMSET', $insertInRedis);
338 29
        $this->db->executeCommand('SADD', [$this->getTypeItemsKey($item->type), $guid]);
339
        // affect rule
340 29
        if (isset($insertInRedis['ruleGuid']) === true) {
341
            $this->db->executeCommand('SADD', [$this->getRuleItemsKey($insertInRedis['ruleGuid']), $guid]);
342
        }
343 29
        $this->db->executeCommand('EXEC');
344 29
        return true;
345
346
    }
347
348
    /**
349
     * @param Item $item item to insert in DB
350
     * @param string $guid guid generated
351
     * @return array Redis command
352
     * @since XXX
353
     */
354 29
    private function prepareRedisItem($item, $guid)
355
    {
356 29
        $redisCleanItem = [$this->getItemKey($guid)];
357 29
        $redisItem = [$this->getItemKey($guid),
358 29
            'data', serialize($item->data),
359 29
            'type', $item->type,
360 29
            'createdAt', $item->createdAt,
361 29
            'updatedAt', $item->updatedAt
362 29
        ];
363
364 29
        $ruleGuid = $this->db->executeCommand('HGET', [$this->getRuleMappingKey(), $item->ruleName]);
365
366 29
        if ($ruleGuid !== null) {
367 20
            $redisItem[] = 'ruleGuid';
368 20
            $redisItem[] = $ruleGuid;
369 20
            $redisCleanItem[] = 'ruleClass';
370 29
        } elseif (class_exists($item->ruleName) || Yii::$container->has($item->ruleName)) {
371 1
            $redisItem[] = 'ruleClass';
372 1
            $redisItem[] = $item->ruleName;
373 1
            $redisCleanItem[] = 'ruleGuid';
374 1
        }
375
376 29
        if ($item->description !== null) {
377 23
            $redisItem[] = 'description';
378 23
            $redisItem[] = $item->description;
379 23
        } else {
380 25
            $redisCleanItem[] = 'description';
381
        }
382 29
        return [$redisItem, $redisCleanItem];
383
    }
384
385
386
    /**
387
     * @inheritdoc
388
     */
389 2
    protected function updateItem($name, $item)
390
    {
391 2
        $item->updatedAt = time();
392
393 2
        $guid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $name]);
394
395 2
        $ruleGuid = $this->db->executeCommand('HGET', [$this->getRuleMappingKey(), $item->ruleName]);
396 2
        $newRule = $ruleGuid;
397 2
        if (($ruleGuid === null) && (empty($item->ruleName) === false)) {
398 1
            $newRule = $item->ruleName;
399 1
        }
400
401 2
        list($currentRuleGuid, $currentRuleClass, $currentType) = $this->db->executeCommand('HMGET', [$this->getItemKey($guid), 'ruleGuid', 'ruleClass', 'type']);
402
403 2
        $oldRule = ($currentRuleGuid === null) ? $currentRuleClass : $currentRuleGuid;
404 2
        $isUpdated = $newRule !== $oldRule;
405
406 2
        $this->db->executeCommand('MULTI');
407 2
        if ($name !== $item->name) {
408 2
            $this->remap($name, $item->name, $guid, false);
409 2
        }
410
411 2
        list($updateItem, $updateEmptyItem) = $this->prepareRedisItem($item, $guid);
412
413 2
        if ($isUpdated && $currentRuleGuid !== null) {
414
            $this->db->executeCommand('SREM', [$this->getRuleItemsKey($currentRuleGuid), $guid]);
415
        }
416 2
        if ($isUpdated && $ruleGuid !== null) {
417
            $this->db->executeCommand('SADD', [$this->getRuleItemsKey($ruleGuid), $guid]);
418
        }
419 2
        if ($item->type !== $currentType) {
420
            $this->db->executeCommand('SREM', [$this->getTypeItemsKey($currentType), $guid]);
421
            $this->db->executeCommand('SADD', [$this->getTypeItemsKey($item->type), $guid]);
422
        }
423
424
        // update item
425 2
        $this->db->executeCommand('HMSET', $updateItem);
426
        // remove useless props
427 2
        if (count($updateEmptyItem) > 1) {
428 2
            $this->db->executeCommand('HDEL', $updateEmptyItem);
429 2
        }
430
431
432 2
        $this->db->executeCommand('EXEC');
433 2
        return true;
434
435
    }
436
437
    /**
438
     * @param string $oldName old mapping name
439
     * @param string $newName new mapping name
440
     * @param string $guid unique ID
441
     * @since XXX
442
     */
443 2
    private function remap($oldName, $newName, $guid, $isRule = false)
444
    {
445 2
        if ($isRule) {
446 1
            $mappingKey = $this->getRuleMappingKey();
447 1
            $mappingGuidKey = $this->getRuleMappingGuidKey();
448 1
        } else {
449 2
            $mappingKey = $this->getItemMappingKey();
450 2
            $mappingGuidKey = $this->getItemMappingGuidKey();
451
        }
452 2
        $this->db->executeCommand('HDEL', [$mappingKey, $oldName]);
453
        // add new mapping
454 2
        $this->db->executeCommand('HSET', [$mappingKey, $newName, $guid]);
455 2
        $this->db->executeCommand('HSET', [$mappingGuidKey, $guid, $newName]);
456 2
    }
457
458
    /**
459
     * @inheritdoc
460
     */
461 3
    protected function removeItem($item)
462
    {
463 3
        $mappingKey = $this->getItemMappingKey();
464
465 3
        $guid = $this->db->executeCommand('HGET', [$mappingKey, $item->name]);
466 3
        $ruleGuid = $this->db->executeCommand('HGET', [$this->getRuleMappingKey(), $item->ruleName]);
467
468 3
        $parentGuids = $this->db->executeCommand('SMEMBERS', [$this->getItemParentsKey($guid)]);
469 3
        $childrenGuids = $this->db->executeCommand('SMEMBERS', [$this->getItemChildrenKey($guid)]);
470
471 3
        $this->db->executeCommand('MULTI');
472
        // delete mapping
473 3
        $this->db->executeCommand('HDEL', [$mappingKey, $item->name]);
474 3
        $this->db->executeCommand('HDEL', [$this->getItemMappingGuidKey(), $guid]);
475
        // delete rule <-> item link
476 3
        $this->db->executeCommand('SREM', [$this->getRuleItemsKey($ruleGuid), $guid]);
477 3
        $this->db->executeCommand('SREM', [$this->getTypeItemsKey($item->type), $guid]);
478
        // detach from hierarchy
479 3
        foreach($parentGuids as $parentGuid) {
480 3
            $this->db->executeCommand('SREM', [$this->getItemChildrenKey($parentGuid), $guid]);
481 3
        }
482
        // detach children
483 3
        foreach($childrenGuids as $childGuid) {
484 1
            $this->db->executeCommand('SREM', [$this->getItemParentsKey($childGuid), $guid]);
485 3
        }
486 3
        $this->db->executeCommand('DEL', [$this->getItemParentsKey($guid)]);
487 3
        $this->db->executeCommand('DEL', [$this->getItemChildrenKey($guid)]);
488
        // delete rule
489 3
        $this->db->executeCommand('DEL', [$this->getItemKey($guid)]);
490 3
        $this->db->executeCommand('EXEC');
491 3
        return true;
492
493
    }
494
495
    /**
496
     * @inheritdoc
497
     */
498 22
    protected function addRule($rule)
499
    {
500 22
        if(empty($rule->name) === true) {
501 1
            throw new InvalidParamException("Rule name must be defined");
502
        }
503 22
        $ruleExists = (int)$this->db->executeCommand('HEXISTS', [$this->getRuleMappingKey(), $rule->name]);
504 22
        if ($ruleExists === 1)
505 22
        {
506 1
            throw new DuplicateKeyException("Rule '{$rule->name}' already defined");
507
        }
508 22
        $guid = Guid::v4();
509 22
        $time = time();
510 22
        if ($rule->createdAt === null) {
511 22
            $rule->createdAt = $time;
512 22
        }
513 22
        if ($rule->updatedAt === null) {
514 22
            $rule->updatedAt = $time;
515 22
        }
516 22
        $this->db->executeCommand('MULTI');
517 22
        $this->db->executeCommand('HSET', [$this->getRuleMappingKey(), $rule->name, $guid]);
518 22
        $this->db->executeCommand('HSET', [$this->getRuleMappingGuidKey(),$guid, $rule->name]);
519 22
        $this->db->executeCommand('HMSET', [$this->getRuleKey($guid),
520 22
            'data', serialize($rule),
521 22
            'createdAt', $rule->createdAt,
522 22
            'updatedAt', $rule->updatedAt
523 22
        ]);
524
525 22
        $this->db->executeCommand('EXEC');
526 22
        return true;
527
528
    }
529
530
    /**
531
     * @inheritdoc
532
     */
533 1
    protected function updateRule($name, $rule)
534
    {
535 1
        $rule->updatedAt = time();
536
537 1
        $guid = $this->db->executeCommand('HGET', [$this->getRuleMappingKey(), $name]);
538
539 1
        $this->db->executeCommand('MULTI');
540 1
        if ($name !== $rule->name) {
541 1
            $this->remap($name, $rule->name, $guid, true);
542 1
        }
543 1
        $this->db->executeCommand('HMSET', [$this->getRuleKey($guid),
544 1
            'data', serialize($rule),
545 1
            'createdAt', $rule->createdAt,
546 1
            'updatedAt', $rule->updatedAt
547 1
        ]);
548
549 1
        $this->db->executeCommand('EXEC');
550 1
        return true;
551
    }
552
553
    /**
554
     * @inheritdoc
555
     */
556 2
    protected function removeRule($rule)
557
    {
558 2
        $guid = $this->db->executeCommand('HGET', [$this->getRuleMappingKey(), $rule->name]);
559
560 2
        $ruleMembers = $this->db->executeCommand('SMEMBERS', [$this->getRuleItemsKey($guid)]);
561
562 2
        $this->db->executeCommand('MULTI');
563
        // delete mapping
564 2
        $this->db->executeCommand('HDEL', [$this->getRuleMappingKey(), $rule->name]);
565 2
        $this->db->executeCommand('HDEL', [$this->getRuleMappingGuidKey(), $guid]);
566
        // detach items
567 2
        foreach($ruleMembers as $itemGuid)
568
        {
569
            $this->db->executeCommand('HDEL', [$this->getItemKey($itemGuid), 'ruleGuid']);
570 2
        }
571
        // delete rule <-> item link
572 2
        $this->db->executeCommand('DEL', [$this->getRuleItemsKey($guid)]);
573
        // delete rule
574 2
        $this->db->executeCommand('DEL', [$this->getRuleKey($guid)]);
575 2
        $this->db->executeCommand('EXEC');
576 2
        return true;
577
    }
578
579
    /**
580
     * @inheritdoc
581
     */
582 5
    public function getRules()
583
    {
584 5
        $ruleNames = $this->db->executeCommand('HKEYS', [$this->getRuleMappingKey()]);
585 5
        $rules = [];
586 5
        foreach ($ruleNames as $ruleName)
587
        {
588 4
            $rules[$ruleName] = $this->getRule($ruleName);
589 5
        }
590 5
        return $rules;
591
    }
592
593
    /**
594
     * @inheritdoc
595
     */
596 20
    public function getRule($name)
597
    {
598 20
        $rule = null;
599 20
        $guid = $this->db->executeCommand('HGET', [$this->getRuleMappingKey(), $name]);
600 20
        if ($guid !== null) {
601 20
            $rule = $this->getRuleGuid($guid);
602 20
        } elseif(class_exists($name) === true) {
603 1
            $rule = new $name;
604 3
        } elseif(Yii::$container->has($name) === true) {
605 1
            $rule = Yii::$container->get($name);
606 1
        }
607 20
        return $rule;
608
    }
609
610
    /**
611
     * @param string $guid rule unique ID
612
     * @return Rule
613
     * @since XXX
614
     */
615 20
    protected function getRuleGuid($guid)
616
    {
617 20
        $data = $this->db->executeCommand('HGET', [$this->getRuleKey($guid), 'data']);
618 20
        $rule = unserialize($data);
619 20
        return $rule;
620
    }
621
622
    /**
623
     * @inheritdoc
624
     */
625 24
    public function addChild($parent, $child)
626
    {
627 24
        if ($parent->name === $child->name) {
628 1
            throw new InvalidParamException("Cannot add '{$parent->name}' as a child of itself.");
629
        }
630 24
        if ($parent instanceof Permission && $child instanceof Role) {
631 1
            throw new InvalidParamException('Cannot add a role as a child of a permission.');
632
        }
633 24
        if ($this->detectLoop($parent, $child)) {
634 1
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
635
        }
636
637 24
        list($parentGuid, $childGuid) = $this->db->executeCommand('HMGET', [$this->getItemMappingKey(), $parent->name, $child->name]);
638
639 24
        $this->db->executeCommand('MULTI');
640 24
        $this->db->executeCommand('SADD', [$this->getItemParentsKey($childGuid), $parentGuid]);
641 24
        $this->db->executeCommand('SADD', [$this->getItemChildrenKey($parentGuid), $childGuid]);
642 24
        $this->db->executeCommand('EXEC');
643 24
        return true;
644
    }
645
646
    /**
647
     * @inheritdoc
648
     */
649 1
    public function removeChild($parent, $child)
650
    {
651 1
        list($parentGuid, $childGuid) = $this->db->executeCommand('HMGET', [$this->getItemMappingKey(), $parent->name, $child->name]);
652 1
        $this->db->executeCommand('MULTI');
653 1
        $this->db->executeCommand('SREM', [$this->getItemParentsKey($childGuid), $parentGuid]);
654 1
        $this->db->executeCommand('SREM', [$this->getItemChildrenKey($parentGuid), $childGuid]);
655 1
        $this->db->executeCommand('EXEC');
656 1
        return true;
657
658
    }
659
660
    /**
661
     * @inheritdoc
662
     */
663 1
    public function removeChildren($parent)
664
    {
665 1
        $guid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $parent->name]);
666 1
        $childrenGuids = $this->db->executeCommand('SMEMBERS', [$this->getItemChildrenKey($guid)]);
667
668 1
        $this->db->executeCommand('MULTI');
669 1
        foreach($childrenGuids as $childGuid)
670
        {
671 1
            $this->db->executeCommand('SREM', [$this->getItemParentsKey($childGuid), $guid]);
672 1
        }
673 1
        $this->db->executeCommand('DEL', [$this->getItemChildrenKey($guid)]);
674 1
        $this->db->executeCommand('EXEC');
675 1
        return true;
676
677
    }
678
679
    /**
680
     * @inheritdoc
681
     */
682 24
    public function getChildren($name)
683
    {
684 24
        $guid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $name]);
685 24
        return $this->getChildrenByGuid($guid);
686
687
    }
688
689
    /**
690
     * @param string $guid item guid
691
     * @return Item[]
692
     * @since XXX
693
     */
694 24
    protected function getChildrenByGuid($guid)
695
    {
696 24
        $childrenGuids = $this->db->executeCommand('SMEMBERS', [$this->getItemChildrenKey($guid)]);
697 24
        $children = [];
698 24
        if (count($childrenGuids) > 0) {
699 24
            foreach($childrenGuids as $childGuid) {
700 24
                $children[] = $this->getItemByGuid($childGuid);
701 24
            }
702 24
        }
703 24
        return $children;
704
    }
705
706
    /**
707
     * @inheritdoc
708
     */
709 5
    public function hasChild($parent, $child)
710
    {
711 5
        list($parentGuid, $childGuid) = $this->db->executeCommand('HMGET', [$this->getItemMappingKey(), $parent->name, $child->name]);
712 5
        $result = (int)$this->db->executeCommand('SISMEMBER', [$this->getItemChildrenKey($parentGuid), $childGuid]);
713
714 5
        return $result === 1;
715
716
    }
717
718
    /**
719
     * @inheritdoc
720
     */
721 1
    public function revokeAll($userId)
722
    {
723 1
        if (empty($userId) === true) {
724 1
            return false;
725
        }
726 1
        $roleGuids = $this->db->executeCommand('ZRANGEBYSCORE', [$this->getUserAssignmentsKey($userId), '-inf', '+inf']);
727 1
        $this->db->executeCommand('MULTI');
728 1
        if (count($roleGuids) > 0) {
729 1
            foreach ($roleGuids as $roleGuid) {
730 1
                $this->db->executeCommand('ZREM', [$this->getRoleAssignmentsKey($roleGuid), $userId]);
731 1
            }
732 1
        }
733 1
        $this->db->executeCommand('DEL', [$this->getUserAssignmentsKey($userId)]);
734 1
        $this->db->executeCommand('EXEC');
735 1
        return true;
736
737
    }
738
739
    /**
740
     * @inheritdoc
741
     */
742 1
    public function revoke($role, $userId)
743
    {
744 1
        if (empty($userId) === true) {
745 1
            return false;
746
        }
747 1
        $roleGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $role->name]);
748
749 1
        $this->db->executeCommand('MULTI');
750 1
        $this->db->executeCommand('ZREM', [$this->getUserAssignmentsKey($userId), $roleGuid]);
751 1
        $this->db->executeCommand('ZREM', [$this->getRoleAssignmentsKey($roleGuid), $userId]);
752 1
        $this->db->executeCommand('EXEC');
753 1
        return true;
754
    }
755
756
    /**
757
     * @inheritdoc
758
     */
759 1
    public function removeAllRoles()
760
    {
761 1
        $this->removeAllItems(Item::TYPE_ROLE);
762 1
    }
763
764
    /**
765
     * @inheritdoc
766
     */
767 1
    public function removeAllRules()
768
    {
769 1
        $rules = $this->getRules();
770 1
        foreach($rules as $rule) {
771 1
            $this->removeRule($rule);
772 1
        }
773 1
    }
774
775
    /**
776
     * @inheritdoc
777
     */
778 1
    public function removeAllPermissions()
779
    {
780 1
        $this->removeAllItems(Item::TYPE_PERMISSION);
781 1
    }
782
783
    /**
784
     * @inheritdoc
785
     */
786 31
    public function removeAll()
787
    {
788 31
        $authKeys = [];
789 31
        $nextCursor = 0;
790
        do {
791 31
            list($nextCursor, $keys) = $this->db->executeCommand('SCAN', [$nextCursor, 'MATCH', $this->globalMatchKey]);
792 31
            $authKeys = array_merge($authKeys, $keys);
793
794 31
        } while($nextCursor != 0);
795
796 31
        if (count($authKeys) > 0) {
797 29
            $this->db->executeCommand('DEL', $authKeys);
798 29
        }
799 31
    }
800
801
    /**
802
     * @inheritdoc
803
     */
804 1
    public function removeAllAssignments()
805
    {
806 1
        $roleAssignKey = $this->getRoleAssignmentsKey('*');
807 1
        $userAssignKey = $this->getUserAssignmentsKey('*');
808 1
        $assignmentKeys = [];
809
810 1
        $nextCursor = 0;
811
        do {
812 1
            list($nextCursor, $keys) = $this->db->executeCommand('SCAN', [$nextCursor, 'MATCH', $roleAssignKey]);
813 1
            $assignmentKeys = array_merge($assignmentKeys, $keys);
814
815 1
        } while($nextCursor != 0);
816
817 1
        $nextCursor = 0;
818
        do {
819 1
            list($nextCursor, $keys) = $this->db->executeCommand('SCAN', [$nextCursor, 'MATCH', $userAssignKey]);
820 1
            $assignmentKeys = array_merge($assignmentKeys, $keys);
821
822 1
        } while($nextCursor != 0);
823
824 1
        if (count($assignmentKeys) > 0) {
825 1
            $this->db->executeCommand('DEL', $assignmentKeys);
826 1
        }
827 1
    }
828
829
    /**
830
     * @param integer $type
831
     * @since XXX
832
     */
833 2
    public function removeAllItems($type)
834
    {
835 2
        $items = $this->getItems($type);
836 2
        foreach ($items as $item) {
837 2
            $this->removeItem($item);
838 2
        }
839 2
    }
840
841
    /**
842
     * @inheritdoc
843
     */
844 4
    public function getRolesByUser($userId)
845
    {
846 4
        if (!isset($userId) || $userId === '') {
847 1
            return [];
848
        }
849 4
        $roleGuids = $this->db->executeCommand('ZRANGEBYSCORE', [$this->getUserAssignmentsKey($userId), '-inf', '+inf']);
850 4
        $roles = [];
851 4
        if (count($roleGuids) > 0) {
852 4
            foreach ($roleGuids as $roleGuid) {
853 4
                $isRole = (int)$this->db->executeCommand('SISMEMBER', [$this->getTypeItemsKey(Item::TYPE_ROLE), $roleGuid]);
854 4
                if ($isRole === 1) {
855 4
                    $item = $this->getItemByGuid($roleGuid);
856 4
                    $roles[$item->name] = $item;
857 4
                }
858 4
            }
859 4
        }
860
861 4
        return $roles;
862
    }
863
864
    /**
865
     * @inheritdoc
866
     */
867 2
    public function getUserIdsByRole($roleName)
868
    {
869 2
        if (empty($roleName)) {
870 1
            return [];
871
        }
872 1
        $roleGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $roleName]);
873 1
        $userIds = [];
874 1
        if ($roleGuid !== null) {
875 1
            $userIds = $this->db->executeCommand('ZRANGEBYSCORE', [$this->getRoleAssignmentsKey($roleGuid), '-inf', '+inf']);
876 1
        }
877 1
        return $userIds;
878
879
    }
880
881
    /**
882
     * @inheritdoc
883
     */
884 20
    public function assign($role, $userId)
885
    {
886 20
        $assignment = new Assignment([
887 20
            'userId' => $userId,
888 20
            'roleName' => $role->name,
889 20
            'createdAt' => time(),
890 20
        ]);
891 20
        $roleGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $role->name]);
892
893 20
        $this->db->executeCommand('MULTI');
894 20
        $this->db->executeCommand('ZADD', [$this->getUserAssignmentsKey($userId), $assignment->createdAt, $roleGuid]);
895 20
        $this->db->executeCommand('ZADD', [$this->getRoleAssignmentsKey($roleGuid), $assignment->createdAt, $userId]);
896 20
        $this->db->executeCommand('EXEC');
897 20
        return $assignment;
898
    }
899
900
    /**
901
     * @inheritdoc
902
     */
903 1
    public function getPermissionsByUser($userId)
904
    {
905 1
        $rolesGuids = $this->db->executeCommand('ZRANGEBYSCORE', [$this->getUserAssignmentsKey($userId), '-inf', '+inf']);
906 1
        $permissions = [];
907 1
        if (count($rolesGuids) > 0) {
908 1
            $permissionsGuid = [];
909 1
            foreach($rolesGuids as $roleGuid) {
910 1
                $isPerm = (int)$this->db->executeCommand('SISMEMBER', [$this->getTypeItemsKey(Item::TYPE_PERMISSION), $roleGuid]);
911 1
                if ($isPerm === 1) {
912 1
                    $permissionsGuid[] = $roleGuid;
913 1
                }
914 1
            }
915 1
            foreach ($rolesGuids as $roleGuid) {
916 1
                list(, $permGuids) = $this->getChildrenRecursiveGuid($roleGuid, Item::TYPE_PERMISSION);
917 1
                $permissionsGuid = array_merge($permissionsGuid, $permGuids);
918 1
            }
919 1
            foreach($permissionsGuid as $permissionGuid) {
920 1
                $item = $this->getItemByGuid($permissionGuid);
921 1
                $permissions[$item->name] = $item;
922 1
            }
923 1
        }
924 1
        return $permissions;
925
    }
926
927
    /**
928
     * @inheritdoc
929
     */
930 1
    public function getChildRoles($roleName)
931
    {
932 1
        $roleGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $roleName]);
933 1
        if (is_null($roleGuid)) {
934
            throw new InvalidParamException("Role \"$roleName\" not found.");
935
        }
936 1
        list(, $rolesGuid) = $this->getChildrenRecursiveGuid($roleGuid, Item::TYPE_ROLE);
937
938 1
        $roles[$roleName] = $this->getRole($roleName);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$roles was never initialized. Although not strictly required by PHP, it is generally a good practice to add $roles = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
939 1
        foreach($rolesGuid as $roleGuid) {
940 1
            $item = $this->getItemByGuid($roleGuid);
941 1
            $roles[$item->name] = $item;
942 1
        }
943 1
        return $roles;
0 ignored issues
show
Best Practice introduced by
The expression return $roles; seems to be an array, but some of its elements' types (null) are incompatible with the return type declared by the interface yii\rbac\ManagerInterface::getChildRoles of type yii\rbac\Role[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
944
    }
945
946
    /**
947
     * @inheritdoc
948
     */
949 1
    public function getPermissionsByRole($roleName)
950
    {
951 1
        $roleGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $roleName]);
952 1
        $permissions = [];
953 1
        list(, $permissionsGuid) = $this->getChildrenRecursiveGuid($roleGuid, Item::TYPE_PERMISSION);
954 1
        foreach($permissionsGuid as $permissionGuid) {
955 1
            $item = $this->getItemByGuid($permissionGuid);
956 1
            $permissions[$item->name] = $item;
957 1
        }
958 1
        return $permissions;
959
    }
960
961
    /**
962
     * @param string $itemGuid item unique ID
963
     * @param integer $type Item type
964
     * @return array
965
     * @since XXX
966
     */
967 3
    protected function getChildrenRecursiveGuid($itemGuid, $type)
968
    {
969 3
        $childrenGuid = $this->db->executeCommand('SMEMBERS', [$this->getItemChildrenKey($itemGuid)]);
970 3
        $typedChildrenGuid = $this->db->executeCommand('SINTER', [$this->getItemChildrenKey($itemGuid), $this->getTypeItemsKey($type)]);
971 3
        foreach($childrenGuid as $childGuid) {
972 3
            list($subChildrenGuid, $subTypedChildrenGuid) = $this->getChildrenRecursiveGuid($childGuid, $type);
973 3
            $childrenGuid = array_merge($childrenGuid, $subChildrenGuid);
974 3
            $typedChildrenGuid = array_merge($typedChildrenGuid, $subTypedChildrenGuid);
975 3
        }
976 3
        return [$childrenGuid, $typedChildrenGuid];
977
    }
978
979
    /**
980
     * @param Item $parent parent item
981
     * @param Item $child child item
982
     * @return bool
983
     * @since XXX
984
     */
985 24
    protected function detectLoop(Item $parent, Item $child)
986
    {
987 24
        if ($child->name === $parent->name) {
988 2
            return true;
989
        }
990 24
        foreach ($this->getChildren($child->name) as $grandchild) {
991 20
            if ($this->detectLoop($parent, $grandchild)) {
992 2
                return true;
993
            }
994 24
        }
995 24
        return false;
996
    }
997
998
    /**
999
     * @param string $itemName item name
1000
     * @return array
1001
     * @since XXX
1002
     */
1003 1
    public function getParents($itemName)
1004
    {
1005 1
        $itemGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $itemName]);
1006 1
        return $this->getParentsGuid($itemGuid);
1007
    }
1008
1009 1
    protected function getParentsGuid($itemGuid)
1010
    {
1011 1
        $parentsGuid = $this->db->executeCommand('SMEMBERS', [$this->getItemParentsKey($itemGuid)]);
1012 1
        $parents = [];
1013 1
        if (count($parentsGuid) > 0) {
1014 1
            array_unshift($parentsGuid, $this->getItemMappingGuidKey());
1015 1
            $parents = $this->db->executeCommand('HMGET', $parentsGuid);
1016 1
        }
1017 1
        return $parents;
1018
    }
1019
1020
    /**
1021
     * @inheritdoc
1022
     */
1023 3
    public function getAssignments($userId)
1024
    {
1025 3
        $roleGuids = $this->db->executeCommand('ZRANGEBYSCORE', [$this->getUserAssignmentsKey($userId), '-inf', '+inf', 'WITHSCORES']);
1026 3
        $assignments = [];
1027 3
        if (count($roleGuids) > 0) {
1028 3
            $guids = [];
1029 3
            $dates = [];
1030 3
            $nbRolesGuids = count($roleGuids);
1031 3
            for($i=0; $i < $nbRolesGuids; $i = $i + 2) {
1032 3
                $guids[] = $roleGuids[$i];
1033 3
                $dates[] = $roleGuids[($i + 1)];
1034 3
            }
1035 3
            array_unshift($guids, $this->getItemMappingGuidKey());
1036 3
            $names = $this->db->executeCommand('HMGET', $guids);
1037 3
            foreach ($names as $i => $name) {
1038 3
                $assignments[$name] = new Assignment([
1039 3
                    'userId' => $userId,
1040 3
                    'roleName' => $name,
1041 3
                    'createdAt' => $dates[$i],
1042 3
                ]);
1043 3
            }
1044 3
        }
1045
1046 3
        return $assignments;
1047
1048
    }
1049
1050
    /**
1051
     * @inheritdoc
1052
     */
1053
    public function getAssignment($roleName, $userId)
1054
    {
1055
        $roleGuid = $this->db->executeCommand('HGET', [$this->getItemMappingKey(), $roleName]);
1056
        $assignment = null;
1057
        if ($roleGuid !== null) {
1058
            $assignmentScore = $this->db->executeCommand('ZSCORE', [$this->getUserAssignmentsKey($userId), $roleGuid]);
1059
            if ($assignmentScore !== null) {
1060
                $assignment = new Assignment([
1061
                    'userId' => $userId,
1062
                    'roleName' => $roleName,
1063
                    'createdAt' => $assignmentScore,
1064
                ]);
1065
            }
1066
        }
1067
        return $assignment;
1068
    }
1069
1070
    /**
1071
     * @inheritdoc
1072
     */
1073 2
    public function checkAccess($userId, $permissionName, $params = [])
1074
    {
1075 2
        $assignments = $this->getAssignments($userId);
1076 2
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
1077
    }
1078
1079
1080
    /**
1081
     * @param Item $parent parent item
1082
     * @param Item $child child item
1083
     * @return bool
1084
     * @since XXX
1085
     */
1086 1
    public function canAddChild($parent, $child)
1087
    {
1088 1
        return !$this->detectLoop($parent, $child);
1089
    }
1090
1091
    /**
1092
     * @param string|integer $user user ID
1093
     * @param string $itemName current item name
1094
     * @param array $params parameters applied in rule
1095
     * @param Assignment[] $assignments
1096
     * @return bool
1097
     * @since XXX
1098
     * @throws \yii\base\InvalidConfigException
1099
     */
1100 2
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
1101
    {
1102 2
        if (($item = $this->getItem($itemName)) === null) {
1103 1
            return false;
1104
        }
1105 2
        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
1106 2
        if (!$this->executeRule($user, $item, $params)) {
1107 2
            return false;
1108
        }
1109 2
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
1110 2
            return true;
1111
        }
1112 1
        $parents = $this->getParents($itemName);
1113 1
        foreach ($parents as $parent) {
1114 1
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
1115 1
                return true;
1116
            }
1117 1
        }
1118 1
        return false;
1119
    }
1120
1121
    /**
1122
     * @param array $dataRow
1123
     * @return Permission|Role
1124
     * @since XXX
1125
     */
1126 25
    protected function populateItem($dataRow)
1127
    {
1128 25
        $class = $dataRow['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
1129 25
        if (!isset($dataRow['data']) || ($data = @unserialize($dataRow['data'])) === false) {
1130
            $data = null;
1131
        }
1132
1133 25
        return new $class([
1134 25
            'name' => $dataRow['name'],
1135 25
            'type' => $dataRow['type'],
1136 25
            'description' => isset($dataRow['description']) ? $dataRow['description'] : null,
1137 25
            'ruleName' => isset($dataRow['ruleName']) ? $dataRow['ruleName'] : null,
1138 25
            'data' => $data,
1139 25
            'createdAt' => $dataRow['createdAt'],
1140 25
            'updatedAt' => $dataRow['updatedAt'],
1141 25
        ]);
1142
    }
1143
}
1144