Completed
Pull Request — master (#149)
by Raffael
05:07
created

Acl   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 284
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 5
dl 0
loc 284
ccs 0
cts 113
cp 0
rs 6.96
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B isAllowed() 0 42 9
B getAclPrivilege() 0 23 7
A validateAcl() 0 12 3
B validateRule() 0 18 7
A resolveAclTable() 0 22 5
A processShareMember() 0 30 5
A processShareReference() 0 23 4
B processRuleset() 0 20 7
A verifyRole() 0 11 5

How to fix   Complexity   

Complex Class

Complex classes like Acl 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 Acl, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem;
13
14
use Balloon\Filesystem\Acl\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\Filesystem\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
15
use Balloon\Filesystem\Node\NodeInterface;
16
use Balloon\Server;
17
use Balloon\Server\User;
18
use MongoDB\BSON\ObjectId;
19
use MongoDB\BSON\UTCDateTime;
20
use Psr\Log\LoggerInterface;
21
22
class Acl
23
{
24
    /**
25
     * Privileges.
26
     */
27
    public const PRIVILEGE_DENY = 'd';
28
    public const PRIVILEGE_READ = 'r';
29
    public const PRIVILEGE_WRITE = 'w';
30
    public const PRIVILEGE_WRITEPLUS = 'w+';
31
    public const PRIVILEGE_READWRITE = 'rw';
32
    public const PRIVILEGE_MANAGE = 'm';
33
34
    /**
35
     * ACL privileges weight table.
36
     */
37
    public const PRIVILEGES_WEIGHT = [
38
        self::PRIVILEGE_DENY => 0,
39
        self::PRIVILEGE_READ => 1,
40
        self::PRIVILEGE_WRITE => 2,
41
        self::PRIVILEGE_WRITEPLUS => 3,
42
        self::PRIVILEGE_READWRITE => 4,
43
        self::PRIVILEGE_MANAGE => 5,
44
    ];
45
46
    /**
47
     * Types.
48
     */
49
    public const TYPE_USER = 'user';
50
    public const TYPE_GROUP = 'group';
51
52
    /**
53
     * Logger.
54
     *
55
     * @var LoggerInterface
56
     */
57
    protected $logger;
58
59
    /**
60
     * Constructor.
61
     */
62
    public function __construct(LoggerInterface $logger)
63
    {
64
        $this->logger = $logger;
65
    }
66
67
    /**
68
     * Check acl.
69
     */
70
    public function isAllowed(NodeInterface $node, string $privilege = self::PRIVILEGE_READ, ?User $user = null): bool
71
    {
72
        $this->logger->debug('check acl for ['.$node->getId().'] with privilege ['.$privilege.']', [
73
            'category' => get_class($this),
74
        ]);
75
76
        $custom = $user;
0 ignored issues
show
Unused Code introduced by
$custom is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
77
        $user = $user === null ? $node->getFilesystem()->getUser() : $user;
78
79
        if (null === $user) {
80
            $this->logger->debug('system acl call, grant full access', [
81
                'category' => get_class($this),
82
            ]);
83
84
            return true;
85
        }
86
87
        if (!isset(self::PRIVILEGES_WEIGHT[$privilege])) {
88
            throw new Exception('unknown privilege '.$privilege.' requested');
89
        }
90
91
        $priv = $this->getAclPrivilege($node, $user);
92
        $result = false;
93
94
        if (self::PRIVILEGE_WRITEPLUS === $priv && $node->getOwner() == $user->getId()) {
95
            $result = true;
96
        } elseif (self::PRIVILEGE_WRITEPLUS !== $priv && self::PRIVILEGES_WEIGHT[$priv] >= self::PRIVILEGES_WEIGHT[$privilege]) {
97
            $result = true;
98
        }
99
100
        if ($result === true) {
101
            $this->logger->debug('grant access to node ['.$node->getId().'] for user ['.$user->getId().'] by privilege ['.$priv.']', [
102
                'category' => get_class($this),
103
            ]);
104
        } else {
105
            $this->logger->debug('deny access to node ['.$node->getId().'] for user ['.$user->getId().'] by privilege ['.$priv.']', [
106
                'category' => get_class($this),
107
            ]);
108
        }
109
110
        return $result;
111
    }
112
113
    /**
114
     * Get access privilege.
115
     */
116
    public function getAclPrivilege(NodeInterface $node, ?User $user = null): string
117
    {
118
        $user = $user === null ? $node->getFilesystem()->getUser() : $user;
119
120
        if ($node->isShareMember()) {
121
            return $this->processShareMember($node, $user);
122
        }
123
        if ($node->isReference() && $node->isOwnerRequest()) {
124
            return $this->processShareReference($node, $user);
125
        }
126
        if (!$node->isOwnerRequest()) {
127
            $this->logger->warning('user ['.$user.'] not allowed to access non owned node ['.$node->getId().']', [
128
                'category' => get_class($this),
129
            ]);
130
131
            return self::PRIVILEGE_DENY;
132
        }
133
        if ($node->isOwnerRequest()) {
134
            return self::PRIVILEGE_MANAGE;
135
        }
136
137
        return self::PRIVILEGE_DENY;
138
    }
139
140
    /**
141
     * Validate acl.
142
     */
143
    public function validateAcl(Server $server, array $acl): bool
144
    {
145
        if (0 === count($acl)) {
146
            throw new Exception('there must be at least one acl rule');
147
        }
148
149
        foreach ($acl as $rule) {
150
            $this->validateRule($server, $rule);
151
        }
152
153
        return true;
154
    }
155
156
    /**
157
     * Validate rule.
158
     */
159
    public function validateRule(Server $server, array $rule): bool
160
    {
161
        if (!isset($rule['type']) || self::TYPE_USER !== $rule['type'] && self::TYPE_GROUP !== $rule['type']) {
162
            throw new Exception('rule must contain either a type group or user');
163
        }
164
165
        if (!isset($rule['privilege']) || !isset(self::PRIVILEGES_WEIGHT[$rule['privilege']])) {
166
            throw new Exception('rule must contain a valid privilege');
167
        }
168
169
        if (!isset($rule['id'])) {
170
            throw new Exception('rule must contain a resource id');
171
        }
172
173
        $this->verifyRole($server, $rule['type'], new ObjectId($rule['id']));
174
175
        return true;
176
    }
177
178
    /**
179
     * Get acl with resolved roles.
180
     */
181
    public function resolveAclTable(Server $server, array $acl): array
182
    {
183
        foreach ($acl as $key => &$rule) {
184
            try {
185
                if ('user' === $rule['type']) {
186
                    $rule['role'] = $server->getUserById(new ObjectId($rule['id']));
187
                } elseif ('group' === $rule['type']) {
188
                    $rule['role'] = $server->getGroupById(new ObjectId($rule['id']));
189
                } else {
190
                    throw new Exception('invalid acl rule resource type');
191
                }
192
            } catch (\Exception $e) {
193
                unset($acl[$key]);
194
                $this->logger->error('acl role ['.$rule['id'].'] could not be resolved, remove from list', [
195
                    'category' => get_class($this),
196
                    'exception' => $e,
197
                ]);
198
            }
199
        }
200
201
        return $acl;
202
    }
203
204
    /**
205
     * Process share member.
206
     */
207
    protected function processShareMember(NodeInterface $node, User $user): string
208
    {
209
        try {
210
            $share = $node->getFilesystem()->findRawNode($node->getShareId());
211
        } catch (\Exception $e) {
212
            $this->logger->error('could not found share node ['.$node->getShareId().'] for share child node ['.$node->getId().'], dead reference?', [
213
                'category' => get_class($this),
214
                'exception' => $e,
215
            ]);
216
217
            return self::PRIVILEGE_DENY;
218
        }
219
220
        if ((string) $share['owner'] === (string) $user->getId()) {
221
            return self::PRIVILEGE_MANAGE;
222
        }
223
224
        $acl = $node->getAttributes()['acl'];
225
        $share = $this->processRuleset($user, $share['acl']);
226
227
        if (count($acl) > 0) {
228
            $own = $this->processRuleset($user, $node->getAttributes()['acl']);
229
230
            if ($share !== self::PRIVILEGE_DENY) {
231
                return $own;
232
            }
233
        }
234
235
        return $share;
236
    }
237
238
    /**
239
     * Process share reference.
240
     */
241
    protected function processShareReference(NodeInterface $node, User $user): string
242
    {
243
        try {
244
            $share = $node->getFilesystem()->findRawNode($node->getShareId());
245
        } catch (\Exception $e) {
246
            $this->logger->error('could not find share node ['.$node->getShareId().'] for reference ['.$node->getId().'], dead reference?', [
247
                 'category' => get_class($this),
248
                 'exception' => $e,
249
            ]);
250
251
            return self::PRIVILEGE_DENY;
252
        }
253
254
        if ($share['deleted'] instanceof UTCDateTime || true !== $share['shared']) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
255
            $this->logger->error('share node ['.$share['_id'].'] has been deleted, dead reference?', [
256
                 'category' => get_class($this),
257
            ]);
258
259
            return self::PRIVILEGE_DENY;
260
        }
261
262
        return $this->processRuleset($user, $share['acl']);
263
    }
264
265
    /**
266
     * Process ruleset.
267
     */
268
    protected function processRuleset(User $user, array $acl): string
269
    {
270
        $result = self::PRIVILEGE_DENY;
271
        $groups = $user->getGroups();
272
273
        foreach ($acl as $rule) {
274
            if (self::TYPE_USER === $rule['type'] && $rule['id'] === (string) $user->getId()) {
275
                $priv = $rule['privilege'];
276
            } elseif (self::TYPE_GROUP === $rule['type'] && in_array($rule['id'], $groups)) {
277
                $priv = $rule['privilege'];
278
            } else {
279
                continue;
280
            }
281
            if (self::PRIVILEGES_WEIGHT[$priv] > self::PRIVILEGES_WEIGHT[$result]) {
282
                $result = $priv;
283
            }
284
        }
285
286
        return $result;
287
    }
288
289
    /**
290
     * Verify if role exists.
291
     *
292
     * @param string $id
293
     */
294
    protected function verifyRole(Server $server, string $type, ObjectId $id): bool
295
    {
296
        if (self::TYPE_USER === $type && $server->getUserById($id)) {
297
            return true;
298
        }
299
        if (self::TYPE_GROUP === $type && $server->getGroupById($id)) {
300
            return true;
301
        }
302
303
        throw new Exception('invalid acl rule resource type');
304
    }
305
}
306