Completed
Pull Request — master (#23)
by
unknown
02:24
created

RealMeSetupTask::validateEntityID()   C

Complexity

Conditions 9
Paths 12

Size

Total Lines 77

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 12
nop 1
dl 0
loc 77
rs 6.9462
c 0
b 0
f 0

How to fix   Long Method   

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
namespace SilverStripe\RealMe\Task;
4
5
use Exception;
6
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Core\Manifest\ModuleLoader;
9
use SilverStripe\RealMe\RealMeService;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Dev\BuildTask;
13
14
/**
15
 * Class RealMeSetupTask
16
 *
17
 * This class is intended to be run by a server administrator once the module is setup and configured via environment
18
 * variables, and YML fragments. The following tasks are done by this build task:
19
 *
20
 * - Check to ensure that the task is being run from the cmdline (not in the browser, it's too sensitive)
21
 * - Check to ensure that the task hasn't already been run, and if it has, fail unless `force=1` is passed to the script
22
 * - Validate all required values have been added in the appropriate place, and provide appropriate errors if not
23
 * - Output metadata XML that must be submitted to RealMe in order to integrate with ITE and Production environments
24
 */
25
class RealMeSetupTask extends BuildTask
26
{
27
    private static $segment = 'RealMeSetupTask';
0 ignored issues
show
introduced by
The private property $segment is not used, and could be removed.
Loading history...
28
29
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
30
        'Service' => '%$' . RealMeService::class,
31
    ];
32
33
    protected $title = "RealMe Setup Task";
34
35
    protected $description = 'Validates a realme configuration & creates the resources needed to integrate with realme';
36
37
    /**
38
     * @var RealMeService
39
     */
40
    private $service;
41
42
    /**
43
     * A list of validation errors found while validating the realme configuration.
44
     *
45
     * @var string[]
46
     */
47
    private $errors = array();
48
49
    /**
50
     * Run this setup task. See class phpdoc for the full description of what this does
51
     *
52
     * @param SS_HTTPRequest $request
0 ignored issues
show
Bug introduced by
The type SilverStripe\RealMe\Task\SS_HTTPRequest 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...
53
     */
54
    public function run($request)
55
    {
56
        try {
57
            // Ensure we are running on the command-line, and not running in a browser
58
            if (false === Director::is_cli()) {
59
                throw new Exception(_t(
60
                    self::class . '.ERR_NOT_CLI',
61
                    'This task can only be run from the command-line, not in your browser.'
62
                ));
63
            }
64
65
            // Validate all required values exist
66
            $forEnv = $request->getVar('forEnv');
67
68
            // Throws an exception if there was a problem with the config.
69
            $this->validateInputs($forEnv);
70
71
            $this->outputMetadataXmlContent($forEnv);
72
73
            $this->message(PHP_EOL . _t(
74
                self::class . '.BUILD_FINISH',
75
                'RealMe setup complete. Please copy the XML into a file for upload to the %s environment or DIA ' .
76
                'to complete the integration',
77
                '',
78
                array('env' => $forEnv)
79
            ));
80
        } catch (Exception $e) {
81
            $this->message($e->getMessage() . PHP_EOL);
82
        }
83
    }
84
85
    /**
86
     * @param RealMeService $service
87
     */
88
    public function setService($service)
89
    {
90
        $this->service = $service;
91
92
        return $this;
93
    }
94
95
    /**
96
     * Validate all inputs to this setup script. Ensures that all required values are available, where-ever they need to
97
     * be loaded from (environment variables, Config API, or directly passed to this script via the cmd-line)
98
     *
99
     * @param string $forEnv The environment that we want to output content for (mts, ite, or prod)
100
     *
101
     * @throws Exception if there were errors with the request or setup format.
102
     */
103
    private function validateInputs($forEnv)
104
    {
105
        // Ensure that 'forEnv=' is specified on the cli, and ensure that it matches a RealMe environment
106
        $this->validateRealMeEnvironments($forEnv);
107
108
        // Ensure we have the necessary directory structures, and their visibility
109
        $this->validateDirectoryStructure();
110
111
        // Ensure we have the certificates in the correct places.
112
        $this->validateCertificates();
113
114
        // Ensure the entityID is valid, and the privacy realm and service name are correct
115
        $this->validateEntityID($forEnv);
116
117
        // Make sure we have an authncontext for each environment.
118
        $this->validateAuthNContext();
119
120
        // Ensure data required for metadata XML output exists
121
        $this->validateMetadata();
122
123
        // Output validation errors, if any are found
124
        if (sizeof($this->errors) > 0) {
125
            $errorList = PHP_EOL . ' - ' . join(PHP_EOL . ' - ', $this->errors);
126
127
            throw new Exception(_t(
128
                self::class . '.ERR_VALIDATION',
129
                'There were {numissues} issue(s) found during validation that must be fixed prior to setup: {issues}',
130
                '',
131
                array(
132
                    'numissues' => sizeof($this->errors),
133
                    'issues' => $errorList
134
                )
135
            ));
136
        }
137
138
        $this->message(_t(
139
            self::class . '.VALIDATION_SUCCESS',
140
            'Validation succeeded, continuing with setup...'
141
        ));
142
    }
143
144
    /**
145
     * Outputs metadata template XML to console, so it can be sent to RealMe Operations team
146
     *
147
     * @param string $forEnv The RealMe environment to output metadata content for (e.g. mts, ite, prod).
148
     */
149
    private function outputMetadataXmlContent($forEnv)
150
    {
151
        // Output metadata XML so that it can be sent to RealMe via the agency
152
        $this->message(_t(
153
            self::class . '.OUPUT_PREFIX',
154
            'Metadata XML is listed below for the \'%s\' RealMe environment, this should be sent to the agency so they '
155
                . 'can pass it on to RealMe Operations staff',
156
            $forEnv
157
        ) . PHP_EOL . PHP_EOL);
158
159
        $configDir = $this->getConfigurationTemplateDir();
160
        $templateFile = Controller::join_links($configDir, 'metadata.xml');
161
162
        if (false === $this->isReadable($templateFile)) {
163
            throw new Exception(sprintf("Can't read metadata.xml file at %s", $templateFile));
164
        }
165
166
        $supportContact = $this->service->getMetadataContactSupport();
167
168
        $message = $this->replaceTemplateContents(
169
            $templateFile,
170
            array(
171
                '{{entityID}}' => $this->service->getSPEntityID(),
172
                '{{certificate-data}}' => $this->service->getSPCertContent(),
173
                '{{nameidformat}}' => $this->service->getNameIdFormat(),
174
                '{{acs-url}}' => $this->service->getAssertionConsumerServiceUrlForEnvironment($forEnv),
175
                '{{organisation-name}}' => $this->service->getMetadataOrganisationName(),
176
                '{{organisation-display-name}}' => $this->service->getMetadataOrganisationDisplayName(),
177
                '{{organisation-url}}' => $this->service->getMetadataOrganisationUrl(),
178
                '{{contact-support1-company}}' => $supportContact['company'],
179
                '{{contact-support1-firstnames}}' => $supportContact['firstNames'],
180
                '{{contact-support1-surname}}' => $supportContact['surname'],
181
            )
182
        );
183
184
        $this->message($message);
185
    }
186
187
    /**
188
     * Replace content in a template file with an array of replacements
189
     *
190
     * @param string $templatePath The path to the template file
191
     * @param array|null $replacements An array of '{{variable}}' => 'value' replacements
192
     * @return string The contents, with all {{variables}} replaced
193
     */
194
    private function replaceTemplateContents($templatePath, $replacements = null)
195
    {
196
        $configText = file_get_contents($templatePath);
197
198
        if (true === is_array($replacements)) {
199
            $configText = str_replace(array_keys($replacements), array_values($replacements), $configText);
200
        }
201
202
        return $configText;
203
    }
204
205
    /**
206
     * @return string The full path to RealMe configuration
207
     */
208
    private function getConfigurationTemplateDir($x = false)
209
    {
210
        $dir = $this->config()->template_config_dir;
211
        $path = Controller::join_links(BASE_PATH, $dir);
212
213
        if ($dir && false !== $this->isReadable($path)) {
214
            return $path;
215
        }
216
217
        $path = ModuleLoader::inst()->getManifest()->getModule('realme')->getPath();
218
219
        return $path . '/templates/saml-conf';
220
    }
221
222
    /**
223
     * Output a message to the console
224
     * @param string $message
225
     * @return void
226
     */
227
    private function message($message)
228
    {
229
        echo $message . PHP_EOL;
230
    }
231
232
    /**
233
     * Thin wrapper around is_readable(), used mainly so we can test this class completely
234
     *
235
     * @param string $filename The filename or directory to test
236
     * @return bool true if the file/dir is readable, false if not
237
     */
238
    private function isReadable($filename)
239
    {
240
        return is_readable($filename);
241
    }
242
243
    /**
244
     * The entity ID will pass validation, but raise an exception if the format of the service name and privacy realm
245
     * are in the incorrect format.
246
     * The service name and privacy realm need to be under 10 chars eg.
247
     * http://hostname.domain/serviceName/privacyRealm
248
     *
249
     * @param string $forEnv
250
     * @return void
251
     */
252
    private function validateEntityID($forEnv)
253
    {
254
        $entityId = $this->service->getSPEntityID();
255
256
        if (is_null($entityId)) {
257
            $this->errors[] = _t(
258
                self::class . '.ERR_CONFIG_NO_ENTITYID',
259
                'No entityID specified for environment \'{env}\'. Specify this in your YML configuration, see the' .
260
                ' module documentation for more details',
261
                array('env' => $forEnv)
262
            );
263
        }
264
265
        // make sure the entityID is a valid URL
266
        $entityId = filter_var($entityId, FILTER_VALIDATE_URL);
267
        if ($entityId === false) {
268
            $this->errors[] = _t(
269
                self::class . '.ERR_CONFIG_ENTITYID',
270
                'The Entity ID (\'{entityId}\') must be https, not be \'localhost\', and must contain a valid ' .
271
                'service name and privacy realm e.g. https://my-realme-integration.govt.nz/p-realm/s-name',
272
                '',
273
                array(
274
                    'entityId' => $entityId
275
                )
276
            );
277
278
            // invalid entity id, no point continuing.
279
            return;
280
        }
281
282
        // check it's not localhost and HTTPS. and make sure we have a host / scheme
283
        $urlParts = parse_url($entityId);
284
        if ($urlParts['host'] === 'localhost' || $urlParts['scheme'] === 'http') {
285
            $this->errors[] = _t(
286
                self::class . '.ERR_CONFIG_ENTITYID',
287
                'The Entity ID (\'{entityId}\') must be https, not be \'localhost\', and must contain a valid ' .
288
                'service name and privacy realm e.g. https://my-realme-integration.govt.nz/p-realm/s-name',
289
                '',
290
                array(
291
                    'entityId' => $entityId
292
                )
293
            );
294
295
            // if there's this much wrong, we want them to fix it first.
296
            return;
297
        }
298
299
        $path = ltrim($urlParts['path']);
300
        $urlParts = preg_split("/\\//", $path);
301
302
303
        // A valid Entity ID is in the form of "https://www.domain.govt.nz/<privacy-realm>/<service-name>"
304
        // Validate Service Name
305
        $serviceName = array_pop($urlParts);
0 ignored issues
show
Bug introduced by
It seems like $urlParts can also be of type false; however, parameter $array of array_pop() does only seem to accept array, 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

305
        $serviceName = array_pop(/** @scrutinizer ignore-type */ $urlParts);
Loading history...
306
        if (mb_strlen($serviceName) > 20 || 0 === mb_strlen($serviceName)) {
307
            $this->errors[] = _t(
308
                self::class . '.ERR_CONFIG_ENTITYID_SERVICE_NAME',
309
                'The service name \'{serviceName}\' must be a maximum of 20 characters and not blank for entityID ' .
310
                '\'{entityId}\'',
311
                '',
312
                array(
313
                    'serviceName' => $serviceName,
314
                    'entityId' => $entityId
315
                )
316
            );
317
        }
318
319
        // Validate Privacy Realm
320
        $privacyRealm = array_pop($urlParts);
321
        if (null === $privacyRealm || 0 === mb_strlen($privacyRealm)) {
322
            $this->errors[] = _t(
323
                self::class . '.ERR_CONFIG_ENTITYID_PRIVACY_REALM',
324
                'The privacy realm \'{privacyRealm}\' must not be blank for entityID \'{entityId}\'',
325
                '',
326
                array(
327
                    'privacyRealm' => $privacyRealm,
328
                    'entityId' => $entityId
329
                )
330
            );
331
        }
332
    }
333
334
    /**
335
     * Ensure we have an authncontext (how secure auth we require for each environment)
336
     *
337
     * e.g. urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength
338
     */
339
    private function validateAuthNContext()
340
    {
341
        foreach ($this->service->getAllowedRealMeEnvironments() as $env) {
342
            $context = $this->service->getAuthnContextForEnvironment($env);
343
            if (is_null($context)) {
344
                $this->errors[] = _t(
345
                    self::class . '.ERR_CONFIG_NO_AUTHNCONTEXT',
346
                    'No AuthnContext specified for environment \'{env}\'. Specify this in your YML configuration, ' .
347
                    'see the module documentation for more details',
348
                    array('env' => $env)
349
                );
350
            }
351
352
            if (!in_array($context, $this->service->getAllowedAuthNContextList())) {
353
                $this->errors[] = _t(
354
                    self::class . '.ERR_CONFIG_INVALID_AUTHNCONTEXT',
355
                    'The AuthnContext specified for environment \'{env}\' is invalid, please check your configuration',
356
                    array('env' => $env)
357
                );
358
            }
359
        }
360
    }
361
362
    /**
363
     * Ensure's the environment we're building the setup for exists.
364
     *
365
     * @param string $forEnv The environment that we're going to configure with this run.
366
     */
367
    private function validateRealMeEnvironments($forEnv)
368
    {
369
        $allowedEnvs = $this->service->getAllowedRealMeEnvironments();
370
        if (0 === mb_strlen($forEnv)) {
371
            $this->errors[] = _t(
372
                self::class . '.ERR_ENV_NOT_SPECIFIED',
373
                'The RealMe environment was not specified on the cli It must be one of: {allowedEnvs} ' .
374
                    'e.g. sake dev/tasks/RealMeSetupTask forEnv=mts',
375
                array(
376
                    'allowedEnvs' => join(', ', $allowedEnvs)
377
                )
378
            );
379
            return;
380
        }
381
382
        if (false === in_array($forEnv, $allowedEnvs)) {
383
            $this->errors[] = _t(
384
                self::class . '.ERR_ENV_NOT_ALLOWED',
385
                'The RealMe environment specified on the cli (\'{env}\') is not allowed. ' .
386
                    'It must be one of: {allowedEnvs}',
387
                array(
388
                    'env' => $forEnv,
389
                    'allowedEnvs' => join(', ', $allowedEnvs)
390
                )
391
            );
392
        }
393
    }
394
395
    /**
396
     * Ensures that the directory structure is correct and the necessary directories are writable.
397
     */
398
    private function validateDirectoryStructure()
399
    {
400
        if (is_null($this->service->getCertDir())) {
0 ignored issues
show
introduced by
The condition is_null($this->service->getCertDir()) is always false.
Loading history...
401
            $this->errors[] = _t(self::class . '.ERR_CERT_DIR_MISSING');
402
        } elseif (!$this->isReadable($this->service->getCertDir())) {
403
            $this->errors[] = _t(
404
                self::class . '.ERR_CERT_DIR_NOT_READABLE',
405
                '',
406
                '',
407
                array('dir' => $this->service->getCertDir())
408
            );
409
        }
410
    }
411
412
    /**
413
     * Ensures that the required metadata is filled out correctly in the realme configuration.
414
     */
415
    private function validateMetadata()
416
    {
417
        if (is_null($this->service->getMetadataOrganisationName())) {
418
            $this->errors[] = _t(self::class . '.ERR_CONFIG_NO_ORGANISATION_NAME');
419
        }
420
421
        if (is_null($this->service->getMetadataOrganisationDisplayName())) {
422
            $this->errors[] = _t(self::class . '.ERR_CONFIG_NO_ORGANISATION_DISPLAY_NAME');
423
        }
424
425
        if (is_null($this->service->getMetadataOrganisationUrl())) {
426
            $this->errors[] = _t(self::class . '.ERR_CONFIG_NO_ORGANISATION_URL');
427
        }
428
429
        $contact = $this->service->getMetadataContactSupport();
430
        if (is_null($contact['company']) || is_null($contact['firstNames']) || is_null($contact['surname'])) {
0 ignored issues
show
introduced by
The condition is_null($contact['surname']) is always false.
Loading history...
introduced by
The condition is_null($contact['firstNames']) is always false.
Loading history...
431
            $this->errors[] = _t(self::class . '.ERR_CONFIG_NO_SUPPORT_CONTACT');
432
        }
433
    }
434
435
    /**
436
     * Ensures the certificates are readable and that the service can sign and unencrypt using them
437
     */
438
    private function validateCertificates()
439
    {
440
        $signingCertFile = $this->service->getSigningCertPath();
441
        if (is_null($signingCertFile) || !$this->isReadable($signingCertFile)) {
442
            $this->errors[] = _t(
443
                self::class . '.ERR_CERT_NO_SIGNING_CERT',
444
                '',
445
                '',
446
                array(
447
                    'const' => 'REALME_SIGNING_CERT_FILENAME'
448
                )
449
            );
450
        } elseif (is_null($this->service->getSPCertContent())) {
451
            // Signing cert exists, but doesn't include BEGIN/END CERTIFICATE lines, or doesn't contain the cert
452
            $this->errors[] = _t(
453
                self::class . '.ERR_CERT_SIGNING_CERT_CONTENT',
454
                '',
455
                '',
456
                array('file' => $this->service->getSigningCertPath())
457
            );
458
        }
459
    }
460
}
461