HierAuthorize   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 361
Duplicated Lines 12.19 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 3
Bugs 1 Features 1
Metric Value
wmc 50
c 3
b 1
f 1
lcom 1
cbo 5
dl 44
loc 361
rs 8.6206

10 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 32 5
A authorize() 0 7 1
C validate() 0 25 7
A _getHierarchy() 19 19 3
C _parseHierarchy() 0 38 7
A _getAcl() 17 17 3
B _parseAcl() 8 27 5
A _iterateAccessRights() 0 17 3
A _flattenSuperRole() 0 14 3
C _getRoles() 0 68 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like HierAuthorize often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HierAuthorize, and based on these observations, apply Extract Interface, too.

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()
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...
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()
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...
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])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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