Passed
Push — master ( 14655d...b2e44c )
by Tim
09:17 queued 06:35
created

ACL::opEquals()   B

Complexity

Conditions 7
Paths 22

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 18
nc 22
nop 2
dl 0
loc 31
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\statistics;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
11
use function array_shift;
12
use function array_key_exists;
13
use function in_array;
14
use function is_string;
15
use function preg_grep;
16
use function preg_match;
17
use function sprintf;
18
use function var_export;
19
20
/**
21
 * Generic library for access control lists.
22
 *
23
 * @package SimpleSAMLphp
24
 */
25
26
class ACL
27
{
28
    /**
29
     * The access control list, as an array.
30
     *
31
     * @var array
32
     */
33
    private array $acl;
34
35
36
    /**
37
     * Initializer for this access control list.
38
     *
39
     * @param array|string $acl  The access control list.
40
     */
41
    public function __construct(array|string $acl)
42
    {
43
        if (is_string($acl)) {
0 ignored issues
show
introduced by
The condition is_string($acl) is always false.
Loading history...
44
            $acl = self::getById($acl);
45
        }
46
47
        foreach ($acl as $rule) {
48
            Assert::isArray(
49
                $rule,
50
                sprintf('Invalid rule in access control list: %s', var_export($rule, true)),
51
                Error\Exception::class,
52
            );
53
54
            Assert::minCount($rule, 1, 'Empty rule in access control list.', Error\Exception::class);
55
56
            $action = array_shift($rule);
57
            Assert::oneOf(
58
                $action,
59
                ['allow', 'deny'],
60
                sprintf('Invalid action in rule in access control list: %s', var_export($action, true)),
61
                Error\Exception::class,
62
            );
63
        }
64
        $this->acl = $acl;
65
    }
66
67
    /**
68
     * Retrieve an access control list with the given id.
69
     *
70
     * @param string $id  The id of the access control list.
71
     * @return array  The access control list array.
72
     */
73
    private static function getById(string $id): array
74
    {
75
        $config = Configuration::getOptionalConfig('acl.php');
76
        if (!$config->hasValue($id)) {
77
            throw new Error\Exception('No ACL with id ' . var_export($id, true) . ' in config/acl.php.');
78
        }
79
80
        return $config->getArray($id);
81
    }
82
83
84
    /**
85
     * Match the attributes against the access control list.
86
     *
87
     * @param array $attributes  The attributes of an user.
88
     * @return boolean  TRUE if the user is allowed to access the resource, FALSE if not.
89
     */
90
    public function allows(array $attributes): bool
91
    {
92
        foreach ($this->acl as $rule) {
93
            $action = array_shift($rule);
94
95
            if (!self::match($attributes, $rule)) {
96
                continue;
97
            }
98
99
            if ($action === 'allow') {
100
                return true;
101
            } else {
102
                return false;
103
            }
104
        }
105
        return false;
106
    }
107
108
109
    /**
110
     * Match the attributes against the given rule.
111
     *
112
     * @param array $attributes  The attributes of an user.
113
     * @param array $rule  The rule we should check.
114
     * @return boolean  TRUE if the rule matches, FALSE if not.
115
     */
116
    private static function match(array $attributes, array $rule): bool
117
    {
118
        $op = array_shift($rule);
119
        if ($op === null) {
120
            // An empty rule always matches
121
            return true;
122
        }
123
124
        switch ($op) {
125
            case 'and':
126
                return self::opAnd($attributes, $rule);
127
            case 'equals':
128
                return self::opEquals($attributes, $rule);
129
            case 'equals-preg':
130
                return self::opEqualsPreg($attributes, $rule);
131
            case 'has':
132
                return self::opHas($attributes, $rule);
133
            case 'has-preg':
134
                return self::opHasPreg($attributes, $rule);
135
            case 'not':
136
                return !self::match($attributes, $rule);
137
            case 'or':
138
                return self::opOr($attributes, $rule);
139
            default:
140
                throw new Error\Exception('Invalid ACL operation: ' . var_export($op, true));
141
        }
142
    }
143
144
145
    /**
146
     * 'and' match operator.
147
     *
148
     * @param array $attributes  The attributes of an user.
149
     * @param array $rule  The rule we should check.
150
     * @return boolean  TRUE if the rule matches, FALSE if not.
151
     */
152
    private static function opAnd(array $attributes, array $rule): bool
153
    {
154
        foreach ($rule as $subRule) {
155
            if (!self::match($attributes, $subRule)) {
156
                return false;
157
            }
158
        }
159
160
        // All matches
161
        return true;
162
    }
163
164
165
    /**
166
     * 'equals' match operator.
167
     *
168
     * @param array $attributes  The attributes of an user.
169
     * @param array $rule  The rule we should check.
170
     * @return boolean  TRUE if the rule matches, FALSE if not.
171
     */
172
    private static function opEquals(array $attributes, array $rule): bool
173
    {
174
        $attributeName = array_shift($rule);
175
176
        if (!array_key_exists($attributeName, $attributes)) {
177
            $attributeValues = [];
178
        } else {
179
            $attributeValues = $attributes[$attributeName];
180
        }
181
182
        foreach ($rule as $value) {
183
            $found = false;
184
            foreach ($attributeValues as $i => $v) {
185
                if ($value !== $v) {
186
                    continue;
187
                }
188
                unset($attributeValues[$i]);
189
                $found = true;
190
                break;
191
            }
192
            if (!$found) {
193
                return false;
194
            }
195
        }
196
        if (!empty($attributeValues)) {
197
            // One of the attribute values didn't match
198
            return false;
199
        }
200
201
        // All the values in the attribute matched one in the rule
202
        return true;
203
    }
204
205
206
    /**
207
     * 'equals-preg' match operator.
208
     *
209
     * @param array $attributes  The attributes of an user.
210
     * @param array $rule  The rule we should check.
211
     * @return boolean  TRUE if the rule matches, FALSE if not.
212
     */
213
    private static function opEqualsPreg(array $attributes, array $rule): bool
214
    {
215
        $attributeName = array_shift($rule);
216
217
        if (!array_key_exists($attributeName, $attributes)) {
218
            $attributeValues = [];
219
        } else {
220
            $attributeValues = $attributes[$attributeName];
221
        }
222
223
        foreach ($rule as $pattern) {
224
            $found = false;
225
            foreach ($attributeValues as $i => $v) {
226
                if (!preg_match($pattern, $v)) {
227
                    continue;
228
                }
229
                unset($attributeValues[$i]);
230
                $found = true;
231
                break;
232
            }
233
            if (!$found) {
234
                return false;
235
            }
236
        }
237
238
        if (!empty($attributeValues)) {
239
            // One of the attribute values didn't match
240
            return false;
241
        }
242
243
        // All the values in the attribute matched one in the rule
244
        return true;
245
    }
246
247
248
    /**
249
     * 'has' match operator.
250
     *
251
     * @param array $attributes  The attributes of an user.
252
     * @param array $rule  The rule we should check.
253
     * @return boolean  TRUE if the rule matches, FALSE if not.
254
     */
255
    private static function opHas(array $attributes, array $rule): bool
256
    {
257
        $attributeName = array_shift($rule);
258
259
        if (!array_key_exists($attributeName, $attributes)) {
260
            $attributeValues = [];
261
        } else {
262
            $attributeValues = $attributes[$attributeName];
263
        }
264
265
        foreach ($rule as $value) {
266
            if (!in_array($value, $attributeValues, true)) {
267
                return false;
268
            }
269
        }
270
271
        // Found all values in the rule in the attribute
272
        return true;
273
    }
274
275
276
    /**
277
     * 'has-preg' match operator.
278
     *
279
     * @param array $attributes  The attributes of an user.
280
     * @param array $rule  The rule we should check.
281
     * @return boolean  TRUE if the rule matches, FALSE if not.
282
     */
283
    private static function opHasPreg(array $attributes, array $rule): bool
284
    {
285
        $attributeName = array_shift($rule);
286
287
        if (!array_key_exists($attributeName, $attributes)) {
288
            $attributeValues = [];
289
        } else {
290
            $attributeValues = $attributes[$attributeName];
291
        }
292
293
        foreach ($rule as $pattern) {
294
            $matches = preg_grep($pattern, $attributeValues);
295
            if (count($matches) === 0) {
296
                return false;
297
            }
298
        }
299
300
        // Found all values in the rule in the attribute
301
        return true;
302
    }
303
304
305
    /**
306
     * 'or' match operator.
307
     *
308
     * @param array $attributes  The attributes of an user.
309
     * @param array $rule  The rule we should check.
310
     * @return boolean  TRUE if the rule matches, FALSE if not.
311
     */
312
    private static function opOr(array $attributes, array $rule): bool
313
    {
314
        foreach ($rule as $subRule) {
315
            if (self::match($attributes, $subRule)) {
316
                return true;
317
            }
318
        }
319
320
        // None matches
321
        return false;
322
    }
323
}
324