Issues (219)

Branch: 4-cactus

plugins/BEdita/API/src/Auth/EndpointAuthorize.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2019 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
14
namespace BEdita\API\Auth;
15
16
use BEdita\Core\Model\Entity\EndpointPermission;
17
use BEdita\Core\Model\Table\RolesTable;
18
use Cake\Auth\BaseAuthorize;
19
use Cake\Datasource\ModelAwareTrait;
20
use Cake\Http\ServerRequest;
21
use Cake\Utility\Hash;
22
23
/**
24
 * Provide authorization on a per-endpoint basis.
25
 *
26
 * @since 4.0.0
27
 * @property \BEdita\Core\Model\Table\EndpointsTable $Endpoints
28
 * @property \BEdita\Core\Model\Table\EndpointPermissionsTable $EndpointPermissions
29
 */
30
class EndpointAuthorize extends BaseAuthorize
31
{
32
    use ModelAwareTrait;
33
34
    /**
35
     * {@inheritDoc}
36
     *
37
     * If 'blockAnonymousUsers' is true no access will be granted
38
     * to unauthenticated users otherwise authorization check is performed
39
     * If 'defaultAuthorized' is set current request is authorized
40
     * unless a specific permission is set.
41
     */
42
    protected $_defaultConfig = [
43
        'blockAnonymousUsers' => true,
44
        'defaultAuthorized' => false,
45
    ];
46
47
    /**
48
     * Cache result of `authorized()` method call.
49
     *
50
     * This is required for controller to know whether authorization was granted on all contents,
51
     * or only on those that belong to the current user. Whatever that means, it is controller's
52
     * responsibility to interpret, as it may vary. Some controller may also decide to ignore this
53
     * fine-grained authorization level.
54
     *
55
     * @var bool|string
56
     */
57
    protected $authorized;
58
59
    /**
60
     * @inheritDoc
61
     */
62
    public function authorize($user, ServerRequest $request): bool
63
    {
64
        // if 'blockAnonymousUsers' configuration is true and user unlogged authorization is denied
65
        if (
66
            !$this->getConfig('defaultAuthorized') &&
67
            $this->isAnonymous($user) &&
68
            $this->getConfig('blockAnonymousUsers')
69
        ) {
70
            $this->unauthenticated();
71
        }
72
        $this->loadModel('Endpoints');
73
        $this->loadModel('EndpointPermissions');
74
75
        // For anonymous users performing write operations, use strict mode.
76
        $readRequest = $request->is(['get', 'head']);
77
        $strict = ($this->isAnonymous($user) && !$readRequest);
78
79
        $endpointId = $this->Endpoints->fetchId($request->getPath());
80
        $permsCount = $this->EndpointPermissions->fetchCount($endpointId);
81
82
        // If request si authorized and no permission is set on it then it is authorized for anyone
83
        if ($this->getConfig('defaultAuthorized') && ($endpointId === null || $permsCount === 0)) {
84
            return $this->authorized = true;
85
        }
86
87
        $permissions = $this->EndpointPermissions->fetchPermissions($endpointId, $user, $strict);
88
        $this->authorized = $this->checkPermissions($permissions, $readRequest);
89
90
        if (empty($permissions) && ($endpointId === null || $permsCount === 0)) {
91
            // If no permissions are set for an endpoint, assume the least restrictive permissions possible.
92
            // This does not apply to write operations for anonymous users: those **MUST** be explicitly allowed.
93
            $this->authorized = !$strict;
94
        }
95
96
        // if 'administratorOnly' configuration is true logged user must have administrator role
97
        if ($this->authorized && $this->getConfig('administratorOnly')) {
98
            $this->authorized = in_array(RolesTable::ADMIN_ROLE, Hash::extract($user, 'roles.{n}.id'));
0 ignored issues
show
It seems like Cake\Utility\Hash::extract($user, 'roles.{n}.id') can also be of type ArrayAccess; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

98
            $this->authorized = in_array(RolesTable::ADMIN_ROLE, /** @scrutinizer ignore-type */ Hash::extract($user, 'roles.{n}.id'));
Loading history...
99
        }
100
101
        if ($this->isAnonymous($user) && $this->authorized !== true) {
102
            // Anonymous user should not get a 403. Thus, we invoke authentication provider's
103
            // `unauthenticated()` method. Furthermore, for anonymous users, `mine` doesn't make any sense,
104
            // so we treat that as a non-authorized request.
105
            $this->unauthenticated();
106
        }
107
108
        // Authorization is granted for both `true` and `'mine'` values.
109
        return !empty($this->authorized);
110
    }
111
112
    /**
113
     * Perform user unauthentication to return 401 Unauthorized
114
     * instead of 403 Forbidden
115
     *
116
     * @return void
117
     */
118
    protected function unauthenticated()
119
    {
120
        $controller = $this->_registry->getController();
121
        $controller
122
            ->Auth->getAuthenticate('BEdita/API.Jwt')
123
            ->unauthenticated($controller->getRequest(), $controller->getResponse());
124
    }
125
126
    /**
127
     * Check if user is anonymous.
128
     *
129
     * @param array|\ArrayAccess $user User data.
130
     * @return bool
131
     */
132
    public function isAnonymous($user)
133
    {
134
        return !empty($user['_anonymous']);
135
    }
136
137
    /**
138
     * Checks if request can be authorized basing on a set of applicable permissions.
139
     *
140
     * @param \BEdita\Core\Model\Entity\EndpointPermission[] $permissions Set of applicable permissions.
141
     * @param bool $readRequest Read request flag.
142
     * @return bool|string
143
     */
144
    protected function checkPermissions(array $permissions, bool $readRequest)
145
    {
146
        $shift = EndpointPermission::PERM_READ;
147
        if (!$readRequest) {
148
            $shift = EndpointPermission::PERM_WRITE;
149
        }
150
151
        $result = EndpointPermission::PERM_NO;
152
        foreach ($permissions as $permission) {
153
            $permission = $permission->permission >> $shift & EndpointPermission::PERM_YES;
154
            $result = $result | $permission;
155
156
            if ($permission === EndpointPermission::PERM_BLOCK) {
157
                $result = EndpointPermission::PERM_NO;
158
159
                break;
160
            }
161
        }
162
163
        return EndpointPermission::decode($result);
164
    }
165
}
166