determineNonExistentInstitutions()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 2
dl 0
loc 13
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Copyright 2016 SURFnet B.V.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupMiddleware\ManagementBundle\Validator;
20
21
use Assert\Assertion;
22
use Assert\AssertionFailedException;
23
use Assert\InvalidArgumentException as AssertionException;
24
use InvalidArgumentException as CoreInvalidArgumentException;
25
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
26
use Surfnet\StepupMiddleware\ApiBundle\Configuration\Entity\ConfiguredInstitution;
27
use Surfnet\StepupMiddleware\ApiBundle\Configuration\Service\ConfiguredInstitutionService;
28
use Surfnet\StepupMiddleware\ApiBundle\Identity\Entity\WhitelistEntry;
29
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\WhitelistService;
30
use Surfnet\StepupMiddleware\ManagementBundle\Exception\InvalidArgumentException;
31
use Surfnet\StepupMiddleware\ManagementBundle\Validator\Assert as StepupAssert;
32
use Symfony\Component\Validator\Constraint;
33
use Symfony\Component\Validator\ConstraintValidator;
34
use Symfony\Component\Validator\Violation\ConstraintViolationBuilder;
35
36
/**
37
 * @SuppressWarnings("PHPMD.CouplingBetweenObjects") Coupling to assertion classes is rather high, might be a good candidate for refactoring
38
 */
39
final class ReconfigureInstitutionRequestValidator extends ConstraintValidator
40
{
41
    /**
42
     * @var string[] internal cache, access through getConfiguredInstitutions()
43
     */
44
    private ?array $configuredInstitutions = null;
45
46
    /**
47
     * @var string[] internal cache, access through getWhitelistedInstitutions()
48
     */
49
    private ?array $whitelistedInstitutions = null;
50
51
    public function __construct(
52
        private readonly ConfiguredInstitutionService $configuredInstitutionsService,
53
        private readonly SecondFactorTypeService $secondFactorTypeService,
54
        private readonly WhitelistService $whitelistService,
55
    ) {
56
    }
57
58
    public function validate(mixed $value, Constraint $constraint): void
59
    {
60
        /** @var ConstraintViolationBuilder|false $violation */
61
        $violation = false;
62
63
        try {
64
            $this->validateRoot($value);
65
        } catch (AssertionException $exception) {
66
            // method is not in the interface yet, but the old method is deprecated.
67
            $violation = $this->context->buildViolation($exception->getMessage());
68
            $violation->atPath($exception->getPropertyPath());
0 ignored issues
show
Bug introduced by
It seems like $exception->getPropertyPath() can also be of type null; however, parameter $path of Symfony\Component\Valida...lderInterface::atPath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
            $violation->atPath(/** @scrutinizer ignore-type */ $exception->getPropertyPath());
Loading history...
69
        } catch (CoreInvalidArgumentException $exception) {
70
            $violation = $this->context->buildViolation($exception->getMessage());
71
        }
72
73
        if ($violation) {
74
            $violation->addViolation();
75
        }
76
    }
77
78
    public function validateRoot(array $configuration): void
79
    {
80
        $this->validateInstitutionsExist(array_keys($configuration));
81
82
        foreach ($configuration as $institution => $options) {
83
            $this->validateInstitutionConfigurationOptions($options, $institution);
84
        }
85
    }
86
87
    public function validateInstitutionsExist(array $institutions): void
88
    {
89
        $configuredInstitutions = $this->getConfiguredInstitutions();
90
91
        $nonExistentInstitutions = $this->determineNonExistentInstitutions($institutions, $configuredInstitutions);
92
93
        if ($nonExistentInstitutions !== []) {
94
            throw new InvalidArgumentException(
95
                sprintf('Cannot reconfigure non-existent institution(s): %s', implode(', ', $nonExistentInstitutions)),
96
            );
97
        }
98
    }
99
100
    /**
101
     * @SuppressWarnings("PHPMD.ExcessiveMethodLength")
102
     */
103
    public function validateInstitutionConfigurationOptions(array $options, string $institution): void
104
    {
105
        $propertyPath = sprintf('Institution(%s)', $institution);
106
        $requiredOptions = [
107
            'use_ra_locations',
108
            'show_raa_contact_information',
109
            'verify_email',
110
            'number_of_tokens_per_identity',
111
            'allowed_second_factors',
112
        ];
113
        $optionalOptions = [
114
            'self_vet',
115
            'sso_on_2fa',
116
            'sso_registration_bypass',
117
            'allow_self_asserted_tokens',
118
            'use_ra',
119
            'use_raa',
120
            'select_raa',
121
        ];
122
        StepupAssert::requiredAndOptionalOptions(
123
            $options,
124
            $requiredOptions,
125
            $optionalOptions,
126
            sprintf(
127
                'Invalid option(s) for "%s". Required options: "%s"; Optional options: "%s"',
128
                $institution,
129
                implode(', ', $requiredOptions),
130
                implode(', ', $optionalOptions),
131
            ),
132
            $propertyPath,
133
        );
134
        Assertion::boolean(
135
            $options['use_ra_locations'],
136
            sprintf('Option "use_ra_locations" for "%s" must be a boolean value', $institution),
137
            $propertyPath,
138
        );
139
        Assertion::boolean(
140
            $options['show_raa_contact_information'],
141
            sprintf('Option "show_raa_contact_information" for "%s" must be a boolean value', $institution),
142
            $propertyPath,
143
        );
144
        Assertion::boolean(
145
            $options['verify_email'],
146
            sprintf('Option "verify_email" for "%s" must be a boolean value', $institution),
147
            $propertyPath,
148
        );
149
        if (isset($options['self_vet'])) {
150
            Assertion::boolean(
151
                $options['self_vet'],
152
                sprintf('Option "self_vet" for "%s" must be a boolean value', $institution),
153
                $propertyPath,
154
            );
155
        }
156
        if (isset($options['sso_on_2fa'])) {
157
            Assertion::boolean(
158
                $options['sso_on_2fa'],
159
                sprintf('Option "sso_on_2fa" for "%s" must be a boolean value', $institution),
160
                $propertyPath,
161
            );
162
        }
163
        if (isset($options['sso_registration_bypass'])) {
164
            Assertion::boolean(
165
                $options['sso_registration_bypass'],
166
                sprintf('Option "sso_registration_bypass" for "%s" must be a boolean value', $institution),
167
                $propertyPath,
168
            );
169
        }
170
        if (isset($options['allow_self_asserted_tokens'])) {
171
            Assertion::nullOrBoolean(
172
                $options['allow_self_asserted_tokens'],
173
                sprintf('Option "allow_self_asserted_tokens" for "%s" must be a boolean value', $institution),
174
                $propertyPath,
175
            );
176
        }
177
        Assertion::integer(
178
            $options['number_of_tokens_per_identity'],
179
            sprintf('Option "number_of_tokens_per_identity" for "%s" must be an integer value', $institution),
180
            $propertyPath,
181
        );
182
        Assertion::min(
183
            $options['number_of_tokens_per_identity'],
184
            0,
185
            sprintf('Option "number_of_tokens_per_identity" for "%s" must be greater than or equal to 0', $institution),
186
            $propertyPath,
187
        );
188
        Assertion::isArray(
189
            $options['allowed_second_factors'],
190
            sprintf('Option "allowed_second_factors" for "%s" must be an array of strings', $institution),
191
            $propertyPath,
192
        );
193
        Assertion::allString(
194
            $options['allowed_second_factors'],
195
            sprintf('Option "allowed_second_factors" for "%s" must be an array of strings', $institution),
196
            $propertyPath,
197
        );
198
        Assertion::allInArray(
199
            $options['allowed_second_factors'],
200
            $this->secondFactorTypeService->getAvailableSecondFactorTypes(),
201
            'Option "allowed_second_factors" for "%s" must contain valid second factor types',
202
            $propertyPath,
203
        );
204
        $this->validateAuthorizationSettings($options, $institution, $propertyPath);
205
    }
206
207
    /**
208
     * Accessor for configured institutions to be able to use an internal cache
209
     *
210
     * @return string[]
211
     */
212
    private function getConfiguredInstitutions(): array
213
    {
214
        if ($this->configuredInstitutions !== null && $this->configuredInstitutions !== []) {
215
            return $this->configuredInstitutions;
216
        }
217
218
        $this->configuredInstitutions = array_map(
219
            fn(ConfiguredInstitution $configuredInstitution): string => $configuredInstitution->institution->getInstitution(),
220
            $this->configuredInstitutionsService->getAll(),
221
        );
222
223
        return $this->configuredInstitutions;
224
    }
225
226
    /**
227
     * Accessor for whitelisted institutions to be able to use an internal cache
228
     *
229
     * @return string[]
230
     */
231
    private function getWhitelistedInstitutions(): array
232
    {
233
        if ($this->whitelistedInstitutions !== null && $this->whitelistedInstitutions !== []) {
234
            return $this->whitelistedInstitutions;
235
        }
236
237
        $this->whitelistedInstitutions = array_map(
238
            fn(WhitelistEntry $whitelistEntry): string => (string)$whitelistEntry->institution,
239
            $this->whitelistService->getAllEntries()->toArray(),
240
        );
241
242
        return $this->whitelistedInstitutions;
243
    }
244
245
    /**
246
     * @param string[] $institutions
247
     * @param string[] $configuredInstitutions
248
     * @return string[]
249
     */
250
    public function determineNonExistentInstitutions(array $institutions, array $configuredInstitutions): array
251
    {
252
        $normalizedConfiguredInstitutions = array_map(
253
            strtolower(...),
0 ignored issues
show
Bug introduced by
The type Surfnet\StepupMiddleware...le\Validator\strtolower was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
254
            $configuredInstitutions,
255
        );
256
257
        return array_filter(
258
            $institutions,
259
            function ($institution) use ($normalizedConfiguredInstitutions): bool {
260
                $normalizedInstitution = strtolower($institution);
261
262
                return !in_array($normalizedInstitution, $normalizedConfiguredInstitutions);
263
            },
264
        );
265
    }
266
267
    /**
268
     * Validates if the authorization_settings array is configured correctly
269
     *
270
     *  - The optional options should contain whitelisted institutions
271
     *  - Or be empty
272
     *
273
     * @throws AssertionFailedException
274
     */
275
    private function validateAuthorizationSettings(
276
        array $authorizationSettings,
277
        string $institution,
278
        string $propertyPath,
279
    ): void {
280
        $acceptedOptions = [
281
            'use_ra',
282
            'use_raa',
283
            'select_raa',
284
        ];
285
286
        $whitelistedInstitutions = $this->getWhitelistedInstitutions();
287
288
        foreach ($authorizationSettings as $optionName => $setting) {
289
            if (in_array($optionName, $acceptedOptions)) {
290
                // 1. Value must be array
291
                Assertion::isArray(
292
                    $setting,
293
                    sprintf(
294
                        'Option "%s" for "%s" must be an array of strings. ("%s") was passed.',
295
                        $optionName,
296
                        $institution,
297
                        var_export($setting, true),
298
                    ),
299
                    $propertyPath,
300
                );
301
302
                // 2. The contents of the array must be empty or string
303
                Assertion::allString(
304
                    $setting,
305
                    sprintf(
306
                        'All values of option "%s" for "%s" should be of type string. ("%s") was passed.',
307
                        $optionName,
308
                        $institution,
309
                        var_export($setting, true),
310
                    ),
311
                    $propertyPath,
312
                );
313
314
                // 3. The institutions that are used in the configuration, should be known, configured, institutions
315
                Assertion::allInArray(
316
                    $authorizationSettings[$optionName],
317
                    $whitelistedInstitutions,
318
                    sprintf(
319
                        'All values of option "%s" for "%s" should be known institutions. ("%s") was passed.',
320
                        $optionName,
321
                        $institution,
322
                        var_export($setting, true),
323
                    ),
324
                    $propertyPath,
325
                );
326
            }
327
        }
328
    }
329
}
330