Completed
Push — feature/behat-support ( c4f7cb...a1a06f )
by Michiel
04:23
created

ReconfigureInstitutionRequestValidator   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 296
Duplicated Lines 6.42 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
wmc 18
lcom 1
cbo 14
dl 19
loc 296
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A validate() 19 19 4
A validateRoot() 0 9 2
A validateInstitutionsExist() 0 12 2
B validateInstitutionConfigurationOptions() 0 78 1
A getConfiguredInstitutions() 0 15 2
A getWhitelistedInstitutions() 0 15 2
A determineNonExistentInstitutions() 0 18 1
A validateAuthorizationSettings() 0 52 3

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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\InvalidArgumentException as AssertionException;
23
use InvalidArgumentException as CoreInvalidArgumentException;
24
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
25
use Surfnet\StepupMiddleware\ApiBundle\Configuration\Entity\ConfiguredInstitution;
26
use Surfnet\StepupMiddleware\ApiBundle\Configuration\Service\ConfiguredInstitutionService;
27
use Surfnet\StepupMiddleware\ApiBundle\Identity\Entity\WhitelistEntry;
28
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\WhitelistService;
29
use Surfnet\StepupMiddleware\ManagementBundle\Exception\InvalidArgumentException;
30
use Surfnet\StepupMiddleware\ManagementBundle\Validator\Assert as StepupAssert;
31
use Symfony\Component\Validator\Constraint;
32
use Symfony\Component\Validator\ConstraintValidator;
33
34
final class ReconfigureInstitutionRequestValidator extends ConstraintValidator
35
{
36
    /**
37
     * @var ConfiguredInstitutionService
38
     */
39
    private $configuredInstitutionsService;
40
41
    /**
42
     * @var string[] internal cache, access through getConfiguredInstitutions()
43
     */
44
    private $configuredInstitutions;
45
46
    /**
47
     * @var SecondFactorTypeService
48
     */
49
    private $secondFactorTypeService;
50
51
    /**
52
     * @var WhitelistService
53
     */
54
    private $whitelistService;
55
56
    /**
57
     * @var string[] internal cache, access through getWhitelistedInstitutions()
58
     */
59
    private $whitelistedInstitutions;
60
61
    public function __construct(
62
        ConfiguredInstitutionService $configuredInstitutionsService,
63
        SecondFactorTypeService $secondFactorTypeService,
64
        WhitelistService $whitelistService
65
    ) {
66
        $this->configuredInstitutionsService = $configuredInstitutionsService;
67
        $this->secondFactorTypeService = $secondFactorTypeService;
68
        $this->whitelistService = $whitelistService;
69
    }
70
71 View Code Duplication
    public function validate($value, Constraint $constraint)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
72
    {
73
        /** @var \Symfony\Component\Validator\Violation\ConstraintViolationBuilder|false $violation */
74
        $violation = false;
75
76
        try {
77
            $this->validateRoot($value);
78
        } catch (AssertionException $exception) {
79
            // method is not in the interface yet, but the old method is deprecated.
80
            $violation = $this->context->buildViolation($exception->getMessage());
81
            $violation->atPath($exception->getPropertyPath());
82
        } catch (CoreInvalidArgumentException $exception) {
83
            $violation = $this->context->buildViolation($exception->getMessage());
84
        }
85
86
        if ($violation) {
87
            $violation->addViolation();
88
        }
89
    }
90
91
    public function validateRoot(array $configuration)
92
    {
93
        Assertion::isArray($configuration, 'Invalid body structure, must be an object', '(root)');
94
        $this->validateInstitutionsExist(array_keys($configuration));
95
96
        foreach ($configuration as $institution => $options) {
97
            $this->validateInstitutionConfigurationOptions($options, $institution);
98
        }
99
    }
100
101
    /**
102
     * @param array $institutions
103
     */
104
    public function validateInstitutionsExist(array $institutions)
105
    {
106
        $configuredInstitutions = $this->getConfiguredInstitutions();
107
108
        $nonExistentInstitutions = $this->determineNonExistentInstitutions($institutions, $configuredInstitutions);
109
110
        if (!empty($nonExistentInstitutions)) {
111
            throw new InvalidArgumentException(
112
                sprintf('Cannot reconfigure non-existent institution(s): %s', implode(', ', $nonExistentInstitutions))
113
            );
114
        }
115
    }
116
117
    /**
118
     * @param array $options
119
     * @param string $institution
120
     */
121
    public function validateInstitutionConfigurationOptions($options, $institution)
122
    {
123
        $propertyPath = sprintf('Institution(%s)', $institution);
124
125
        Assertion::isArray($options, 'Invalid institution configuration, must be an object', $propertyPath);
126
127
        $requiredOptions = [
128
            'use_ra_locations',
129
            'show_raa_contact_information',
130
            'verify_email',
131
            'number_of_tokens_per_identity',
132
            'allowed_second_factors',
133
        ];
134
135
        $optionalOptions = [
136
            'use_ra',
137
            'use_raa',
138
            'select_raa',
139
        ];
140
141
        StepupAssert::requiredAndOptionalOptions(
142
            $options,
143
            $requiredOptions,
144
            $optionalOptions,
145
            sprintf('Expected only options "%s" for "%s"', join(', ', $requiredOptions), $institution),
146
            $propertyPath
147
        );
148
149
        Assertion::boolean(
150
            $options['use_ra_locations'],
151
            sprintf('Option "use_ra_locations" for "%s" must be a boolean value', $institution),
152
            $propertyPath
153
        );
154
155
        Assertion::boolean(
156
            $options['show_raa_contact_information'],
157
            sprintf('Option "show_raa_contact_information" for "%s" must be a boolean value', $institution),
158
            $propertyPath
159
        );
160
161
        Assertion::boolean(
162
            $options['verify_email'],
163
            sprintf('Option "verify_email" for "%s" must be a boolean value', $institution),
164
            $propertyPath
165
        );
166
167
        Assertion::integer(
168
            $options['number_of_tokens_per_identity'],
169
            sprintf('Option "number_of_tokens_per_identity" for "%s" must be an integer value', $institution),
170
            $propertyPath
171
        );
172
173
        Assertion::min(
174
            $options['number_of_tokens_per_identity'],
175
            0,
176
            sprintf('Option "number_of_tokens_per_identity" for "%s" must be greater than or equal to 0', $institution),
177
            $propertyPath
178
        );
179
180
        Assertion::isArray(
181
            $options['allowed_second_factors'],
182
            sprintf('Option "allowed_second_factors" for "%s" must be an array of strings', $institution),
183
            $propertyPath
184
        );
185
        Assertion::allString(
186
            $options['allowed_second_factors'],
187
            sprintf('Option "allowed_second_factors" for "%s" must be an array of strings', $institution),
188
            $propertyPath
189
        );
190
        Assertion::allInArray(
191
            $options['allowed_second_factors'],
192
            $this->secondFactorTypeService->getAvailableSecondFactorTypes(),
193
            'Option "allowed_second_factors" for "%s" must contain valid second factor types',
194
            $propertyPath
195
        );
196
197
        $this->validateAuthorizationSettings($options, $institution, $propertyPath);
198
    }
199
200
    /**
201
     * Accessor for configured institutions to be able to use an internal cache
202
     *
203
     * @return string[]
204
     */
205
    private function getConfiguredInstitutions()
206
    {
207
        if (!empty($this->configuredInstitutions)) {
208
            return $this->configuredInstitutions;
209
        }
210
211
        $this->configuredInstitutions = array_map(
212
            function (ConfiguredInstitution $configuredInstitution) {
213
                return $configuredInstitution->institution->getInstitution();
214
            },
215
            $this->configuredInstitutionsService->getAll()
216
        );
217
218
        return $this->configuredInstitutions;
219
    }
220
221
    /**
222
     * Accessor for whitelisted institutions to be able to use an internal cache
223
     *
224
     * @return string[]
225
     */
226
    private function getWhitelistedInstitutions()
227
    {
228
        if (!empty($this->whitelistedInstitutions)) {
229
            return $this->whitelistedInstitutions;
230
        }
231
232
        $this->whitelistedInstitutions = array_map(
233
            function (WhitelistEntry $whitelistEntry) {
234
                return (string)$whitelistEntry->institution;
235
            },
236
            $this->whitelistService->getAllEntries()->toArray()
237
        );
238
239
        return $this->whitelistedInstitutions;
240
    }
241
242
    /**
243
     * @param string[] $institutions
244
     * @param $configuredInstitutions
245
     * @return string[]
246
     */
247
    public function determineNonExistentInstitutions(array $institutions, $configuredInstitutions)
248
    {
249
        $normalizedConfiguredInstitutions = array_map(
250
            function ($institution) {
251
                return strtolower($institution);
252
            },
253
            $configuredInstitutions
254
        );
255
256
        return array_filter(
257
            $institutions,
258
            function ($institution) use ($normalizedConfiguredInstitutions) {
259
                $normalizedInstitution = strtolower($institution);
260
261
                return !in_array($normalizedInstitution, $normalizedConfiguredInstitutions);
262
            }
263
        );
264
    }
265
266
    /**
267
     * Validates if the authorization_settings array is configured correctly
268
     *
269
     *  - The optional options should contain whitelisted institutions
270
     *  - Or be empty
271
     *
272
     * @param $authorizationSettings
273
     * @param $institution
274
     * @param $propertyPath
275
     * @throws \Assert\AssertionFailedException
276
     */
277
    private function validateAuthorizationSettings($authorizationSettings, $institution, $propertyPath)
278
    {
279
        $acceptedOptions = [
280
            'use_ra',
281
            'use_raa',
282
            'select_raa',
283
        ];
284
285
        $whitelistedInstitutions = $this->getWhitelistedInstitutions();
286
287
        foreach ($authorizationSettings as $optionName => $setting) {
288
            if (in_array($optionName, $acceptedOptions)) {
289
290
                // 1. Value must be array
291
                Assertion::isArray(
292
                    $authorizationSettings[$optionName],
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
                    $authorizationSettings[$optionName],
305
                    sprintf(
306
                        'All values of option "%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" 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