1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* BEdita, API-first content management framework |
4
|
|
|
* Copyright 2019 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 Cake\Auth\BaseAuthorize; |
19
|
|
|
use Cake\Datasource\ModelAwareTrait; |
20
|
|
|
use Cake\Http\ServerRequest; |
21
|
|
|
use Cake\Utility\Hash; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Provide authorization on a per-endpoint basis. |
25
|
|
|
* |
26
|
|
|
* @since 4.0.0 |
27
|
|
|
* @property \BEdita\Core\Model\Table\EndpointsTable $Endpoints |
28
|
|
|
* @property \BEdita\Core\Model\Table\EndpointPermissionsTable $EndpointPermissions |
29
|
|
|
*/ |
30
|
|
|
class EndpointAuthorize extends BaseAuthorize |
31
|
|
|
{ |
32
|
|
|
use ModelAwareTrait; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* {@inheritDoc} |
36
|
|
|
* |
37
|
|
|
* If 'blockAnonymousUsers' is true no access will be granted |
38
|
|
|
* to unauthenticated users otherwise authorization check is performed |
39
|
|
|
* If 'defaultAuthorized' is set current request is authorized |
40
|
|
|
* unless a specific permission is set. |
41
|
|
|
*/ |
42
|
|
|
protected $_defaultConfig = [ |
43
|
|
|
'blockAnonymousUsers' => true, |
44
|
|
|
'defaultAuthorized' => false, |
45
|
|
|
]; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Cache result of `authorized()` method call. |
49
|
|
|
* |
50
|
|
|
* This is required for controller to know whether authorization was granted on all contents, |
51
|
|
|
* or only on those that belong to the current user. Whatever that means, it is controller's |
52
|
|
|
* responsibility to interpret, as it may vary. Some controller may also decide to ignore this |
53
|
|
|
* fine-grained authorization level. |
54
|
|
|
* |
55
|
|
|
* @var bool|string |
56
|
|
|
*/ |
57
|
|
|
protected $authorized; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @inheritDoc |
61
|
|
|
*/ |
62
|
|
|
public function authorize($user, ServerRequest $request): bool |
63
|
|
|
{ |
64
|
|
|
// if 'blockAnonymousUsers' configuration is true and user unlogged authorization is denied |
65
|
|
|
if ( |
66
|
|
|
!$this->getConfig('defaultAuthorized') && |
67
|
|
|
$this->isAnonymous($user) && |
68
|
|
|
$this->getConfig('blockAnonymousUsers') |
69
|
|
|
) { |
70
|
|
|
$this->unauthenticated(); |
71
|
|
|
} |
72
|
|
|
$this->loadModel('Endpoints'); |
73
|
|
|
$this->loadModel('EndpointPermissions'); |
74
|
|
|
|
75
|
|
|
// For anonymous users performing write operations, use strict mode. |
76
|
|
|
$readRequest = $request->is(['get', 'head']); |
77
|
|
|
$strict = ($this->isAnonymous($user) && !$readRequest); |
78
|
|
|
|
79
|
|
|
$endpointId = $this->Endpoints->fetchId($request->getPath()); |
80
|
|
|
$permsCount = $this->EndpointPermissions->fetchCount($endpointId); |
81
|
|
|
|
82
|
|
|
// If request si authorized and no permission is set on it then it is authorized for anyone |
83
|
|
|
if ($this->getConfig('defaultAuthorized') && ($endpointId === null || $permsCount === 0)) { |
84
|
|
|
return $this->authorized = true; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
$permissions = $this->EndpointPermissions->fetchPermissions($endpointId, $user, $strict); |
88
|
|
|
$this->authorized = $this->checkPermissions($permissions, $readRequest); |
89
|
|
|
|
90
|
|
|
if (empty($permissions) && ($endpointId === null || $permsCount === 0)) { |
91
|
|
|
// If no permissions are set for an endpoint, assume the least restrictive permissions possible. |
92
|
|
|
// This does not apply to write operations for anonymous users: those **MUST** be explicitly allowed. |
93
|
|
|
$this->authorized = !$strict; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
// if 'administratorOnly' configuration is true logged user must have administrator role |
97
|
|
|
if ($this->authorized && $this->getConfig('administratorOnly')) { |
98
|
|
|
$this->authorized = in_array(RolesTable::ADMIN_ROLE, Hash::extract($user, 'roles.{n}.id')); |
|
|
|
|
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
if ($this->isAnonymous($user) && $this->authorized !== true) { |
102
|
|
|
// Anonymous user should not get a 403. Thus, we invoke authentication provider's |
103
|
|
|
// `unauthenticated()` method. Furthermore, for anonymous users, `mine` doesn't make any sense, |
104
|
|
|
// so we treat that as a non-authorized request. |
105
|
|
|
$this->unauthenticated(); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
// Authorization is granted for both `true` and `'mine'` values. |
109
|
|
|
return !empty($this->authorized); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Perform user unauthentication to return 401 Unauthorized |
114
|
|
|
* instead of 403 Forbidden |
115
|
|
|
* |
116
|
|
|
* @return void |
117
|
|
|
*/ |
118
|
|
|
protected function unauthenticated() |
119
|
|
|
{ |
120
|
|
|
$controller = $this->_registry->getController(); |
121
|
|
|
$controller |
122
|
|
|
->Auth->getAuthenticate('BEdita/API.Jwt') |
123
|
|
|
->unauthenticated($controller->getRequest(), $controller->getResponse()); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Check if user is anonymous. |
128
|
|
|
* |
129
|
|
|
* @param array|\ArrayAccess $user User data. |
130
|
|
|
* @return bool |
131
|
|
|
*/ |
132
|
|
|
public function isAnonymous($user) |
133
|
|
|
{ |
134
|
|
|
return !empty($user['_anonymous']); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Checks if request can be authorized basing on a set of applicable permissions. |
139
|
|
|
* |
140
|
|
|
* @param \BEdita\Core\Model\Entity\EndpointPermission[] $permissions Set of applicable permissions. |
141
|
|
|
* @param bool $readRequest Read request flag. |
142
|
|
|
* @return bool|string |
143
|
|
|
*/ |
144
|
|
|
protected function checkPermissions(array $permissions, bool $readRequest) |
145
|
|
|
{ |
146
|
|
|
$shift = EndpointPermission::PERM_READ; |
147
|
|
|
if (!$readRequest) { |
148
|
|
|
$shift = EndpointPermission::PERM_WRITE; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$result = EndpointPermission::PERM_NO; |
152
|
|
|
foreach ($permissions as $permission) { |
153
|
|
|
$permission = $permission->permission >> $shift & EndpointPermission::PERM_YES; |
154
|
|
|
$result = $result | $permission; |
155
|
|
|
|
156
|
|
|
if ($permission === EndpointPermission::PERM_BLOCK) { |
157
|
|
|
$result = EndpointPermission::PERM_NO; |
158
|
|
|
|
159
|
|
|
break; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
return EndpointPermission::decode($result); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
|