Passed
Pull Request — master (#58)
by Alberto
02:38
created

RequestRolesPolicy::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2022 Atlas Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
namespace BEdita\WebTools\Policy;
16
17
use Authorization\IdentityInterface;
18
use Authorization\Policy\Exception\MissingMethodException;
19
use Authorization\Policy\RequestPolicyInterface;
20
use Authorization\Policy\Result;
21
use Cake\Core\App;
22
use Cake\Core\InstanceConfigTrait;
23
use Cake\Http\ServerRequest;
24
use Cake\Utility\Hash;
25
26
/**
27
 * RequestRolesPolicy class.
28
 * Given a request and an identity applies the corresponding role based rule for controller and action.
29
 */
30
class RequestRolesPolicy implements RequestPolicyInterface
31
{
32
    use InstanceConfigTrait;
33
34
    /**
35
     * Default configuration.
36
     *
37
     * - `rules` an array of rules to apply.
38
     *    The keys of the array are the Controller names. Values can be:
39
     *    - an array of roles (or a role name) to check against
40
     *    - a class name or instance that implements `\Authorization\Policy\RequestPolicyInterface`
41
     *    - a callable item
42
     *    - an array with controller actions as keys and values one of the above values
43
     *
44
     *    Examples of rules are:
45
     *    ```
46
     *    [
47
     *        // ControllerName => rules
48
     *        'SingleRole' => 'admin', // check identity against `admin` role
49
     *        'ArrayRole' => ['editor', 'manager'], // check identity against one of these roles
50
     *        'FullQualifiedClassName' => 'PolicyClass::class', // it needs to implements RequestPolicyInterface
51
     *        'Custom' => 'CustomAppPolicy',  // search for `\App\Policy\CustomAppPolicy`. It needs to implements RequestPolicyInterface
52
     *        'Gustavo' => function ($identity, $request) { // it applies the policy callback to all actions of GustavoController
53
     *            // here the policy
54
     *        },
55
     *        'Supporto' => new \Super\Custom\Policy(), // the class must be an instance of RequestPolicyInterface
56
     *                                                  // or must implement __invoke(?IdentityInterface $identity, ServerRequest $request) magic method
57
     *        'Mixed' => [
58
     *            'index' => 'PolicyClass::class', // it applies rule only to MixedController::index() action
59
     *            'view' => ['editor', 'manager'], // it applies roles rule only to MixedController::view() action,
60
     *            'here' => function ($identity, $request) {
61
     *                // here the policy
62
     *            },
63
     *            '*' => ['admin'], // fallback rule for all other controller actions
64
     *         ],
65
     *     ]
66
     *     ```
67
     * - `ruleRequired` set true to forbidden access when missing rule for controller.
68
     *
69
     * @var array
70
     */
71
    protected $_defaultConfig = [
72
        'rules' => [],
73
        'ruleRequired' => false,
74
    ];
75
76
    /**
77
     * Constructor.
78
     * Setup policy configuration.
79
     *
80
     * @param array $config The configuration
81
     */
82
    public function __construct(array $config = [])
83
    {
84
        $this->setConfig($config);
85
    }
86
87
    /**
88
     * Method to check if the request can be accessed.
89
     *
90
     * @param \Authorization\IdentityInterface|null $identity Identity
91
     * @param \Cake\Http\ServerRequest $request Server Request
92
     * @return bool|\Authorization\Policy\ResultInterface
93
     */
94
    public function canAccess(?IdentityInterface $identity, ServerRequest $request)
95
    {
96
        $rule = $this->getRule($request);
97
        if (empty($rule)) {
98
            return $this->missingRuleResult();
99
        }
100
101
        if ($identity === null) {
102
            return new Result(false, 'missing identity');
103
        }
104
105
        if ($rule instanceof RequestPolicyInterface) {
106
            return $rule->canAccess($identity, $request);
107
        }
108
109
        if (is_callable($rule)) {
110
            return $rule($identity, $request);
111
        }
112
113
        if (is_array($rule)) {
114
            return $this->applyRolesPolicy($identity, $rule);
115
        }
116
117
        if (!is_string($rule)) {
118
            throw new \LogicException(sprintf(
119
                'Invalid rule for %s::%s() in RequestRolesPolicy',
120
                $request->getParam('controller'),
121
                $request->getParam('action')
122
            ));
123
        }
124
125
        $policyRuleClass = App::className($rule, 'Policy');
126
        if ($policyRuleClass === null) {
127
            return $this->applyRolesPolicy($identity, [$rule]);
128
        }
129
130
        $policyRule = new $policyRuleClass();
131
        if (!$policyRule instanceof RequestPolicyInterface) {
132
            throw new MissingMethodException(['canAccess', 'access', get_class($policyRule)]);
133
        }
134
135
        return $policyRule->canAccess($identity, $request);
136
    }
137
138
    /**
139
     * Build missing rule result.
140
     *
141
     * @return \Authorization\Policy\Result
142
     */
143
    protected function missingRuleResult(): Result
144
    {
145
        if ($this->getConfig('ruleRequired') === true) {
146
            return new Result(false, 'required rule is missing');
147
        }
148
149
        return new Result(true);
150
    }
151
152
    /**
153
     * Apply a simple role policy.
154
     * If `$identity` belongs to one of `$roles` than it can access.
155
     *
156
     * @param \Authorization\IdentityInterface $identity Identity
157
     * @param array $roles The roles to check against
158
     * @return \Authorization\Policy\Result
159
     */
160
    protected function applyRolesPolicy(IdentityInterface $identity, array $roles): Result
161
    {
162
        $identityRoles = (array)Hash::get((array)$identity->getOriginalData(), 'roles');
163
164
        if (!empty(array_intersect($identityRoles, $roles))) {
165
            return new Result(true);
166
        }
167
168
        return new Result(false, 'request forbidden for identity\'s roles');
169
    }
170
171
    /**
172
     * Get the rule to apply.
173
     *
174
     * @param \Cake\Http\ServerRequest $request Server Request
175
     * @return mixed
176
     */
177
    protected function getRule(ServerRequest $request)
178
    {
179
        $controller = $request->getParam('controller');
180
        $rule = $this->getConfig(sprintf('rules.%s', $controller));
181
        if ($rule === null) {
182
            return false; // no rule was set
183
        }
184
185
        if (is_string($rule) || is_callable($rule) || $rule instanceof RequestPolicyInterface) {
186
            return $rule;
187
        }
188
189
        if (!is_array($rule)) {
190
            throw new \LogicException(sprintf('Invalid Rule for %s in RequestRolesPolicy', $controller));
191
        }
192
193
        $action = $request->getParam('action');
194
        $defaultRule = Hash::get($rule, '*');
195
196
        return Hash::get($rule, $action, $defaultRule);
197
    }
198
}
199