1 | <?php |
||||||
2 | /** |
||||||
3 | * BEdita, API-first content management framework |
||||||
4 | * Copyright 2016 ChannelWeb Srl, Chialab Srl |
||||||
5 | * |
||||||
6 | * This file is part of BEdita: you can redistribute it and/or modify |
||||||
7 | * it under the terms of the GNU Lesser General Public License as published |
||||||
8 | * by the Free Software Foundation, either version 3 of the License, or |
||||||
9 | * (at your option) any later version. |
||||||
10 | * |
||||||
11 | * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details. |
||||||
12 | */ |
||||||
13 | |||||||
14 | namespace BEdita\API\Auth; |
||||||
15 | |||||||
16 | use BEdita\Core\Model\Entity\EndpointPermission; |
||||||
17 | use BEdita\Core\Model\Table\RolesTable; |
||||||
18 | use BEdita\Core\State\CurrentApplication; |
||||||
19 | use Cake\Auth\BaseAuthorize; |
||||||
20 | use Cake\Http\ServerRequest; |
||||||
21 | use Cake\Network\Exception\NotFoundException; |
||||||
22 | use Cake\ORM\TableRegistry; |
||||||
23 | use Cake\Utility\Hash; |
||||||
24 | |||||||
25 | /** |
||||||
26 | * Provide authorization on a per-endpoint basis. |
||||||
27 | * |
||||||
28 | * @since 4.0.0 |
||||||
29 | */ |
||||||
30 | class EndpointAuthorize extends BaseAuthorize |
||||||
31 | { |
||||||
32 | |||||||
33 | /** |
||||||
34 | * {@inheritDoc} |
||||||
35 | * |
||||||
36 | * If 'blockAnonymousUsers' is true no access will be granted |
||||||
37 | * to unauthenticated users otherwise authorization check is performed |
||||||
38 | * If 'defaultAuthorized' is set current request is authorized |
||||||
39 | * unless a specific permission is set. |
||||||
40 | */ |
||||||
41 | protected $_defaultConfig = [ |
||||||
42 | 'blockAnonymousUsers' => true, |
||||||
43 | 'defaultAuthorized' => false, |
||||||
44 | ]; |
||||||
45 | |||||||
46 | /** |
||||||
47 | * Current endpoint entity. |
||||||
48 | * |
||||||
49 | * @var \BEdita\Core\Model\Entity\Endpoint|null |
||||||
50 | */ |
||||||
51 | protected $endpoint = null; |
||||||
52 | |||||||
53 | /** |
||||||
54 | * Request object instance. |
||||||
55 | * |
||||||
56 | * @var \Cake\Http\ServerRequest |
||||||
57 | */ |
||||||
58 | protected $request; |
||||||
59 | |||||||
60 | /** |
||||||
61 | * Cache result of `authorized()` method call. |
||||||
62 | * |
||||||
63 | * This is required for controller to know whether authorization was granted on all contents, |
||||||
64 | * or only on those that belong to the current user. Whatever that means, it is controller's |
||||||
65 | * responsibility to interpret, as it may vary. Some controller may also decide to ignore this |
||||||
66 | * fine-grained authorization level. |
||||||
67 | * |
||||||
68 | * @var bool|string |
||||||
69 | */ |
||||||
70 | protected $authorized; |
||||||
71 | |||||||
72 | /** |
||||||
73 | * {@inheritDoc} |
||||||
74 | */ |
||||||
75 | public function authorize($user, ServerRequest $request) |
||||||
76 | { |
||||||
77 | $this->request = $request; |
||||||
78 | |||||||
79 | // if 'blockAnonymousUsers' configuration is true and user unlogged authorization is denied |
||||||
80 | if (!$this->getConfig('defaultAuthorized') && |
||||||
81 | $this->isAnonymous($user) && |
||||||
82 | $this->getConfig('blockAnonymousUsers')) { |
||||||
83 | $this->unauthenticated(); |
||||||
84 | } |
||||||
85 | |||||||
86 | // For anonymous users performing write operations, use strict mode. |
||||||
87 | $strict = ($this->isAnonymous($user) && !$this->request->is(['get', 'head'])); |
||||||
88 | |||||||
89 | if (empty($this->endpoint)) { |
||||||
90 | $this->getEndpoint(); |
||||||
91 | } |
||||||
92 | |||||||
93 | $permissions = $this->getPermissions($user, $strict)->toArray(); |
||||||
94 | $allPermissions = $this->getPermissions(false); |
||||||
95 | |||||||
96 | // If request si authorized and no permission is set on it then it is authorized for anyone |
||||||
97 | if ($this->getConfig('defaultAuthorized') && ($this->endpoint->isNew() || $allPermissions->count() === 0)) { |
||||||
98 | return $this->authorized = true; |
||||||
99 | } |
||||||
100 | |||||||
101 | $this->authorized = $this->checkPermissions($permissions); |
||||||
102 | if (empty($permissions) && ($this->endpoint->isNew() || $allPermissions->count() === 0)) { |
||||||
103 | // If no permissions are set for an endpoint, assume the least restrictive permissions possible. |
||||||
104 | // This does not apply to write operations for anonymous users: those **MUST** be explicitly allowed. |
||||||
105 | $this->authorized = !$strict; |
||||||
106 | } |
||||||
107 | |||||||
108 | // if 'administratorOnly' configuration is true logged user must have administrator role |
||||||
109 | if ($this->authorized && $this->getConfig('administratorOnly')) { |
||||||
110 | $this->authorized = in_array(RolesTable::ADMIN_ROLE, Hash::extract($user, 'roles.{n}.id')); |
||||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
111 | } |
||||||
112 | |||||||
113 | if ($this->isAnonymous($user) && $this->authorized !== true) { |
||||||
114 | // Anonymous user should not get a 403. Thus, we invoke authentication provider's |
||||||
115 | // `unauthenticated()` method. Furthermore, for anonymous users, `mine` doesn't make any sense, |
||||||
116 | // so we treat that as a non-authorized request. |
||||||
117 | $this->unauthenticated(); |
||||||
118 | } |
||||||
119 | |||||||
120 | // Authorization is granted for both `true` and `'mine'` values. |
||||||
121 | return !empty($this->authorized); |
||||||
122 | } |
||||||
123 | |||||||
124 | /** |
||||||
125 | * Perform user unauthentication to return 401 Unauthorized |
||||||
126 | * instead of 403 Forbidden |
||||||
127 | * |
||||||
128 | * @return void |
||||||
129 | */ |
||||||
130 | protected function unauthenticated() |
||||||
131 | { |
||||||
132 | $controller = $this->_registry->getController(); |
||||||
133 | $controller |
||||||
134 | ->Auth->getAuthenticate('BEdita/API.Jwt') |
||||||
135 | ->unauthenticated($controller->request, $controller->response); |
||||||
0 ignored issues
–
show
It seems like
$controller->response can also be of type null ; however, parameter $response of Cake\Auth\BaseAuthenticate::unauthenticated() does only seem to accept Cake\Http\Response , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() It seems like
$controller->request can also be of type null ; however, parameter $request of Cake\Auth\BaseAuthenticate::unauthenticated() does only seem to accept Cake\Http\ServerRequest , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
136 | } |
||||||
137 | |||||||
138 | /** |
||||||
139 | * Check if user is anonymous. |
||||||
140 | * |
||||||
141 | * @param array|\ArrayAccess $user User data. |
||||||
142 | * @return bool |
||||||
143 | */ |
||||||
144 | public function isAnonymous($user) |
||||||
145 | { |
||||||
146 | return !empty($user['_anonymous']); |
||||||
147 | } |
||||||
148 | |||||||
149 | /** |
||||||
150 | * Get endpoint for request. |
||||||
151 | * |
||||||
152 | * @return \BEdita\Core\Model\Entity\Endpoint |
||||||
153 | * @throws \Cake\Network\Exception\NotFoundException If endpoint is disabled |
||||||
154 | */ |
||||||
155 | protected function getEndpoint() |
||||||
156 | { |
||||||
157 | $endpointName = $this->request->url; |
||||||
158 | if (($slashPos = strpos($endpointName, '/')) !== false) { |
||||||
159 | $endpointName = substr($endpointName, 0, $slashPos); |
||||||
160 | } |
||||||
161 | |||||||
162 | $Endpoints = TableRegistry::get('Endpoints'); |
||||||
163 | $this->endpoint = $Endpoints->find() |
||||||
0 ignored issues
–
show
It seems like
$Endpoints->find()->wher...endpointName))->first() can also be of type array . However, the property $endpoint is declared as type BEdita\Core\Model\Entity\Endpoint|null . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||||||
164 | ->where([ |
||||||
165 | 'Endpoints.name' => $endpointName, |
||||||
166 | ]) |
||||||
167 | ->first(); |
||||||
168 | |||||||
169 | if (!$this->endpoint) { |
||||||
170 | $this->endpoint = $Endpoints->newEntity( |
||||||
171 | [ |
||||||
172 | 'name' => $endpointName, |
||||||
173 | 'enabled' => true, |
||||||
174 | ], |
||||||
175 | ['validate' => false] |
||||||
176 | ); |
||||||
177 | } |
||||||
178 | |||||||
179 | if (!$this->endpoint->enabled) { |
||||||
0 ignored issues
–
show
|
|||||||
180 | throw new NotFoundException(__d('bedita', 'Resource not found.')); |
||||||
181 | } |
||||||
182 | |||||||
183 | return $this->endpoint; |
||||||
0 ignored issues
–
show
|
|||||||
184 | } |
||||||
185 | |||||||
186 | /** |
||||||
187 | * Get list of applicable permissions. |
||||||
188 | * |
||||||
189 | * @param array|\ArrayAccess|false $user Authenticated (or anonymous) user. |
||||||
190 | * @param bool $strict Use strict mode. Do not consider permissions set on all applications/endpoints. |
||||||
191 | * @return \Cake\ORM\Query |
||||||
192 | * @todo Future optimization: Permissions that are `0` on the two bits that are interesting for the current request can be excluded... |
||||||
193 | */ |
||||||
194 | protected function getPermissions($user, $strict = false) |
||||||
195 | { |
||||||
196 | $applicationId = CurrentApplication::getApplicationId(); |
||||||
197 | $endpointIds = $this->endpoint && !$this->endpoint->isNew() ? [$this->endpoint->id] : []; |
||||||
198 | |||||||
199 | $query = TableRegistry::get('EndpointPermissions') |
||||||
200 | ->find('byApplication', compact('applicationId', 'strict')) |
||||||
201 | ->find('byEndpoint', compact('endpointIds', 'strict')); |
||||||
202 | |||||||
203 | if ($user !== false && !$this->isAnonymous($user)) { |
||||||
204 | $roleIds = Hash::extract($user, 'roles.{n}.id'); |
||||||
205 | $query = $query |
||||||
206 | ->find('byRole', compact('roleIds')); |
||||||
207 | } |
||||||
208 | |||||||
209 | return $query; |
||||||
210 | } |
||||||
211 | |||||||
212 | /** |
||||||
213 | * Checks if request can be authorized basing on a set of applicable permissions. |
||||||
214 | * |
||||||
215 | * @param \BEdita\Core\Model\Entity\EndpointPermission[] $permissions Set of applicable permissions. |
||||||
216 | * @return bool|string |
||||||
217 | */ |
||||||
218 | protected function checkPermissions(array $permissions) |
||||||
219 | { |
||||||
220 | $shift = EndpointPermission::PERM_READ; |
||||||
221 | if (!$this->request->is(['get', 'head'])) { |
||||||
222 | $shift = EndpointPermission::PERM_WRITE; |
||||||
223 | } |
||||||
224 | |||||||
225 | $result = EndpointPermission::PERM_NO; |
||||||
226 | foreach ($permissions as $permission) { |
||||||
227 | $permission = $permission->permission >> $shift & EndpointPermission::PERM_YES; |
||||||
228 | $result = $result | $permission; |
||||||
229 | |||||||
230 | if ($permission === EndpointPermission::PERM_BLOCK) { |
||||||
231 | $result = EndpointPermission::PERM_NO; |
||||||
232 | |||||||
233 | break; |
||||||
234 | } |
||||||
235 | } |
||||||
236 | |||||||
237 | return EndpointPermission::decode($result); |
||||||
238 | } |
||||||
239 | } |
||||||
240 |