1
|
|
|
<?php |
2
|
|
|
namespace HierAuth\Auth; |
3
|
|
|
|
4
|
|
|
use Cake\Auth\BaseAuthorize; |
5
|
|
|
use Cake\Cache\Cache; |
6
|
|
|
use Cake\Controller\ComponentRegistry; |
7
|
|
|
use Cake\Core\Exception\Exception; |
8
|
|
|
use Cake\Network\Request; |
9
|
|
|
use Symfony\Component\Yaml\Yaml; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Licensed under The MIT License |
13
|
|
|
* For full copyright and license information, please see the LICENSE.txt |
14
|
|
|
* Redistributions of files must retain the above copyright notice. |
15
|
|
|
* |
16
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php MIT License |
17
|
|
|
*/ |
18
|
|
|
class HierAuthorize extends BaseAuthorize |
19
|
|
|
{ |
20
|
|
|
|
21
|
|
|
protected $_denySign = '-'; |
22
|
|
|
protected $_referenceSign = '@'; |
23
|
|
|
protected $_allSign = 'ALL'; |
24
|
|
|
|
25
|
|
|
protected $_hierarchy; |
26
|
|
|
protected $_acl; |
27
|
|
|
|
28
|
|
|
protected $_rootHierarchy; |
29
|
|
|
|
30
|
|
|
protected $_defaultConfig = [ |
31
|
|
|
'hierarchyFile' => 'hierarchy.yml', |
32
|
|
|
'aclFile' => 'acl.yml', |
33
|
|
|
'roleColumn' => 'roles', |
34
|
|
|
]; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @param ComponentRegistry $registry The controller for this request. |
38
|
|
|
* @param array $config An array of config. This class does not use any config. |
39
|
|
|
*/ |
40
|
|
|
public function __construct(ComponentRegistry $registry, array $config = []) |
41
|
|
|
{ |
42
|
|
|
parent::__construct($registry, $config); |
43
|
|
|
|
44
|
|
|
if (!file_exists(CONFIG . $this->config('hierarchyFile'))) { |
45
|
|
|
throw new Exception( |
46
|
|
|
sprintf("Provided hierarchy config file %s doesn't exist.", $this->config('hierarchyFile')) |
47
|
|
|
); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
if (!file_exists(CONFIG . $this->config('aclFile'))) { |
51
|
|
|
throw new Exception(sprintf("Provided ACL config file %s doesn't exist.", $this->config('aclFile'))); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
// caching |
55
|
|
|
$hierarchyModified = filemtime(CONFIG . $this->config('hierarchyFile')); |
56
|
|
|
$aclModified = filemtime(CONFIG . $this->config('aclFile')); |
57
|
|
|
|
58
|
|
|
$lastModified = ($hierarchyModified > $aclModified) ? $hierarchyModified : $aclModified; |
59
|
|
|
|
60
|
|
|
if (Cache::read('hierarchy_auth_build_time') < $lastModified) { |
61
|
|
|
$this->_hierarchy = $this->_getHierarchy(); |
62
|
|
|
$this->_acl = $this->_getAcl(); |
63
|
|
|
|
64
|
|
|
Cache::write('hierarchy_auth_cache', ['acl' => $this->_acl, 'hierarchy' => $this->_hierarchy]); |
65
|
|
|
Cache::write('hierarchy_auth_build_time', time()); |
66
|
|
|
} else { |
67
|
|
|
$cache = Cache::read('hierarchy_auth_cache'); |
68
|
|
|
$this->_hierarchy = $cache['hierarchy']; |
69
|
|
|
$this->_acl = $cache['acl']; |
70
|
|
|
} |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @param array $user Active user data |
75
|
|
|
* @param Request $request Request instance. |
76
|
|
|
* @return bool |
77
|
|
|
*/ |
78
|
|
|
public function authorize($user, Request $request) |
79
|
|
|
{ |
80
|
|
|
$controller = $request->param('controller'); |
81
|
|
|
$action = $request->param('action'); |
82
|
|
|
|
83
|
|
|
return $this->validate($user, $controller, $action); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Authorize user based on controller, and action |
88
|
|
|
* |
89
|
|
|
* @param array $user Active user data |
90
|
|
|
* @param string $controller Controller to validate |
91
|
|
|
* @param string $action Action to validate |
92
|
|
|
* @return bool |
93
|
|
|
*/ |
94
|
|
|
public function validate($user, $controller, $action) |
95
|
|
|
{ |
96
|
|
|
$userRoles = $this->_getRoles($user); |
97
|
|
|
$authRoles = []; |
98
|
|
|
|
99
|
|
|
if (isset($this->_acl[$this->_allSign])) { |
100
|
|
|
$authRoles = $this->_acl[$this->_allSign]; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
if (isset($this->_acl[$controller][$this->_allSign])) { |
104
|
|
|
$authRoles = array_merge($this->_acl[$controller][$this->_allSign], $authRoles); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
if (isset($this->_acl[$controller][$action])) { |
108
|
|
|
$authRoles = array_merge($this->_acl[$controller][$action], $authRoles); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
foreach ($userRoles as $userRole) { |
112
|
|
|
if (isset($authRoles[$userRole]) && $authRoles[$userRole]) { |
113
|
|
|
return true; |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
return false; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Retrieve and parse hierarchy configuration |
122
|
|
|
* |
123
|
|
|
* @return array |
124
|
|
|
*/ |
125
|
|
View Code Duplication |
protected function _getHierarchy() |
|
|
|
|
126
|
|
|
{ |
127
|
|
|
$file = $this->config('hierarchyFile'); |
128
|
|
|
|
129
|
|
|
$yaml = file_get_contents(CONFIG . $file); |
130
|
|
|
try { |
131
|
|
|
$yaml = Yaml::parse($yaml); |
132
|
|
|
} catch (\Exception $e) { |
133
|
|
|
throw new Exception(sprintf('Malformed hierarchy config file, check YAML syntax: %s', $e->getMessage())); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
if (!isset($yaml['hierarchy'])) { |
137
|
|
|
throw new Exception("The hierarchy configuration must be under key \"hierarchy\". No such key was found."); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$hierarchy = $this->_parseHierarchy($yaml['hierarchy']); |
141
|
|
|
|
142
|
|
|
return $hierarchy; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Parses and flattens hierarchy settings. |
147
|
|
|
* |
148
|
|
|
* @param array $hierarchy Hierarchy settings as an array. |
149
|
|
|
* @param int $recLevel Recursion level. |
150
|
|
|
* @return array |
151
|
|
|
*/ |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @param array $hierarchy An array of the hierarchy data |
155
|
|
|
* @param int $recLevel Recursion level |
156
|
|
|
* @return array |
157
|
|
|
* @throws Exception |
158
|
|
|
*/ |
159
|
|
|
protected function _parseHierarchy(array $hierarchy, $recLevel = 0) |
160
|
|
|
{ |
161
|
|
|
if (!isset($this->_rootHierarchy)) { |
162
|
|
|
$this->_rootHierarchy = $hierarchy; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$offset = 0; |
166
|
|
|
foreach ($hierarchy as $key => $subRole) { |
167
|
|
|
// recursively go through roles |
168
|
|
|
if (is_array($subRole)) { |
169
|
|
|
$subRole = $this->_parseHierarchy($subRole); |
170
|
|
|
$hierarchy[$key] = array_unique($subRole); // remove duplicate roles |
171
|
|
|
} else { |
172
|
|
|
// flatten references |
173
|
|
|
if (substr($subRole, 0, strlen($this->_referenceSign)) == $this->_referenceSign) { |
174
|
|
|
// check if reference is valid |
175
|
|
|
if (!isset($this->_rootHierarchy[substr($subRole, strlen($this->_referenceSign))])) { |
176
|
|
|
throw new Exception(sprintf("A reference in hierarchy doesn't exist: %s", $subRole)); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
// recursion protection |
180
|
|
|
if ($recLevel >= 10) { |
181
|
|
|
throw new Exception(sprintf("Recursion occured. Check reference: %s", $subRole)); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
// replace reference with referenced roles |
185
|
|
|
$subRole = $this->_parseHierarchy( |
186
|
|
|
$this->_rootHierarchy[substr($subRole, strlen($this->_referenceSign))], |
187
|
|
|
++$recLevel |
188
|
|
|
); |
189
|
|
|
array_splice($hierarchy, $offset, 1, $subRole); |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
$offset++; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
return $hierarchy; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Retrieve and parse acl configuration |
200
|
|
|
* |
201
|
|
|
* @return array |
202
|
|
|
*/ |
203
|
|
View Code Duplication |
protected function _getAcl() |
|
|
|
|
204
|
|
|
{ |
205
|
|
|
$file = $this->config('aclFile'); |
206
|
|
|
|
207
|
|
|
$yaml = file_get_contents(CONFIG . $file); |
208
|
|
|
try { |
209
|
|
|
$yaml = Yaml::parse($yaml); |
210
|
|
|
} catch (\Exception $e) { |
211
|
|
|
throw new Exception(sprintf('Malformed acl configuration file. Check syntax: %s', $e->getMessage())); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
if (!isset($yaml['controllers'])) { |
215
|
|
|
throw new Exception('The ACL configuration must be under key \"controllers\". No such key was found.'); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
return $this->_parseAcl($yaml['controllers']); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Parses ACL configuration |
224
|
|
|
* |
225
|
|
|
* @param array $acl Acl configuration in array format |
226
|
|
|
* @return array |
227
|
|
|
*/ |
228
|
|
|
protected function _parseAcl(array $acl) |
229
|
|
|
{ |
230
|
|
|
$parsedAcl = []; |
231
|
|
|
|
232
|
|
|
// check global ACL access rights |
233
|
|
View Code Duplication |
if (isset($acl[$this->_allSign])) { |
|
|
|
|
234
|
|
|
$parsedAcl[$this->_allSign] = $this->_iterateAccessRights($acl[$this->_allSign]); |
235
|
|
|
unset($acl[$this->_allSign]); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
// iterate through controllers and format role authorization |
239
|
|
|
foreach ($acl as $controller => $actions) { |
240
|
|
|
$parsedAcl[$controller] = []; |
241
|
|
|
// check controller-wide access rights |
242
|
|
View Code Duplication |
if (isset($actions[$this->_allSign])) { |
|
|
|
|
243
|
|
|
$parsedAcl[$controller][$this->_allSign] = $this->_iterateAccessRights($actions[$this->_allSign]); |
244
|
|
|
unset($actions[$this->_allSign]); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
// check controller actions' access rights |
248
|
|
|
foreach ($actions as $action => $roles) { |
249
|
|
|
$parsedAcl[$controller][$action] = $this->_iterateAccessRights($roles); |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return $parsedAcl; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Helper function |
258
|
|
|
* Convert YAML config access rights |
259
|
|
|
* |
260
|
|
|
* @param array $yamlRoles Array of roles to iterate through |
261
|
|
|
* @return array |
262
|
|
|
*/ |
263
|
|
|
protected function _iterateAccessRights(array $yamlRoles) |
264
|
|
|
{ |
265
|
|
|
$checkedRoles = []; |
266
|
|
|
|
267
|
|
|
foreach ($yamlRoles as $role) { |
268
|
|
|
if (substr($role, 0, strlen($this->_denySign)) == $this->_denySign) { |
269
|
|
|
$checkedRoles = array_merge( |
270
|
|
|
$checkedRoles, |
271
|
|
|
$this->_flattenSuperRole(substr($role, strlen($this->_denySign)), false) |
272
|
|
|
); |
273
|
|
|
} else { |
274
|
|
|
$checkedRoles = array_merge($checkedRoles, $this->_flattenSuperRole($role, true)); |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
return $checkedRoles; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Helper function |
283
|
|
|
* Check and return roles belonging to a super role with super role's access rights |
284
|
|
|
* |
285
|
|
|
* @param string $role Referenced role to flatten |
286
|
|
|
* @param bool $authorized Super role is authorized or not |
287
|
|
|
* @return array |
288
|
|
|
*/ |
289
|
|
|
protected function _flattenSuperRole($role, $authorized) |
290
|
|
|
{ |
291
|
|
|
if (isset($this->_hierarchy[$role])) { |
292
|
|
|
$roles = []; |
293
|
|
|
foreach ($this->_hierarchy[$role] as $subRole) { |
294
|
|
|
$roles[$subRole] = $authorized; |
295
|
|
|
} |
296
|
|
|
$roles[$role] = $authorized; |
297
|
|
|
} else { |
298
|
|
|
$roles = [$role => $authorized]; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
return $roles; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Retrieve role labels based on configuration. |
306
|
|
|
* |
307
|
|
|
* @param array $user Active user data |
308
|
|
|
* @return bool|array |
309
|
|
|
*/ |
310
|
|
|
protected function _getRoles(array $user) |
311
|
|
|
{ |
312
|
|
|
// check if json column based authentication |
313
|
|
|
if ($this->config('roleColumn')) { |
314
|
|
|
if (!isset($user[$this->config('roleColumn')])) { |
315
|
|
|
throw new Exception( |
316
|
|
|
sprintf('Provided roleColumn "%s" doesn\'t exist for this user.', $this->config('roleColumn')) |
317
|
|
|
); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
$roles = $user[$this->config('roleColumn')]; |
321
|
|
|
// check if column is received already decoded, if not, json decode. |
322
|
|
|
if (!is_array($roles)) { |
323
|
|
|
$roles = json_decode($roles, true); |
324
|
|
|
if (!is_array($roles)) { |
325
|
|
|
throw new Exception( |
326
|
|
|
sprintf('roleColumn "%s" is not in a valid format.', $this->config('roleColumn')) |
327
|
|
|
); |
328
|
|
|
} |
329
|
|
|
} |
330
|
|
|
} else { |
331
|
|
|
$roleKeys = $this->config('roleKeys'); |
332
|
|
|
|
333
|
|
|
// check if role keys are configured correctly |
334
|
|
|
if (!is_array($roleKeys)) { |
335
|
|
|
throw new Exception('roleKeys must be an array.'); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
$roles = []; |
339
|
|
|
// collect all roles based on configuration, from provided associations |
340
|
|
|
foreach ($roleKeys as $role => $settings) { |
341
|
|
|
// check if multiple roles or single role per user |
342
|
|
|
if (!empty($settings['multi'])) { |
343
|
|
|
if (!isset($user[$role])) { |
344
|
|
|
throw new Exception(sprintf('Provided association %s doesn\'t exist.', $role)); |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
foreach ($user[$role] as $userRole) { |
348
|
|
|
if (!isset($userRole[$settings['column']])) { |
349
|
|
|
throw new Exception( |
350
|
|
|
sprintf( |
351
|
|
|
'Provided column %s with association %s doesn\'t exist.', |
352
|
|
|
$settings['column'], |
353
|
|
|
$role |
354
|
|
|
) |
355
|
|
|
); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
$roles[] = $userRole[$settings['column']]; |
359
|
|
|
} |
360
|
|
|
} else { |
361
|
|
|
if (!isset($user[$role]) || !isset($user[$role][$settings['column']])) { |
362
|
|
|
throw new Exception( |
363
|
|
|
sprintf( |
364
|
|
|
'Provided column %s with association %s doesn\'t exist.', |
365
|
|
|
$settings['column'], |
366
|
|
|
$role |
367
|
|
|
) |
368
|
|
|
); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
$roles[] = $user[$role][$settings['column']]; |
372
|
|
|
} |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
return $roles; |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
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.