Passed
Push — master ( 4251e4...a3bf9e )
by Thijs
05:03
created

ACL::opHas()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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