Completed
Push — master ( c6728e...bf498d )
by Raffael
14:18 queued 04:37
created

Acl::processRuleset()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 0
cts 12
cp 0
rs 8.6666
c 0
b 0
f 0
cc 7
nc 6
nop 2
crap 56
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): bool
71
    {
72
        $this->logger->debug('check acl for ['.$node->getId().'] with privilege ['.$privilege.']', [
73
            'category' => get_class($this),
74
        ]);
75
76
        if (null === $node->getFilesystem()->getUser()) {
77
            $this->logger->debug('system acl call, grant full access', [
78
                'category' => get_class($this),
79
            ]);
80
81
            return true;
82
        }
83
84
        if (!isset(self::PRIVILEGES_WEIGHT[$privilege])) {
85
            throw new Exception('unknown privilege '.$privilege.' requested');
86
        }
87
88
        $priv = $this->getAclPrivilege($node);
89
        $result = false;
90
91
        if (self::PRIVILEGE_WRITEPLUS === $priv && $node->isOwnerRequest()) {
92
            $result = true;
93
        } elseif (self::PRIVILEGE_WRITEPLUS !== $priv && self::PRIVILEGES_WEIGHT[$priv] >= self::PRIVILEGES_WEIGHT[$privilege]) {
94
            $result = true;
95
        }
96
97
        $this->logger->debug('check acl for node ['.$node->getId().'], requested privilege ['.$privilege.']', [
98
            'category' => get_class($this),
99
            'params' => ['privileges' => $priv],
100
        ]);
101
102
        return $result;
103
    }
104
105
    /**
106
     * Get access privilege.
107
     */
108
    public function getAclPrivilege(NodeInterface $node): string
109
    {
110
        $user = $node->getFilesystem()->getUser();
111
112
        if ($node->isShareMember()) {
113
            return $this->processShareMember($node, $user);
114
        }
115
        if ($node->isReference() && $node->isOwnerRequest()) {
116
            return $this->processShareReference($node, $user);
117
        }
118
        if (!$node->isOwnerRequest()) {
119
            $this->logger->warning('user ['.$user.'] not allowed to access non owned node ['.$node->getId().']', [
120
                'category' => get_class($this),
121
            ]);
122
123
            return self::PRIVILEGE_DENY;
124
        }
125
        if ($node->isOwnerRequest()) {
126
            return self::PRIVILEGE_MANAGE;
127
        }
128
129
        return self::PRIVILEGE_DENY;
130
    }
131
132
    /**
133
     * Validate acl.
134
     */
135
    public function validateAcl(Server $server, array $acl): bool
136
    {
137
        if (0 === count($acl)) {
138
            throw new Exception('there must be at least one acl rule');
139
        }
140
141
        foreach ($acl as $rule) {
142
            $this->validateRule($server, $rule);
143
        }
144
145
        return true;
146
    }
147
148
    /**
149
     * Validate rule.
150
     */
151
    public function validateRule(Server $server, array $rule): bool
152
    {
153
        if (!isset($rule['type']) || self::TYPE_USER !== $rule['type'] && self::TYPE_GROUP !== $rule['type']) {
154
            throw new Exception('rule must contain either a type group or user');
155
        }
156
157
        if (!isset($rule['privilege']) || !isset(self::PRIVILEGES_WEIGHT[$rule['privilege']])) {
158
            throw new Exception('rule must contain a valid privilege');
159
        }
160
161
        if (!isset($rule['id'])) {
162
            throw new Exception('rule must contain a resource id');
163
        }
164
165
        $this->verifyRole($server, $rule['type'], new ObjectId($rule['id']));
166
167
        return true;
168
    }
169
170
    /**
171
     * Get acl with resolved roles.
172
     */
173
    public function resolveAclTable(Server $server, array $acl): array
174
    {
175
        foreach ($acl as $key => &$rule) {
176
            try {
177
                if ('user' === $rule['type']) {
178
                    $rule['role'] = $server->getUserById(new ObjectId($rule['id']));
179
                } elseif ('group' === $rule['type']) {
180
                    $rule['role'] = $server->getGroupById(new ObjectId($rule['id']));
181
                } else {
182
                    throw new Exception('invalid acl rule resource type');
183
                }
184
            } catch (\Exception $e) {
185
                unset($acl[$key]);
186
                $this->logger->error('acl role ['.$rule['id'].'] could not be resolved, remove from list', [
187
                    'category' => get_class($this),
188
                    'exception' => $e,
189
                ]);
190
            }
191
        }
192
193
        return $acl;
194
    }
195
196
    /**
197
     * Process share member.
198
     */
199
    protected function processShareMember(NodeInterface $node, User $user): string
200
    {
201
        try {
202
            $share = $node->getFilesystem()->findRawNode($node->getShareId());
203
        } catch (\Exception $e) {
204
            $this->logger->error('could not found share node ['.$node->getShareId().'] for share child node ['.$node->getId().'], dead reference?', [
205
                'category' => get_class($this),
206
                'exception' => $e,
207
            ]);
208
209
            return self::PRIVILEGE_DENY;
210
        }
211
212
        if ((string) $share['owner'] === (string) $user->getId()) {
213
            return self::PRIVILEGE_MANAGE;
214
        }
215
216
        $acl = $node->getAttributes()['acl'];
217
        $share = $this->processRuleset($user, $share['acl']);
218
219
        if (count($acl) > 0) {
220
            $own = $this->processRuleset($user, $node->getAttributes()['acl']);
221
222
            if ($share !== self::PRIVILEGE_DENY) {
223
                return $own;
224
            }
225
        }
226
227
        return $share;
228
    }
229
230
    /**
231
     * Process share reference.
232
     */
233
    protected function processShareReference(NodeInterface $node, User $user): string
234
    {
235
        try {
236
            $share = $node->getFilesystem()->findRawNode($node->getShareId());
237
        } catch (\Exception $e) {
238
            $this->logger->error('could not find share node ['.$node->getShareId().'] for reference ['.$node->getId().'], dead reference?', [
239
                 'category' => get_class($this),
240
                 'exception' => $e,
241
            ]);
242
243
            return self::PRIVILEGE_DENY;
244
        }
245
246
        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...
247
            $this->logger->error('share node ['.$share['_id'].'] has been deleted, dead reference?', [
248
                 'category' => get_class($this),
249
            ]);
250
251
            return self::PRIVILEGE_DENY;
252
        }
253
254
        return $this->processRuleset($user, $share['acl']);
255
    }
256
257
    /**
258
     * Process ruleset.
259
     */
260
    protected function processRuleset(User $user, array $acl): string
261
    {
262
        $result = self::PRIVILEGE_DENY;
263
        $groups = $user->getGroups();
264
265
        foreach ($acl as $rule) {
266
            if (self::TYPE_USER === $rule['type'] && $rule['id'] === (string) $user->getId()) {
267
                $priv = $rule['privilege'];
268
            } elseif (self::TYPE_GROUP === $rule['type'] && in_array($rule['id'], $groups)) {
269
                $priv = $rule['privilege'];
270
            } else {
271
                continue;
272
            }
273
            if (self::PRIVILEGES_WEIGHT[$priv] > self::PRIVILEGES_WEIGHT[$result]) {
274
                $result = $priv;
275
            }
276
        }
277
278
        return $result;
279
    }
280
281
    /**
282
     * Verify if role exists.
283
     *
284
     * @param string $id
285
     */
286
    protected function verifyRole(Server $server, string $type, ObjectId $id): bool
287
    {
288
        if (self::TYPE_USER === $type && $server->getUserById($id)) {
289
            return true;
290
        }
291
        if (self::TYPE_GROUP === $type && $server->getGroupById($id)) {
292
            return true;
293
        }
294
295
        throw new Exception('invalid acl rule resource type');
296
    }
297
}
298