Total Complexity | 45 |
Total Lines | 448 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like RealMeSetupTask often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use RealMeSetupTask, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
26 | class RealMeSetupTask extends BuildTask |
||
27 | { |
||
28 | private static $segment = 'RealMeSetupTask'; |
||
|
|||
29 | |||
30 | private static $dependencies = [ |
||
31 | 'Service' => '%$' . RealMeService::class, |
||
32 | ]; |
||
33 | |||
34 | protected $title = "RealMe Setup Task"; |
||
35 | |||
36 | protected $description = 'Validates a realme configuration & creates the resources needed to integrate with realme'; |
||
37 | |||
38 | /** |
||
39 | * @var RealMeService |
||
40 | */ |
||
41 | private $service; |
||
42 | |||
43 | /** |
||
44 | * A list of validation errors found while validating the realme configuration. |
||
45 | * |
||
46 | * @var string[] |
||
47 | */ |
||
48 | private $errors = array(); |
||
49 | |||
50 | /** |
||
51 | * Run this setup task. See class phpdoc for the full description of what this does |
||
52 | * |
||
53 | * @param HTTPRequest $request |
||
54 | */ |
||
55 | public function run($request) |
||
56 | { |
||
57 | try { |
||
58 | // Ensure we are running on the command-line, and not running in a browser |
||
59 | if (false === Director::is_cli()) { |
||
60 | throw new Exception(_t( |
||
61 | self::class . '.ERR_NOT_CLI', |
||
62 | 'This task can only be run from the command-line, not in your browser.' |
||
63 | )); |
||
64 | } |
||
65 | |||
66 | // Validate all required values exist |
||
67 | $forEnv = $request->getVar('forEnv'); |
||
68 | |||
69 | // Throws an exception if there was a problem with the config. |
||
70 | $this->validateInputs($forEnv); |
||
71 | |||
72 | $this->outputMetadataXmlContent($forEnv); |
||
73 | |||
74 | $this->message(PHP_EOL . _t( |
||
75 | self::class . '.BUILD_FINISH', |
||
76 | 'RealMe setup complete. Please copy the XML into a file for upload to the {env} environment or DIA ' . |
||
77 | 'to complete the integration', |
||
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 | * @return $this |
||
88 | */ |
||
89 | public function setService($service) |
||
90 | { |
||
91 | $this->service = $service; |
||
92 | |||
93 | return $this; |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * Validate all inputs to this setup script. Ensures that all required values are available, where-ever they need to |
||
98 | * be loaded from (environment variables, Config API, or directly passed to this script via the cmd-line) |
||
99 | * |
||
100 | * @param string $forEnv The environment that we want to output content for (mts, ite, or prod) |
||
101 | * |
||
102 | * @throws Exception if there were errors with the request or setup format. |
||
103 | */ |
||
104 | private function validateInputs($forEnv) |
||
105 | { |
||
106 | // Ensure that 'forEnv=' is specified on the cli, and ensure that it matches a RealMe environment |
||
107 | $this->validateRealMeEnvironments($forEnv); |
||
108 | |||
109 | // Ensure we have the necessary directory structures, and their visibility |
||
110 | $this->validateDirectoryStructure(); |
||
111 | |||
112 | // Ensure we have the certificates in the correct places. |
||
113 | $this->validateCertificates(); |
||
114 | |||
115 | // Ensure the entityID is valid, and the privacy realm and service name are correct |
||
116 | $this->validateEntityID($forEnv); |
||
117 | |||
118 | // Make sure we have an authncontext for each environment. |
||
119 | $this->validateAuthNContext(); |
||
120 | |||
121 | // Ensure data required for metadata XML output exists |
||
122 | $this->validateMetadata(); |
||
123 | |||
124 | // Output validation errors, if any are found |
||
125 | if (sizeof($this->errors) > 0) { |
||
126 | $errorList = PHP_EOL . ' - ' . join(PHP_EOL . ' - ', $this->errors); |
||
127 | |||
128 | throw new Exception(_t( |
||
129 | self::class . '.ERR_VALIDATION', |
||
130 | 'There were {numissues} issue(s) found during validation that must be fixed prior to setup: {issues}', |
||
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 \'{env}\' RealMe environment, this should be sent to the agency so ' . |
||
155 | 'they can pass it on to RealMe Operations staff', |
||
156 | ['env' => $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() |
||
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) |
||
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) |
||
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 | array( |
||
273 | 'entityId' => $entityId |
||
274 | ) |
||
275 | ); |
||
276 | |||
277 | // invalid entity id, no point continuing. |
||
278 | return; |
||
279 | } |
||
280 | |||
281 | // check it's not localhost and HTTPS. and make sure we have a host / scheme |
||
282 | $urlParts = parse_url($entityId); |
||
283 | if ($urlParts['host'] === 'localhost' || $urlParts['scheme'] === 'http') { |
||
284 | $this->errors[] = _t( |
||
285 | self::class . '.ERR_CONFIG_ENTITYID', |
||
286 | 'The Entity ID (\'{entityId}\') must be https, not be \'localhost\', and must contain a valid ' . |
||
287 | 'service name and privacy realm e.g. https://my-realme-integration.govt.nz/p-realm/s-name', |
||
288 | array( |
||
289 | 'entityId' => $entityId |
||
290 | ) |
||
291 | ); |
||
292 | |||
293 | // if there's this much wrong, we want them to fix it first. |
||
294 | return; |
||
295 | } |
||
296 | |||
297 | $path = ltrim($urlParts['path']); |
||
298 | $urlParts = preg_split("/\\//", $path); |
||
299 | |||
300 | |||
301 | // A valid Entity ID is in the form of "https://www.domain.govt.nz/<privacy-realm>/<service-name>" |
||
302 | // Validate Service Name |
||
303 | $serviceName = array_pop($urlParts); |
||
304 | if (mb_strlen($serviceName) > 20 || 0 === mb_strlen($serviceName)) { |
||
305 | $this->errors[] = _t( |
||
306 | self::class . '.ERR_CONFIG_ENTITYID_SERVICE_NAME', |
||
307 | 'The service name \'{serviceName}\' must be a maximum of 20 characters and not blank for entityID ' . |
||
308 | '\'{entityId}\'', |
||
309 | array( |
||
310 | 'serviceName' => $serviceName, |
||
311 | 'entityId' => $entityId |
||
312 | ) |
||
313 | ); |
||
314 | } |
||
315 | |||
316 | // Validate Privacy Realm |
||
317 | $privacyRealm = array_pop($urlParts); |
||
318 | if (null === $privacyRealm || 0 === mb_strlen($privacyRealm)) { |
||
319 | $this->errors[] = _t( |
||
320 | self::class . '.ERR_CONFIG_ENTITYID_PRIVACY_REALM', |
||
321 | 'The privacy realm \'{privacyRealm}\' must not be blank for entityID \'{entityId}\'', |
||
322 | array( |
||
323 | 'privacyRealm' => $privacyRealm, |
||
324 | 'entityId' => $entityId |
||
325 | ) |
||
326 | ); |
||
327 | } |
||
328 | } |
||
329 | |||
330 | /** |
||
331 | * Ensure we have an authncontext (how secure auth we require for each environment) |
||
332 | * |
||
333 | * e.g. urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength |
||
334 | */ |
||
335 | private function validateAuthNContext() |
||
336 | { |
||
337 | foreach ($this->service->getAllowedRealMeEnvironments() as $env) { |
||
338 | $context = $this->service->getAuthnContextForEnvironment($env); |
||
339 | if (is_null($context)) { |
||
340 | $this->errors[] = _t( |
||
341 | self::class . '.ERR_CONFIG_NO_AUTHNCONTEXT', |
||
342 | 'No AuthnContext specified for environment \'{env}\'. Specify this in your YML configuration, ' . |
||
343 | 'see the module documentation for more details', |
||
344 | array('env' => $env) |
||
345 | ); |
||
346 | } |
||
347 | |||
348 | if (!in_array($context, $this->service->getAllowedAuthNContextList())) { |
||
349 | $this->errors[] = _t( |
||
350 | self::class . '.ERR_CONFIG_INVALID_AUTHNCONTEXT', |
||
351 | 'The AuthnContext specified for environment \'{env}\' is invalid, please check your configuration', |
||
352 | array('env' => $env) |
||
353 | ); |
||
354 | } |
||
355 | } |
||
356 | } |
||
357 | |||
358 | /** |
||
359 | * Ensure's the environment we're building the setup for exists. |
||
360 | * |
||
361 | * @param string $forEnv The environment that we're going to configure with this run. |
||
362 | */ |
||
363 | private function validateRealMeEnvironments($forEnv) |
||
364 | { |
||
365 | $allowedEnvs = $this->service->getAllowedRealMeEnvironments(); |
||
366 | if (0 === mb_strlen($forEnv)) { |
||
367 | $this->errors[] = _t( |
||
368 | self::class . '.ERR_ENV_NOT_SPECIFIED', |
||
369 | 'The RealMe environment was not specified on the cli It must be one of: {allowedEnvs} ' . |
||
370 | 'e.g. vendor/bin/sake dev/tasks/RealMeSetupTask forEnv=mts', |
||
371 | array( |
||
372 | 'allowedEnvs' => join(', ', $allowedEnvs) |
||
373 | ) |
||
374 | ); |
||
375 | return; |
||
376 | } |
||
377 | |||
378 | if (false === in_array($forEnv, $allowedEnvs)) { |
||
379 | $this->errors[] = _t( |
||
380 | self::class . '.ERR_ENV_NOT_ALLOWED', |
||
381 | 'The RealMe environment specified on the cli (\'{env}\') is not allowed. ' . |
||
382 | 'It must be one of: {allowedEnvs}', |
||
383 | array( |
||
384 | 'env' => $forEnv, |
||
385 | 'allowedEnvs' => join(', ', $allowedEnvs) |
||
386 | ) |
||
387 | ); |
||
388 | } |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * Ensures that the directory structure is correct and the necessary directories are writable. |
||
393 | */ |
||
394 | private function validateDirectoryStructure() |
||
395 | { |
||
396 | if (is_null($this->service->getCertDir())) { |
||
397 | $this->errors[] = _t( |
||
398 | self::class . '.ERR_CERT_DIR_MISSING', |
||
399 | 'No certificate dir is specified. Define the REALME_CERT_DIR environment variable in your .env file' |
||
400 | ); |
||
401 | } elseif (!$this->isReadable($this->service->getCertDir())) { |
||
402 | $this->errors[] = _t( |
||
403 | self::class . '.ERR_CERT_DIR_NOT_READABLE', |
||
404 | 'Certificate dir specified (\'{dir}\') must be created and be readable. Ensure permissions are set ' . |
||
405 | 'correctly and the directory is absolute', |
||
406 | array('dir' => $this->service->getCertDir()) |
||
407 | ); |
||
408 | } |
||
409 | } |
||
410 | |||
411 | /** |
||
412 | * Ensures that the required metadata is filled out correctly in the realme configuration. |
||
413 | */ |
||
414 | private function validateMetadata() |
||
415 | { |
||
416 | if (is_null($this->service->getMetadataOrganisationName())) { |
||
417 | $this->errors[] = _t( |
||
418 | self::class . '.ERR_CONFIG_NO_ORGANISATION_NAME', |
||
419 | 'No organisation name is specified in YML configuration. Ensure the \'metadata_organisation_name\' ' . |
||
420 | 'value is defined in your YML configuration' |
||
421 | ); |
||
422 | } |
||
423 | |||
424 | if (is_null($this->service->getMetadataOrganisationDisplayName())) { |
||
425 | $this->errors[] = _t( |
||
426 | self::class . '.ERR_CONFIG_NO_ORGANISATION_DISPLAY_NAME', |
||
427 | 'No organisation display name is specified in YML configuration. Ensure the ' . |
||
428 | '\'metadata_organisation_display_name\' value is defined in your YML configuration' |
||
429 | ); |
||
430 | } |
||
431 | |||
432 | if (is_null($this->service->getMetadataOrganisationUrl())) { |
||
433 | $this->errors[] = _t( |
||
434 | self::class . '.ERR_CONFIG_NO_ORGANISATION_URL', |
||
435 | 'No organisation URL is specified in YML configuration. Ensure the \'metadata_organisation_url\' ' . |
||
436 | 'value is defined in your YML configuration' |
||
437 | ); |
||
438 | } |
||
439 | |||
440 | $contact = $this->service->getMetadataContactSupport(); |
||
441 | if (is_null($contact['company']) || is_null($contact['firstNames']) || is_null($contact['surname'])) { |
||
442 | $this->errors[] = _t( |
||
443 | self::class . '.ERR_CONFIG_NO_SUPPORT_CONTACT', |
||
444 | 'Support contact detail is missing from YML configuration. Ensure the following values are defined ' . |
||
445 | 'in the YML configuration: metadata_contact_support_company, metadata_contact_support_firstnames,' . |
||
446 | ' metadata_contact_support_surname' |
||
447 | ); |
||
448 | } |
||
449 | } |
||
450 | |||
451 | /** |
||
452 | * Ensures the certificates are readable and that the service can sign and unencrypt using them |
||
453 | */ |
||
454 | private function validateCertificates() |
||
474 | ); |
||
475 | } |
||
476 | } |
||
477 | } |
||
478 |