RequestPolicy   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 167
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 47
c 1
b 0
f 0
dl 0
loc 167
rs 10
wmc 20

5 Methods

Rating   Name   Duplication   Size   Complexity  
A missingRuleResult() 0 7 2
A applyRolesPolicy() 0 9 2
A __construct() 0 3 1
B canAccess() 0 42 9
A getRule() 0 20 6
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 Authorization\Policy\ResultInterface;
22
use Cake\Core\App;
23
use Cake\Core\InstanceConfigTrait;
24
use Cake\Http\ServerRequest;
25
use Cake\Utility\Hash;
26
use LogicException;
27
28
/**
29
 * RequestPolicy class.
30
 * Given a request and an identity applies the corresponding rule for controller and action.
31
 */
32
class RequestPolicy implements RequestPolicyInterface
33
{
34
    use InstanceConfigTrait;
35
36
    /**
37
     * Default configuration.
38
     *
39
     * - `rules` an array of rules to apply.
40
     *    The keys of the array are the Controller names. Values can be:
41
     *    - an array of roles (or a role name) to check against
42
     *    - a class name or instance that implements `\Authorization\Policy\RequestPolicyInterface`
43
     *    - a callable item
44
     *    - an array with controller actions as keys and values one of the above values
45
     *
46
     *    Examples of rules are:
47
     *    ```
48
     *    [
49
     *        // ControllerName => rules
50
     *        'SingleRole' => 'admin', // check identity against `admin` role
51
     *        'ArrayRole' => ['editor', 'manager'], // check identity against one of these roles
52
     *        'FullQualifiedClassName' => 'PolicyClass::class', // it needs to implements RequestPolicyInterface
53
     *        'Custom' => 'CustomAppPolicy',  // search for `\App\Policy\CustomAppPolicy`. It needs to implements RequestPolicyInterface
54
     *        'Gustavo' => function ($identity, $request) { // it applies the policy callback to all actions of GustavoController
55
     *            // here the policy
56
     *        },
57
     *        'Supporto' => new \Super\Custom\Policy(), // the class must be an instance of RequestPolicyInterface
58
     *                                                  // or must implement __invoke(?IdentityInterface $identity, ServerRequest $request) magic method
59
     *        'Mixed' => [
60
     *            'index' => 'PolicyClass::class', // it applies rule only to MixedController::index() action
61
     *            'view' => ['editor', 'manager'], // it applies roles rule only to MixedController::view() action,
62
     *            'here' => function ($identity, $request) {
63
     *                // here the policy
64
     *            },
65
     *            '*' => ['admin'], // fallback rule for all other controller actions
66
     *         ],
67
     *     ]
68
     *     ```
69
     * - `ruleRequired` set true to forbidden access when missing rule for controller.
70
     *
71
     * @var array
72
     */
73
    protected array $_defaultConfig = [
74
        'rules' => [],
75
        'ruleRequired' => false,
76
    ];
77
78
    /**
79
     * Constructor.
80
     * Setup policy configuration.
81
     *
82
     * @param array $config The configuration
83
     */
84
    public function __construct(array $config = [])
85
    {
86
        $this->setConfig($config);
87
    }
88
89
    /**
90
     * Method to check if the request can be accessed.
91
     *
92
     * @param \Authorization\IdentityInterface|null $identity Identity
93
     * @param \Cake\Http\ServerRequest $request Server Request
94
     * @return \Authorization\Policy\ResultInterface|bool
95
     */
96
    public function canAccess(?IdentityInterface $identity, ServerRequest $request): bool|ResultInterface
97
    {
98
        $rule = $this->getRule($request);
99
        if (empty($rule)) {
100
            return $this->missingRuleResult();
101
        }
102
103
        if ($identity === null) {
104
            return new Result(false, 'missing identity');
105
        }
106
107
        if ($rule instanceof RequestPolicyInterface) {
108
            return $rule->canAccess($identity, $request);
109
        }
110
111
        if (is_callable($rule)) {
112
            return $rule($identity, $request);
113
        }
114
115
        if (is_array($rule)) {
116
            return $this->applyRolesPolicy($identity, $rule);
117
        }
118
119
        if (!is_string($rule)) {
120
            throw new LogicException(sprintf(
121
                'Invalid rule for %s::%s() in RequestPolicy',
122
                $request->getParam('controller'),
123
                $request->getParam('action')
124
            ));
125
        }
126
127
        $policyRuleClass = App::className($rule, 'Policy');
128
        if ($policyRuleClass === null) {
129
            return $this->applyRolesPolicy($identity, [$rule]);
130
        }
131
132
        $policyRule = new $policyRuleClass();
133
        if (!$policyRule instanceof RequestPolicyInterface) {
134
            throw new MissingMethodException(['canAccess', 'access', get_class($policyRule)]);
135
        }
136
137
        return $policyRule->canAccess($identity, $request);
138
    }
139
140
    /**
141
     * Build missing rule result.
142
     *
143
     * @return \Authorization\Policy\Result
144
     */
145
    protected function missingRuleResult(): Result
146
    {
147
        if ($this->getConfig('ruleRequired') === true) {
148
            return new Result(false, 'required rule is missing');
149
        }
150
151
        return new Result(true);
152
    }
153
154
    /**
155
     * Apply a simple role policy.
156
     * If `$identity` belongs to one of `$roles` than it can access.
157
     *
158
     * @param \Authorization\IdentityInterface $identity Identity
159
     * @param array $roles The roles to check against
160
     * @return \Authorization\Policy\Result
161
     */
162
    protected function applyRolesPolicy(IdentityInterface $identity, array $roles): Result
163
    {
164
        $identityRoles = (array)Hash::get((array)$identity->getOriginalData(), 'roles');
165
166
        if (empty(array_intersect($identityRoles, $roles))) {
167
            return new Result(false, 'request forbidden for identity\'s roles');
168
        }
169
170
        return new Result(true);
171
    }
172
173
    /**
174
     * Get the rule to apply.
175
     *
176
     * @param \Cake\Http\ServerRequest $request Server Request
177
     * @return mixed
178
     */
179
    protected function getRule(ServerRequest $request): mixed
180
    {
181
        $controller = $request->getParam('controller');
182
        $rule = $this->getConfig(sprintf('rules.%s', $controller));
183
        if ($rule === null) {
184
            return false; // no rule was set
185
        }
186
187
        if (is_string($rule) || is_callable($rule) || $rule instanceof RequestPolicyInterface) {
188
            return $rule;
189
        }
190
191
        if (!is_array($rule)) {
192
            throw new LogicException(sprintf('Invalid Rule for %s in RequestPolicy', $controller));
193
        }
194
195
        $action = $request->getParam('action');
196
        $defaultRule = Hash::get($rule, '*');
197
198
        return Hash::get($rule, $action, $defaultRule);
199
    }
200
}
201