Completed
Push — master ( 37faaa...541bbf )
by Raffael
10:18 queued 06:30
created

Acl   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 52
cbo 6
dl 0
loc 277
ccs 0
cts 111
cp 0
rs 7.44
c 0
b 0
f 0
lcom 1

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B isAllowed() 0 44 9
B getAclPrivilege() 0 27 8
A validateAcl() 0 12 3
B validateRule() 0 18 7
A resolveAclTable() 0 22 5
A processShareMember() 0 19 3
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-2019 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
105
            return $result;
106
        }
107
108
        $this->logger->debug('deny access to node ['.$node->getId().'] for user ['.$user->getId().'] by privilege ['.$priv.']', [
109
            'category' => get_class($this),
110
        ]);
111
112
        return $result;
113
    }
114
115
    /**
116
     * Get access privilege.
117
     */
118
    public function getAclPrivilege(NodeInterface $node, ?User $user = null): string
119
    {
120
        $user = $user === null ? $node->getFilesystem()->getUser() : $user;
121
122
        if ($user === null) {
123
            return self::PRIVILEGE_MANAGE;
124
        }
125
126
        if ($node->isShareMember()) {
127
            return $this->processShareMember($node, $user);
128
        }
129
        if ($node->isReference() && $node->isOwnerRequest()) {
130
            return $this->processShareReference($node, $user);
131
        }
132
        if (!$node->isOwnerRequest()) {
133
            $this->logger->warning('user ['.$user.'] not allowed to access non owned node ['.$node->getId().']', [
134
                'category' => get_class($this),
135
            ]);
136
137
            return self::PRIVILEGE_DENY;
138
        }
139
        if ($node->isOwnerRequest()) {
140
            return self::PRIVILEGE_MANAGE;
141
        }
142
143
        return self::PRIVILEGE_DENY;
144
    }
145
146
    /**
147
     * Validate acl.
148
     */
149
    public function validateAcl(Server $server, array $acl): bool
150
    {
151
        if (0 === count($acl)) {
152
            throw new Exception('there must be at least one acl rule');
153
        }
154
155
        foreach ($acl as $rule) {
156
            $this->validateRule($server, $rule);
157
        }
158
159
        return true;
160
    }
161
162
    /**
163
     * Validate rule.
164
     */
165
    public function validateRule(Server $server, array $rule): bool
166
    {
167
        if (!isset($rule['type']) || self::TYPE_USER !== $rule['type'] && self::TYPE_GROUP !== $rule['type']) {
168
            throw new Exception('rule must contain either a type group or user');
169
        }
170
171
        if (!isset($rule['privilege']) || !isset(self::PRIVILEGES_WEIGHT[$rule['privilege']])) {
172
            throw new Exception('rule must contain a valid privilege');
173
        }
174
175
        if (!isset($rule['id'])) {
176
            throw new Exception('rule must contain a resource id');
177
        }
178
179
        $this->verifyRole($server, $rule['type'], new ObjectId($rule['id']));
180
181
        return true;
182
    }
183
184
    /**
185
     * Get acl with resolved roles.
186
     */
187
    public function resolveAclTable(Server $server, array $acl): array
188
    {
189
        foreach ($acl as $key => &$rule) {
190
            try {
191
                if ('user' === $rule['type']) {
192
                    $rule['role'] = $server->getUserById(new ObjectId($rule['id']));
193
                } elseif ('group' === $rule['type']) {
194
                    $rule['role'] = $server->getGroupById(new ObjectId($rule['id']));
195
                } else {
196
                    throw new Exception('invalid acl rule resource type');
197
                }
198
            } catch (\Exception $e) {
199
                unset($acl[$key]);
200
                $this->logger->error('acl role ['.$rule['id'].'] could not be resolved, remove from list', [
201
                    'category' => get_class($this),
202
                    'exception' => $e,
203
                ]);
204
            }
205
        }
206
207
        return $acl;
208
    }
209
210
    /**
211
     * Process share member.
212
     */
213
    protected function processShareMember(NodeInterface $node, User $user): string
214
    {
215
        try {
216
            $share = $node->getFilesystem()->findRawNode($node->getShareId());
217
        } catch (\Exception $e) {
218
            $this->logger->error('could not found share node ['.$node->getShareId().'] for share child node ['.$node->getId().'], dead reference?', [
219
                'category' => get_class($this),
220
                'exception' => $e,
221
            ]);
222
223
            return self::PRIVILEGE_DENY;
224
        }
225
226
        if ((string) $share['owner'] === (string) $user->getId()) {
227
            return self::PRIVILEGE_MANAGE;
228
        }
229
230
        return $this->processRuleset($user, $share['acl']);
231
    }
232
233
    /**
234
     * Process share reference.
235
     */
236
    protected function processShareReference(NodeInterface $node, User $user): string
237
    {
238
        try {
239
            $share = $node->getFilesystem()->findRawNode($node->getReference());
240
        } catch (\Exception $e) {
241
            $this->logger->error('could not find share node ['.$node->getReference().'] for reference ['.$node->getId().'], dead reference?', [
242
                 'category' => get_class($this),
243
                 'exception' => $e,
244
            ]);
245
246
            return self::PRIVILEGE_DENY;
247
        }
248
249
        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...
250
            $this->logger->error('share node ['.$share['_id'].'] has been deleted, dead reference?', [
251
                 'category' => get_class($this),
252
            ]);
253
254
            return self::PRIVILEGE_DENY;
255
        }
256
257
        return $this->processRuleset($user, $share['acl']);
258
    }
259
260
    /**
261
     * Process ruleset.
262
     */
263
    protected function processRuleset(User $user, array $acl): string
264
    {
265
        $result = self::PRIVILEGE_DENY;
266
        $groups = $user->getGroups();
267
268
        foreach ($acl as $rule) {
269
            if (self::TYPE_USER === $rule['type'] && $rule['id'] === (string) $user->getId()) {
270
                $priv = $rule['privilege'];
271
            } elseif (self::TYPE_GROUP === $rule['type'] && in_array($rule['id'], $groups)) {
272
                $priv = $rule['privilege'];
273
            } else {
274
                continue;
275
            }
276
            if (self::PRIVILEGES_WEIGHT[$priv] > self::PRIVILEGES_WEIGHT[$result]) {
277
                $result = $priv;
278
            }
279
        }
280
281
        return $result;
282
    }
283
284
    /**
285
     * Verify if role exists.
286
     */
287
    protected function verifyRole(Server $server, string $type, ObjectId $id): bool
288
    {
289
        if (self::TYPE_USER === $type && $server->getUserById($id)) {
290
            return true;
291
        }
292
        if (self::TYPE_GROUP === $type && $server->getGroupById($id)) {
293
            return true;
294
        }
295
296
        throw new Exception('invalid acl rule resource type');
297
    }
298
}
299