Total Complexity | 45 |
Total Lines | 433 |
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 |
||
24 | class RealMeSetupTask extends BuildTask |
||
25 | { |
||
26 | private static $segment = 'RealMeSetupTask'; |
||
|
|||
27 | |||
28 | private static $dependencies = [ |
||
29 | 'Service' => '%$' . RealMeService::class, |
||
30 | ]; |
||
31 | |||
32 | protected $title = "RealMe Setup Task"; |
||
33 | |||
34 | protected $description = 'Validates a realme configuration & creates the resources needed to integrate with realme'; |
||
35 | |||
36 | /** |
||
37 | * @var RealMeService |
||
38 | */ |
||
39 | private $service; |
||
40 | |||
41 | /** |
||
42 | * A list of validation errors found while validating the realme configuration. |
||
43 | * |
||
44 | * @var string[] |
||
45 | */ |
||
46 | private $errors = array(); |
||
47 | |||
48 | /** |
||
49 | * Run this setup task. See class phpdoc for the full description of what this does |
||
50 | * |
||
51 | * @param SS_HTTPRequest $request |
||
52 | */ |
||
53 | public function run($request) |
||
54 | { |
||
55 | try { |
||
56 | // Ensure we are running on the command-line, and not running in a browser |
||
57 | if (false === Director::is_cli()) { |
||
58 | throw new Exception(_t( |
||
59 | self::class . '.ERR_NOT_CLI', |
||
60 | 'This task can only be run from the command-line, not in your browser.' |
||
61 | )); |
||
62 | } |
||
63 | |||
64 | // Validate all required values exist |
||
65 | $forEnv = $request->getVar('forEnv'); |
||
66 | |||
67 | // Throws an exception if there was a problem with the config. |
||
68 | $this->validateInputs($forEnv); |
||
69 | |||
70 | $this->outputMetadataXmlContent($forEnv); |
||
71 | |||
72 | $this->message(PHP_EOL . _t( |
||
73 | self::class . '.BUILD_FINISH', |
||
74 | 'RealMe setup complete. Please copy the XML into a file for upload to the %s environment or DIA ' . |
||
75 | 'to complete the integration', |
||
76 | '', |
||
77 | array('env' => $forEnv) |
||
78 | )); |
||
79 | } catch (Exception $e) { |
||
80 | $this->message($e->getMessage() . PHP_EOL); |
||
81 | } |
||
82 | } |
||
83 | |||
84 | /** |
||
85 | * @param RealMeService $service |
||
86 | */ |
||
87 | public function setService($service) |
||
88 | { |
||
89 | $this->service = $service; |
||
90 | |||
91 | return $this; |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * Validate all inputs to this setup script. Ensures that all required values are available, where-ever they need to |
||
96 | * be loaded from (environment variables, Config API, or directly passed to this script via the cmd-line) |
||
97 | * |
||
98 | * @param string $forEnv The environment that we want to output content for (mts, ite, or prod) |
||
99 | * |
||
100 | * @throws Exception if there were errors with the request or setup format. |
||
101 | */ |
||
102 | private function validateInputs($forEnv) |
||
103 | { |
||
104 | // Ensure that 'forEnv=' is specified on the cli, and ensure that it matches a RealMe environment |
||
105 | $this->validateRealMeEnvironments($forEnv); |
||
106 | |||
107 | // Ensure we have the necessary directory structures, and their visibility |
||
108 | $this->validateDirectoryStructure(); |
||
109 | |||
110 | // Ensure we have the certificates in the correct places. |
||
111 | $this->validateCertificates(); |
||
112 | |||
113 | // Ensure the entityID is valid, and the privacy realm and service name are correct |
||
114 | $this->validateEntityID($forEnv); |
||
115 | |||
116 | // Make sure we have an authncontext for each environment. |
||
117 | $this->validateAuthNContext(); |
||
118 | |||
119 | // Ensure data required for metadata XML output exists |
||
120 | $this->validateMetadata(); |
||
121 | |||
122 | // Output validation errors, if any are found |
||
123 | if (sizeof($this->errors) > 0) { |
||
124 | $errorList = PHP_EOL . ' - ' . join(PHP_EOL . ' - ', $this->errors); |
||
125 | |||
126 | throw new Exception(_t( |
||
127 | self::class . '.ERR_VALIDATION', |
||
128 | 'There were {numissues} issue(s) found during validation that must be fixed prior to setup: {issues}', |
||
129 | '', |
||
130 | array( |
||
131 | 'numissues' => sizeof($this->errors), |
||
132 | 'issues' => $errorList |
||
133 | ) |
||
134 | )); |
||
135 | } |
||
136 | |||
137 | $this->message(_t( |
||
138 | self::class . '.VALIDATION_SUCCESS', |
||
139 | 'Validation succeeded, continuing with setup...' |
||
140 | )); |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * Outputs metadata template XML to console, so it can be sent to RealMe Operations team |
||
145 | * |
||
146 | * @param string $forEnv The RealMe environment to output metadata content for (e.g. mts, ite, prod). |
||
147 | */ |
||
148 | private function outputMetadataXmlContent($forEnv) |
||
149 | { |
||
150 | // Output metadata XML so that it can be sent to RealMe via the agency |
||
151 | $this->message(_t( |
||
152 | self::class . '.OUPUT_PREFIX', |
||
153 | 'Metadata XML is listed below for the \'%s\' RealMe environment, this should be sent to the agency so they ' |
||
154 | . 'can pass it on to RealMe Operations staff', |
||
155 | $forEnv |
||
156 | ) . PHP_EOL . PHP_EOL); |
||
157 | |||
158 | $configDir = $this->getConfigurationTemplateDir(); |
||
159 | $templateFile = Controller::join_links($configDir, 'metadata.xml'); |
||
160 | |||
161 | if (false === $this->isReadable($templateFile)) { |
||
162 | throw new Exception(sprintf("Can't read metadata.xml file at %s", $templateFile)); |
||
163 | } |
||
164 | |||
165 | $supportContact = $this->service->getMetadataContactSupport(); |
||
166 | |||
167 | $message = $this->replaceTemplateContents( |
||
168 | $templateFile, |
||
169 | array( |
||
170 | '{{entityID}}' => $this->service->getSPEntityID(), |
||
171 | '{{certificate-data}}' => $this->service->getSPCertContent(), |
||
172 | '{{nameidformat}}' => $this->service->getNameIdFormat(), |
||
173 | '{{acs-url}}' => $this->service->getAssertionConsumerServiceUrlForEnvironment($forEnv), |
||
174 | '{{organisation-name}}' => $this->service->getMetadataOrganisationName(), |
||
175 | '{{organisation-display-name}}' => $this->service->getMetadataOrganisationDisplayName(), |
||
176 | '{{organisation-url}}' => $this->service->getMetadataOrganisationUrl(), |
||
177 | '{{contact-support1-company}}' => $supportContact['company'], |
||
178 | '{{contact-support1-firstnames}}' => $supportContact['firstNames'], |
||
179 | '{{contact-support1-surname}}' => $supportContact['surname'], |
||
180 | ) |
||
181 | ); |
||
182 | |||
183 | $this->message($message); |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Replace content in a template file with an array of replacements |
||
188 | * |
||
189 | * @param string $templatePath The path to the template file |
||
190 | * @param array|null $replacements An array of '{{variable}}' => 'value' replacements |
||
191 | * @return string The contents, with all {{variables}} replaced |
||
192 | */ |
||
193 | private function replaceTemplateContents($templatePath, $replacements = null) |
||
194 | { |
||
195 | $configText = file_get_contents($templatePath); |
||
196 | |||
197 | if (true === is_array($replacements)) { |
||
198 | $configText = str_replace(array_keys($replacements), array_values($replacements), $configText); |
||
199 | } |
||
200 | |||
201 | return $configText; |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * @return string The full path to RealMe configuration |
||
206 | */ |
||
207 | private function getConfigurationTemplateDir() |
||
208 | { |
||
209 | $dir = $this->config()->template_config_dir; |
||
210 | $path = Controller::join_links(BASE_PATH, $dir); |
||
211 | |||
212 | if ($dir && false !== $this->isReadable($path)) { |
||
213 | return $path; |
||
214 | } |
||
215 | |||
216 | return Controller::join_links(BASE_PATH, REALME_MODULE_PATH . '/templates/saml-conf'); |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Output a message to the console |
||
221 | * @param string $message |
||
222 | * @return void |
||
223 | */ |
||
224 | private function message($message) |
||
225 | { |
||
226 | echo $message . PHP_EOL; |
||
227 | } |
||
228 | |||
229 | /** |
||
230 | * Thin wrapper around is_readable(), used mainly so we can test this class completely |
||
231 | * |
||
232 | * @param string $filename The filename or directory to test |
||
233 | * @return bool true if the file/dir is readable, false if not |
||
234 | */ |
||
235 | private function isReadable($filename) |
||
236 | { |
||
237 | return is_readable($filename); |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * The entity ID will pass validation, but raise an exception if the format of the service name and privacy realm |
||
242 | * are in the incorrect format. |
||
243 | * The service name and privacy realm need to be under 10 chars eg. |
||
244 | * http://hostname.domain/serviceName/privacyRealm |
||
245 | * |
||
246 | * @param string $forEnv |
||
247 | * @return void |
||
248 | */ |
||
249 | private function validateEntityID($forEnv) |
||
250 | { |
||
251 | $entityId = $this->service->getSPEntityID(); |
||
252 | |||
253 | if (is_null($entityId)) { |
||
254 | $this->errors[] = _t( |
||
255 | self::class . '.ERR_CONFIG_NO_ENTITYID', |
||
256 | 'No entityID specified for environment \'{env}\'. Specify this in your YML configuration, see the' . |
||
257 | ' module documentation for more details', |
||
258 | '', |
||
259 | array('env' => $forEnv) |
||
260 | ); |
||
261 | } |
||
262 | |||
263 | // make sure the entityID is a valid URL |
||
264 | $entityId = filter_var($entityId, FILTER_VALIDATE_URL); |
||
265 | if ($entityId === false) { |
||
266 | $this->errors[] = _t( |
||
267 | self::class . '.ERR_CONFIG_ENTITYID', |
||
268 | 'The Entity ID (\'{entityId}\') must be https, not be \'localhost\', and must contain a valid ' . |
||
269 | 'service name and privacy realm e.g. https://my-realme-integration.govt.nz/p-realm/s-name', |
||
270 | '', |
||
271 | array( |
||
272 | 'entityId' => $entityId |
||
273 | ) |
||
274 | ); |
||
275 | |||
276 | // invalid entity id, no point continuing. |
||
277 | return; |
||
278 | } |
||
279 | |||
280 | // check it's not localhost and HTTPS. and make sure we have a host / scheme |
||
281 | $urlParts = parse_url($entityId); |
||
282 | if ($urlParts['host'] === 'localhost' || $urlParts['scheme'] === 'http') { |
||
283 | $this->errors[] = _t( |
||
284 | self::class . '.ERR_CONFIG_ENTITYID', |
||
285 | 'The Entity ID (\'{entityId}\') must be https, not be \'localhost\', and must contain a valid ' . |
||
286 | 'service name and privacy realm e.g. https://my-realme-integration.govt.nz/p-realm/s-name', |
||
287 | '', |
||
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 | '', |
||
310 | array( |
||
311 | 'serviceName' => $serviceName, |
||
312 | 'entityId' => $entityId |
||
313 | ) |
||
314 | ); |
||
315 | } |
||
316 | |||
317 | // Validate Privacy Realm |
||
318 | $privacyRealm = array_pop($urlParts); |
||
319 | if (null === $privacyRealm || 0 === mb_strlen($privacyRealm)) { |
||
320 | $this->errors[] = _t( |
||
321 | self::class . '.ERR_CONFIG_ENTITYID_PRIVACY_REALM', |
||
322 | 'The privacy realm \'{privacyRealm}\' must not be blank for entityID \'{entityId}\'', |
||
323 | '', |
||
324 | array( |
||
325 | 'privacyRealm' => $privacyRealm, |
||
326 | 'entityId' => $entityId |
||
327 | ) |
||
328 | ); |
||
329 | } |
||
330 | } |
||
331 | |||
332 | /** |
||
333 | * Ensure we have an authncontext (how secure auth we require for each environment) |
||
334 | * |
||
335 | * e.g. urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength |
||
336 | */ |
||
337 | private function validateAuthNContext() |
||
338 | { |
||
339 | foreach ($this->service->getAllowedRealMeEnvironments() as $env) { |
||
340 | $context = $this->service->getAuthnContextForEnvironment($env); |
||
341 | if (is_null($context)) { |
||
342 | $this->errors[] = _t( |
||
343 | self::class . '.ERR_CONFIG_NO_AUTHNCONTEXT', |
||
344 | 'No AuthnContext specified for environment \'{env}\'. Specify this in your YML configuration, ' . |
||
345 | 'see the module documentation for more details', |
||
346 | '', |
||
347 | array('env' => $env) |
||
348 | ); |
||
349 | } |
||
350 | |||
351 | if (!in_array($context, $this->service->getAllowedAuthNContextList())) { |
||
352 | $this->errors[] = _t( |
||
353 | self::class . '.ERR_CONFIG_INVALID_AUTHNCONTEXT', |
||
354 | 'The AuthnContext specified for environment \'{env}\' is invalid, please check your configuration', |
||
355 | '', |
||
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 | '', |
||
374 | '', |
||
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 | '', |
||
386 | '', |
||
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())) { |
||
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'])) { |
||
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() |
||
457 | ); |
||
458 | } |
||
459 | } |
||
460 | } |
||
461 |