Completed
Branch feature/pre-split (197e7e)
by Anton
03:26
created

PermissionManager::findRule()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
c 0
b 0
f 0
cc 6
eloc 11
nc 5
nop 2
rs 8.6737
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Security;
9
10
use Spiral\Core\Component;
11
use Spiral\Core\Container\SingletonInterface;
12
use Spiral\Security\Exceptions\PermissionException;
13
use Spiral\Security\Exceptions\RoleException;
14
use Spiral\Support\Patternizer;
15
16
/**
17
 * Default implementation of associations repository and manager. Provides ability to set
18
 * permissions in bulk using * syntax.
19
 *
20
 * Attention, this class is serializable and can be cached in memory.
21
 *
22
 * Example:
23
 * $associations->associate('admin', '*');
24
 * $associations->associate('editor', 'posts.*');
25
 */
26
class PermissionManager extends Component implements PermissionsInterface, SingletonInterface
27
{
28
    /**
29
     * Roles associated with their permissions.
30
     *
31
     * @var array
32
     */
33
    private $associations = [];
34
35
    /**
36
     * Roles deassociated with their permissions.
37
     *
38
     * @var array
39
     */
40
    private $deassociations = [];
41
42
    /**
43
     * @var RulesInterface
44
     */
45
    private $rules = null;
46
47
    /**
48
     * @var Patternizer
49
     */
50
    private $patternizer = null;
51
52
    /**
53
     * @param RulesInterface   $rules
54
     * @param Patternizer|null $patternizer
55
     */
56
    public function __construct(RulesInterface $rules, Patternizer $patternizer)
57
    {
58
        $this->rules = $rules;
59
        $this->patternizer = $patternizer;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function hasRole(string $role): bool
66
    {
67
        return array_key_exists($role, $this->associations);
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     *
73
     * @return $this
74
     */
75 View Code Duplication
    public function addRole(string $role): PermissionManager
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
76
    {
77
        if ($this->hasRole($role)) {
78
            throw new RoleException("Role '{$role}' already exists.");
79
        }
80
81
        $this->associations[$role] = [];
82
83
        return $this;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     *
89
     * @return $this
90
     */
91 View Code Duplication
    public function removeRole(string $role): PermissionManager
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
92
    {
93
        if (!$this->hasRole($role)) {
94
            throw new RoleException("Undefined role '{$role}'.");
95
        }
96
97
        unset($this->associations[$role]);
98
99
        return $this;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function getRoles(): array
106
    {
107
        return array_keys($this->associations);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function getRule(string $role, string $permission)
114
    {
115
        if (!$this->hasRole($role)) {
116
            throw new RoleException("Undefined role '{$role}'");
117
        }
118
119
        $rule = $this->findRule($role, $permission);
120
        if ($rule === GuardInterface::ALLOW || $rule === GuardInterface::UNDEFINED) {
121
            return $rule;
122
        }
123
124
        //Behaviour points to rule
125
        return $this->rules->get($rule);
0 ignored issues
show
Bug introduced by
It seems like $rule defined by $this->findRule($role, $permission) on line 119 can also be of type boolean; however, Spiral\Security\RulesInterface::get() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     *
131
     * @return $this
132
     */
133
    public function associate(
134
        string $role,
135
        string $permission,
136
        $rule = GuardInterface::ALLOW
137
    ): PermissionManager {
138
        if (!$this->hasRole($role)) {
139
            throw new RoleException("Undefined role '{$role}'");
140
        }
141
142
        if ($rule !== GuardInterface::ALLOW) {
143
            if (!$this->rules->has($rule)) {
0 ignored issues
show
Bug introduced by
It seems like $rule defined by parameter $rule on line 136 can also be of type boolean; however, Spiral\Security\RulesInterface::has() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
144
                throw new PermissionException("Invalid permission rule '{$rule}'");
145
            }
146
        }
147
148
        $this->associations[$role][$permission] = $rule;
149
150
        return $this;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     *
156
     * @return $this
157
     */
158
    public function deassociate(string $role, string $permission): PermissionManager
159
    {
160
        if (!$this->hasRole($role)) {
161
            throw new RoleException("Undefined role '{$role}'");
162
        }
163
164
        if (!isset($this->associations[$role][$permission])) {
165
            $this->deassociations[$role][] = $permission;
166
        } else {
167
            unset($this->associations[$role][$permission]);
168
        }
169
170
        return $this;
171
    }
172
173
    /**
174
     * @param string $role
175
     * @param string $permission
176
     *
177
     * @return bool|string
178
     */
179
    private function findRule(string $role, string $permission)
180
    {
181
        if (isset($this->deassociations[$role]) && in_array($permission,
182
                $this->deassociations[$role])
183
        ) {
184
            return GuardInterface::UNDEFINED;
185
        }
186
187
        if (isset($this->associations[$role][$permission])) {
188
            //O(1) check
1 ignored issue
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
189
            return $this->associations[$role][$permission];
190
        } else {
191
            //Checking using star syntax
192
            foreach ($this->associations[$role] as $pattern => $behaviour) {
193
                if ($this->patternizer->matches($permission, $pattern)) {
194
                    return $behaviour;
195
                }
196
            }
197
        }
198
199
        return GuardInterface::UNDEFINED;
200
    }
201
}
202