Consent::__construct()   F
last analyzed

Complexity

Conditions 15
Paths 319

Size

Total Lines 82
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 15
eloc 50
c 2
b 0
f 0
nc 319
nop 2
dl 0
loc 82
rs 3.5958

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Consent Authentication Processing filter
5
 *
6
 * Filter for requesting the user to give consent before attributes are
7
 * released to the SP.
8
 *
9
 * @package SimpleSAMLphp
10
 */
11
12
declare(strict_types=1);
13
14
namespace SimpleSAML\Module\consent\Auth\Process;
15
16
use Exception;
17
use SimpleSAML\Assert\Assert;
18
use SimpleSAML\Auth;
19
use SimpleSAML\Error;
20
use SimpleSAML\Logger;
21
use SimpleSAML\Module;
22
use SimpleSAML\Module\consent\Store;
23
use SimpleSAML\SAML2\Constants;
24
use SimpleSAML\Stats;
25
use SimpleSAML\Utils;
26
27
class Consent extends Auth\ProcessingFilter
28
{
29
    /**
30
     * Button to receive focus
31
     *
32
     * @var string|null
33
     */
34
    private ?string $focus = null;
35
36
    /**
37
     * Include attribute values
38
     *
39
     * @var bool
40
     */
41
    private bool $includeValues = false;
42
43
    /**
44
     * Check remember consent
45
     *
46
     * @var bool
47
     */
48
    private bool $checked = false;
49
50
    /**
51
     * Consent backend storage configuration
52
     *
53
     * @var \SimpleSAML\Module\consent\Store|null
54
     */
55
    private ?Store $store = null;
56
57
    /**
58
     * Attributes where the value should be hidden
59
     *
60
     * @var array
61
     */
62
    private array $hiddenAttributes = [];
63
64
    /**
65
     * Attributes which should not require consent
66
     *
67
     * @var array
68
     */
69
    private array $noconsentattributes = [];
70
71
    /**
72
     * Whether we should show the "about service"-link on the no consent page.
73
     *
74
     * @var bool
75
     */
76
    private bool $showNoConsentAboutService = true;
77
78
    /**
79
     * The name of the attribute that holds a unique identifier for the user
80
     *
81
     * @var string
82
     */
83
    private string $identifyingAttribute;
84
85
86
    /**
87
     * Initialize consent filter.
88
     *
89
     * Validates and parses the configuration.
90
     *
91
     * @param array $config Configuration information.
92
     * @param mixed $reserved For future use.
93
     *
94
     * @throws \SimpleSAML\Error\Exception if the configuration is not valid.
95
     */
96
    public function __construct(array $config, $reserved)
97
    {
98
        parent::__construct($config, $reserved);
99
100
        if (array_key_exists('includeValues', $config)) {
101
            if (!is_bool($config['includeValues'])) {
102
                throw new Error\Exception(
103
                    'Consent: includeValues must be boolean. ' .
104
                    var_export($config['includeValues'], true) . ' given.',
105
                );
106
            }
107
            $this->includeValues = $config['includeValues'];
108
        }
109
110
        if (array_key_exists('checked', $config)) {
111
            if (!is_bool($config['checked'])) {
112
                throw new Error\Exception(
113
                    'Consent: checked must be boolean. ' .
114
                    var_export($config['checked'], true) . ' given.',
115
                );
116
            }
117
            $this->checked = $config['checked'];
118
        }
119
120
        if (array_key_exists('focus', $config)) {
121
            if (!in_array($config['focus'], ['yes', 'no'], true)) {
122
                throw new Error\Exception(
123
                    'Consent: focus must be a string with values `yes` or `no`. ' .
124
                    var_export($config['focus'], true) . ' given.',
125
                );
126
            }
127
            $this->focus = $config['focus'];
128
        }
129
130
        if (array_key_exists('hiddenAttributes', $config)) {
131
            if (!is_array($config['hiddenAttributes'])) {
132
                throw new Error\Exception(
133
                    'Consent: hiddenAttributes must be an array. ' .
134
                    var_export($config['hiddenAttributes'], true) . ' given.',
135
                );
136
            }
137
            $this->hiddenAttributes = $config['hiddenAttributes'];
138
        }
139
140
        if (array_key_exists('attributes.exclude', $config)) {
141
            if (!is_array($config['attributes.exclude'])) {
142
                throw new Error\Exception(
143
                    'Consent: attributes.exclude must be an array. ' .
144
                    var_export($config['attributes.exclude'], true) . ' given.',
145
                );
146
            }
147
            $this->noconsentattributes = $config['attributes.exclude'];
148
        }
149
150
        if (array_key_exists('store', $config)) {
151
            try {
152
                $this->store = \SimpleSAML\Module\consent\Store::parseStoreConfig($config['store']);
153
            } catch (Exception $e) {
154
                Logger::error(
155
                    'Consent: Could not create consent storage: ' .
156
                    $e->getMessage(),
157
                );
158
            }
159
        }
160
161
        if (array_key_exists('showNoConsentAboutService', $config)) {
162
            if (!is_bool($config['showNoConsentAboutService'])) {
163
                throw new Error\Exception('Consent: showNoConsentAboutService must be a boolean.');
164
            }
165
            $this->showNoConsentAboutService = $config['showNoConsentAboutService'];
166
        }
167
168
        Assert::keyExists(
169
            $config,
170
            'identifyingAttribute',
171
            "Consent: Missing mandatory 'identifyingAttribute' config setting.",
172
        );
173
        Assert::stringNotEmpty(
174
            $config['identifyingAttribute'],
175
            "Consent: 'identifyingAttribute' must be a non-empty string.",
176
        );
177
        $this->identifyingAttribute = $config['identifyingAttribute'];
178
    }
179
180
181
    /**
182
     * Helper function to check whether consent is disabled.
183
     *
184
     * @param mixed  $option The consent.disable option. Either an array of array, an array or a boolean.
185
     * @param string $entityId The entityID of the SP/IdP.
186
     *
187
     * @return boolean True if disabled, false if not.
188
     */
189
    private static function checkDisable($option, string $entityId): bool
190
    {
191
        if (is_array($option)) {
192
            // Check if consent.disable array has one element that is an array
193
            if (count($option) === count($option, COUNT_RECURSIVE)) {
194
                // Array is not multidimensional.  Simple in_array search suffices
195
                return in_array($entityId, $option, true);
196
            }
197
198
            // Array contains at least one element that is an array, verify both possibilities
199
            if (in_array($entityId, $option, true)) {
200
                return true;
201
            }
202
203
            // Search in multidimensional arrays
204
            foreach ($option as $optionToTest) {
205
                if (!is_array($optionToTest)) {
206
                    continue; // bad option
207
                }
208
209
                if (!array_key_exists('type', $optionToTest)) {
210
                    continue; // option has no type
211
                }
212
213
                // Option has a type - switch processing depending on type value :
214
                if ($optionToTest['type'] === 'regex') {
215
                    // regex-based consent disabling
216
217
                    if (!array_key_exists('pattern', $optionToTest)) {
218
                        continue; // no pattern defined
219
                    }
220
221
                    if (preg_match($optionToTest['pattern'], $entityId) === 1) {
222
                        return true;
223
                    }
224
                } else {
225
                    // option type is not supported
226
                    continue;
227
                }
228
            } // end foreach
229
230
            // Base case : no match
231
            return false;
232
        } else {
233
            return (bool) $option;
234
        }
235
    }
236
237
238
    /**
239
     * Process a authentication response
240
     *
241
     * This function saves the state, and redirects the user to the page where the user can authorize the release of
242
     * the attributes. If storage is used and the consent has already been given the user is passed on.
243
     *
244
     * @param array &$state The state of the response.
245
     *
246
     *
247
     * @throws \SimpleSAML\Module\saml\Error\NoPassive if the request was passive and consent is needed.
248
     */
249
    public function process(array &$state): void
250
    {
251
        Assert::keyExists($state, 'Destination');
252
        Assert::keyExists($state['Destination'], 'entityid');
253
        Assert::keyExists($state['Destination'], 'metadata-set');
254
        Assert::keyExists($state['Source'], 'entityid');
255
        Assert::keyExists($state['Source'], 'metadata-set');
256
257
        $spEntityId = $state['Destination']['entityid'];
258
        $idpEntityId = $state['Source']['entityid'];
259
260
        $metadata = \SimpleSAML\Metadata\MetaDataStorageHandler::getMetadataHandler();
261
262
        /**
263
         * If the consent module is active on a bridge $state['saml:sp:IdP']
264
         * will contain an entry id for the remote IdP. If not, then the
265
         * consent module is active on a local IdP and nothing needs to be
266
         * done.
267
         */
268
        if (isset($state['saml:sp:IdP'])) {
269
            $idpEntityId = $state['saml:sp:IdP'];
270
            $idpmeta = $metadata->getMetaData($idpEntityId, 'saml20-idp-remote');
271
            $state['Source'] = $idpmeta;
272
        }
273
274
        $statsData = ['spEntityID' => $spEntityId];
275
276
        // Do not use consent if disabled
277
        if (
278
            isset($state['Source']['consent.disable']) &&
279
            self::checkDisable($state['Source']['consent.disable'], $spEntityId)
280
        ) {
281
            Logger::debug('Consent: Consent disabled for entity ' . $spEntityId . ' with IdP ' . $idpEntityId);
282
            Stats::log('consent:disabled', $statsData);
283
            return;
284
        }
285
        if (
286
            isset($state['Destination']['consent.disable']) &&
287
            self::checkDisable($state['Destination']['consent.disable'], $idpEntityId)
288
        ) {
289
            Logger::debug('Consent: Consent disabled for entity ' . $spEntityId . ' with IdP ' . $idpEntityId);
290
            Stats::log('consent:disabled', $statsData);
291
            return;
292
        }
293
294
        if ($this->store !== null) {
295
            $attributes = $state['Attributes'];
296
            Assert::keyExists(
297
                $attributes,
298
                $this->identifyingAttribute,
299
                "Consent: Missing '" . $this->identifyingAttribute . "' in user's attributes.",
300
            );
301
302
            $source = $state['Source']['metadata-set'] . '|' . $idpEntityId;
303
            $destination = $state['Destination']['metadata-set'] . '|' . $spEntityId;
304
305
            Assert::keyExists(
306
                $attributes,
307
                $this->identifyingAttribute,
308
                sprintf("Consent: No attribute '%s' was found in the user's attributes.", $this->identifyingAttribute),
309
            );
310
311
            $userId = $attributes[$this->identifyingAttribute][0];
312
            Assert::stringNotEmpty($userId);
313
314
            // Remove attributes that do not require consent
315
            foreach ($attributes as $attrkey => $attrval) {
316
                if (in_array($attrkey, $this->noconsentattributes, true)) {
317
                    unset($attributes[$attrkey]);
318
                }
319
            }
320
321
            Logger::debug('Consent: userid: ' . $userId);
322
            Logger::debug('Consent: source: ' . $source);
323
            Logger::debug('Consent: destination: ' . $destination);
324
325
            $hashedUserId = self::getHashedUserID($userId, $source);
326
            $targetedId = self::getTargetedID($userId, $source, $destination);
327
            $attributeSet = self::getAttributeHash($attributes, $this->includeValues);
328
329
            Logger::debug(
330
                'Consent: hasConsent() [' . $hashedUserId . '|' . $targetedId . '|' . $attributeSet . ']',
331
            );
332
333
            try {
334
                if ($this->store->hasConsent($hashedUserId, $targetedId, $attributeSet)) {
335
                    // Consent already given
336
                    Logger::stats('consent found');
337
                    Stats::log('consent:found', $statsData);
338
                    return;
339
                }
340
341
                Logger::stats('consent notfound');
342
                Stats::log('consent:notfound', $statsData);
343
344
                $state['consent:store'] = $this->store;
345
                $state['consent:store.userId'] = $hashedUserId;
346
                $state['consent:store.destination'] = $targetedId;
347
                $state['consent:store.attributeSet'] = $attributeSet;
348
            } catch (\Exception $e) {
349
                Logger::error('Consent: Error reading from storage: ' . $e->getMessage());
350
                Logger::stats('Consent failed');
351
                Stats::log('consent:failed', $statsData);
352
            }
353
        } else {
354
            Logger::stats('consent nostorage');
355
            Stats::log('consent:nostorage', $statsData);
356
        }
357
358
        $state['consent:focus'] = $this->focus;
359
        $state['consent:checked'] = $this->checked;
360
        $state['consent:hiddenAttributes'] = $this->hiddenAttributes;
361
        $state['consent:noconsentattributes'] = $this->noconsentattributes;
362
        $state['consent:showNoConsentAboutService'] = $this->showNoConsentAboutService;
363
364
        // user interaction necessary. Throw exception on isPassive request
365
        if (isset($state['isPassive']) && $state['isPassive'] === true) {
366
            Stats::log('consent:nopassive', $statsData);
367
            throw new Module\saml\Error\NoPassive(
368
                Constants::STATUS_REQUESTER,
369
                'Unable to give consent on passive request.',
370
            );
371
        }
372
373
        // Save state and redirect
374
        $id = Auth\State::saveState($state, 'consent:request');
375
        $url = Module::getModuleURL('consent/getconsent');
376
377
        $httpUtils = new Utils\HTTP();
378
        $httpUtils->redirectTrustedURL($url, ['StateId' => $id]);
379
    }
380
381
382
    /**
383
     * Generate a unique identifier of the user.
384
     *
385
     * @param string $userid The user id.
386
     * @param string $source The source id.
387
     *
388
     * @return string SHA1 of the user id, source id and salt.
389
     */
390
    public static function getHashedUserID(string $userid, string $source): string
391
    {
392
        $configUtils = new Utils\Config();
393
        return hash('sha1', $userid . '|' . $configUtils->getSecretSalt() . '|' . $source);
394
    }
395
396
397
    /**
398
     * Generate a unique targeted identifier.
399
     *
400
     * @param string $userid The user id.
401
     * @param string $source The source id.
402
     * @param string $destination The destination id.
403
     *
404
     * @return string SHA1 of the user id, source id, destination id and salt.
405
     */
406
    public static function getTargetedID(string $userid, string $source, string $destination): string
407
    {
408
        $configUtils = new Utils\Config();
409
        return hash('sha1', $userid . '|' . $configUtils->getSecretSalt() . '|' . $source . '|' . $destination);
410
    }
411
412
413
    /**
414
     * Generate unique identifier for attributes.
415
     *
416
     * Create a hash value for the attributes that changes when attributes are added or removed. If the attribute
417
     * values are included in the hash, the hash will change if the values change.
418
     *
419
     * @param array  $attributes The attributes.
420
     * @param bool   $includeValues Whether or not to include the attribute value in the generation of the hash.
421
     *
422
     * @return string SHA1 of the user id, source id, destination id and salt.
423
     */
424
    public static function getAttributeHash(array $attributes, bool $includeValues = false): string
425
    {
426
        if ($includeValues) {
427
            foreach ($attributes as &$values) {
428
                sort($values);
429
            }
430
            ksort($attributes);
431
            $hashBase = serialize($attributes);
432
        } else {
433
            $names = array_keys($attributes);
434
            sort($names);
435
            $hashBase = implode('|', $names);
436
        }
437
        return hash('sha1', $hashBase);
438
    }
439
}
440