Passed
Push — master ( 7f699f...e24ab0 )
by Kirill
03:06
created

PermissionManager::findRule()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 15
rs 10
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Security;
13
14
use Spiral\Core\Container\SingletonInterface;
15
use Spiral\Security\Exception\PermissionException;
16
use Spiral\Security\Exception\RoleException;
17
use Spiral\Security\Rule\AllowRule;
18
use Spiral\Security\Rule\ForbidRule;
19
20
/**
21
 * Default implementation of associations repository and manager. Provides ability to set
22
 * permissions in bulk using * syntax.
23
 *
24
 * Attention, this class is serializable and can be cached in memory.
25
 *
26
 * Example:
27
 * $associations->associate('admin', '*');
28
 * $associations->associate('editor', 'posts.*', Allows::class);
29
 * $associations->associate('user', 'posts.*', Forbid::class);
30
 */
31
final class PermissionManager implements PermissionsInterface, SingletonInterface
32
{
33
    /**
34
     * Roles associated with their permissions.
35
     *
36
     * @var array
37
     */
38
    private $permissions = [];
39
40
    /** @var Matcher */
41
    private $matcher;
42
43
    /**@var RulesInterface */
44
    private $rules;
45
46
    /** @var string */
47
    private $defaultRule = ForbidRule::class;
48
49
    /**
50
     * @param RulesInterface $rules
51
     * @param string         $defaultRule
52
     */
53
    public function __construct(RulesInterface $rules, string $defaultRule = ForbidRule::class)
54
    {
55
        $this->matcher = new Matcher();
56
        $this->rules = $rules;
57
        $this->defaultRule = $defaultRule;
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function hasRole(string $role): bool
64
    {
65
        return array_key_exists($role, $this->permissions);
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     *
71
     * @return $this
72
     */
73
    public function addRole(string $role): PermissionManager
74
    {
75
        if ($this->hasRole($role)) {
76
            throw new RoleException("Role '{$role}' already exists");
77
        }
78
79
        $this->permissions[$role] = [
80
            //No associated permissions
81
        ];
82
83
        return $this;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     *
89
     * @return $this
90
     */
91
    public function removeRole(string $role): PermissionManager
92
    {
93
        if (!$this->hasRole($role)) {
94
            throw new RoleException("Undefined role '{$role}'");
95
        }
96
97
        unset($this->permissions[$role]);
98
99
        return $this;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function getRoles(): array
106
    {
107
        return array_keys($this->permissions);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function getPermissions(string $role): array
114
    {
115
        if (!$this->hasRole($role)) {
116
            throw new RoleException("Undefined role '{$role}'");
117
        }
118
119
        return $this->permissions[$role];
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function getRule(string $role, string $permission): RuleInterface
126
    {
127
        if (!$this->hasRole($role)) {
128
            throw new RoleException("Undefined role '{$role}'");
129
        }
130
131
        //Behaviour points to rule
132
        return $this->rules->get($this->findRule($role, $permission));
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     *
138
     * @return $this|self
139
     */
140
    public function associate(string $role, string $permission, string $rule = AllowRule::class): PermissionManager
141
    {
142
        if (!$this->hasRole($role)) {
143
            throw new RoleException("Undefined role '{$role}'");
144
        }
145
146
        if (!$this->rules->has($rule)) {
147
            throw new PermissionException("Undefined rule '{$rule}'");
148
        }
149
150
        $this->permissions[$role][$permission] = $rule;
151
152
        return $this;
153
    }
154
155
    /**
156
     * Associate role/permission with Forbid rule.
157
     *
158
     * @param string $role
159
     * @param string $permission
160
     * @return $this|self
161
     *
162
     * @throws RoleException
163
     * @throws PermissionException
164
     */
165
    public function deassociate(string $role, string $permission): PermissionManager
166
    {
167
        return $this->associate($role, $permission, ForbidRule::class);
168
    }
169
170
    /**
171
     * @param string $role
172
     * @param string $permission
173
     * @return string
174
     *
175
     * @throws PermissionException
176
     */
177
    private function findRule(string $role, string $permission): string
178
    {
179
        if (isset($this->permissions[$role][$permission])) {
180
            //O(1) check
181
            return $this->permissions[$role][$permission];
182
        }
183
184
        //Matching using star syntax
185
        foreach ($this->permissions[$role] as $pattern => $rule) {
186
            if ($this->matcher->matches($permission, $pattern)) {
187
                return $rule;
188
            }
189
        }
190
191
        return $this->defaultRule;
192
    }
193
}
194