Issues (910)

framework/filters/AccessRule.php (1 issue)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\filters;
9
10
use Closure;
11
use yii\base\Action;
12
use yii\base\Component;
13
use yii\base\Controller;
14
use yii\base\InvalidConfigException;
15
use yii\helpers\IpHelper;
16
use yii\helpers\StringHelper;
17
use yii\web\Request;
18
use yii\web\User;
19
20
/**
21
 * This class represents an access rule defined by the [[AccessControl]] action filter.
22
 *
23
 * @author Qiang Xue <[email protected]>
24
 * @since 2.0
25
 */
26
class AccessRule extends Component
27
{
28
    /**
29
     * @var bool whether this is an 'allow' rule or 'deny' rule.
30
     */
31
    public $allow = false;
32
    /**
33
     * @var array|null list of action IDs that this rule applies to. The comparison is case-sensitive.
34
     * If not set or empty, it means this rule applies to all actions.
35
     */
36
    public $actions;
37
    /**
38
     * @var array|null list of the controller IDs that this rule applies to.
39
     *
40
     * The comparison uses [[\yii\base\Controller::uniqueId]], so each controller ID is prefixed
41
     * with the module ID (if any). For a `product` controller in the application, you would specify
42
     * this property like `['product']` and if that controller is located in a `shop` module, this
43
     * would be `['shop/product']`.
44
     *
45
     * The comparison is case-sensitive.
46
     *
47
     * If not set or empty, it means this rule applies to all controllers.
48
     *
49
     * Since version 2.0.12 controller IDs can be specified as wildcards, e.g. `module/*`.
50
     */
51
    public $controllers;
52
    /**
53
     * @var array|null list of roles that this rule applies to (requires properly configured User component).
54
     * Two special roles are recognized, and they are checked via [[User::isGuest]]:
55
     *
56
     * - `?`: matches a guest user (not authenticated yet)
57
     * - `@`: matches an authenticated user
58
     *
59
     * If you are using RBAC (Role-Based Access Control), you may also specify role names.
60
     * In this case, [[User::can()]] will be called to check access.
61
     *
62
     * Note that it is preferred to check for permissions instead.
63
     *
64
     * If this property is not set or empty, it means this rule applies regardless of roles.
65
     * @see permissions
66
     * @see roleParams
67
     */
68
    public $roles;
69
    /**
70
     * @var array|null list of RBAC (Role-Based Access Control) permissions that this rules applies to.
71
     * [[User::can()]] will be called to check access.
72
     *
73
     * If this property is not set or empty, it means this rule applies regardless of permissions.
74
     * @since 2.0.12
75
     * @see roles
76
     * @see roleParams
77
     */
78
    public $permissions;
79
    /**
80
     * @var array|Closure parameters to pass to the [[User::can()]] function for evaluating
81
     * user permissions in [[$roles]].
82
     *
83
     * If this is an array, it will be passed directly to [[User::can()]]. For example for passing an
84
     * ID from the current request, you may use the following:
85
     *
86
     * ```php
87
     * ['postId' => Yii::$app->request->get('id')]
88
     * ```
89
     *
90
     * You may also specify a closure that returns an array. This can be used to
91
     * evaluate the array values only if they are needed, for example when a model needs to be
92
     * loaded like in the following code:
93
     *
94
     * ```php
95
     * 'rules' => [
96
     *     [
97
     *         'allow' => true,
98
     *         'actions' => ['update'],
99
     *         'roles' => ['updatePost'],
100
     *         'roleParams' => function($rule) {
101
     *             return ['post' => Post::findOne(Yii::$app->request->get('id'))];
102
     *         },
103
     *     ],
104
     * ],
105
     * ```
106
     *
107
     * A reference to the [[AccessRule]] instance will be passed to the closure as the first parameter.
108
     *
109
     * @see roles
110
     * @since 2.0.12
111
     */
112
    public $roleParams = [];
113
    /**
114
     * @var array|null list of user IP addresses that this rule applies to. An IP address
115
     * can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix.
116
     * For example, '192.168.*' matches all IP addresses in the segment '192.168.'.
117
     * It may also contain a pattern/mask like '172.16.0.0/12' which would match all IPs from the
118
     * 20-bit private network block in RFC1918.
119
     * If not set or empty, it means this rule applies to all IP addresses.
120
     * @see Request::userIP
121
     * @see IpHelper::inRange
122
     */
123
    public $ips;
124
    /**
125
     * @var array|null list of request methods (e.g. `GET`, `POST`) that this rule applies to.
126
     * If not set or empty, it means this rule applies to all request methods.
127
     * @see \yii\web\Request::method
128
     */
129
    public $verbs;
130
    /**
131
     * @var callable a callback that will be called to determine if the rule should be applied.
132
     * The signature of the callback should be as follows:
133
     *
134
     * ```php
135
     * function ($rule, $action)
136
     * ```
137
     *
138
     * where `$rule` is this rule, and `$action` is the current [[Action|action]] object.
139
     * The callback should return a boolean value indicating whether this rule should be applied.
140
     */
141
    public $matchCallback;
142
    /**
143
     * @var callable|null a callback that will be called if this rule determines the access to
144
     * the current action should be denied. This is the case when this rule matches
145
     * and [[$allow]] is set to `false`.
146
     *
147
     * If not set, the behavior will be determined by [[AccessControl]],
148
     * either using [[AccessControl::denyAccess()]]
149
     * or [[AccessControl::$denyCallback]], if configured.
150
     *
151
     * The signature of the callback should be as follows:
152
     *
153
     * ```php
154
     * function ($rule, $action)
155
     * ```
156
     *
157
     * where `$rule` is this rule, and `$action` is the current [[Action|action]] object.
158
     * @see AccessControl::$denyCallback
159
     */
160
    public $denyCallback;
161
162
163
    /**
164
     * Checks whether the Web user is allowed to perform the specified action.
165
     * @param Action $action the action to be performed
166
     * @param User|false $user the user object or `false` in case of detached User component
167
     * @param Request $request
168
     * @return bool|null `true` if the user is allowed, `false` if the user is denied, `null` if the rule does not apply to the user
169
     */
170 31
    public function allows($action, $user, $request)
171
    {
172
        if (
173 31
            $this->matchAction($action)
174 31
            && $this->matchRole($user)
175 30
            && $this->matchIP($request->getUserIP())
176 30
            && $this->matchVerb($request->getMethod())
177 30
            && $this->matchController($action->controller)
178 30
            && $this->matchCustom($action)
179
        ) {
180 20
            return $this->allow ? true : false;
181
        }
182
183 19
        return null;
184
    }
185
186
    /**
187
     * @param Action $action the action
188
     * @return bool whether the rule applies to the action
189
     */
190 31
    protected function matchAction($action)
191
    {
192 31
        return empty($this->actions) || in_array($action->id, $this->actions, true);
193
    }
194
195
    /**
196
     * @param Controller $controller the controller
197
     * @return bool whether the rule applies to the controller
198
     */
199 20
    protected function matchController($controller)
200
    {
201 20
        if (empty($this->controllers)) {
202 18
            return true;
203
        }
204
205 2
        $id = $controller->getUniqueId();
206 2
        foreach ($this->controllers as $pattern) {
207 2
            if (StringHelper::matchWildcard($pattern, $id)) {
208 2
                return true;
209
            }
210
        }
211
212 2
        return false;
213
    }
214
215
    /**
216
     * @param User $user the user object
217
     * @return bool whether the rule applies to the role
218
     * @throws InvalidConfigException if User component is detached
219
     */
220 31
    protected function matchRole($user)
221
    {
222 31
        $items = empty($this->roles) ? [] : $this->roles;
223
224 31
        if (!empty($this->permissions)) {
225 1
            $items = array_merge($items, $this->permissions);
226
        }
227
228 31
        if (empty($items)) {
229 8
            return true;
230
        }
231
232 24
        if ($user === false) {
0 ignored issues
show
The condition $user === false is always false.
Loading history...
233 1
            throw new InvalidConfigException('The user application component must be available to specify roles in AccessRule.');
234
        }
235
236 23
        foreach ($items as $item) {
237 23
            if ($item === '?') {
238 1
                if ($user->getIsGuest()) {
239 1
                    return true;
240
                }
241 23
            } elseif ($item === '@') {
242 1
                if (!$user->getIsGuest()) {
243 1
                    return true;
244
                }
245
            } else {
246 22
                if (!isset($roleParams)) {
247 22
                    $roleParams = !is_array($this->roleParams) && is_callable($this->roleParams) ? call_user_func($this->roleParams, $this) : $this->roleParams;
248
                }
249 22
                if ($user->can($item, $roleParams)) {
250 12
                    return true;
251
                }
252
            }
253
        }
254
255 12
        return false;
256
    }
257
258
    /**
259
     * @param string|null $ip the IP address
260
     * @return bool whether the rule applies to the IP address
261
     */
262 20
    protected function matchIP($ip)
263
    {
264 20
        if (empty($this->ips)) {
265 18
            return true;
266
        }
267 3
        foreach ($this->ips as $rule) {
268
            if (
269 3
                $rule === '*'
270 3
                || $rule === $ip
271
                || (
272 3
                    $ip !== null
273 3
                    && ($pos = strpos($rule, '*')) !== false
274 3
                    && strncmp($ip, $rule, $pos) === 0
275
                )
276
                || (
277 3
                    strpos($rule, '/') !== false
278 3
                    && IpHelper::inRange($ip, $rule) === true
279
                )
280
            ) {
281 3
                return true;
282
            }
283
        }
284
285 3
        return false;
286
    }
287
288
    /**
289
     * @param string $verb the request method.
290
     * @return bool whether the rule applies to the request
291
     */
292 20
    protected function matchVerb($verb)
293
    {
294 20
        return empty($this->verbs) || in_array(strtoupper($verb), array_map('strtoupper', $this->verbs), true);
295
    }
296
297
    /**
298
     * @param Action $action the action to be performed
299
     * @return bool whether the rule should be applied
300
     */
301 20
    protected function matchCustom($action)
302
    {
303 20
        return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action);
304
    }
305
}
306