Total Complexity | 61 |
Total Lines | 420 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like Fticks 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 Fticks, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
52 | class Fticks extends Auth\ProcessingFilter |
||
53 | { |
||
54 | /** @var string F-ticks version number */ |
||
55 | private static string $fticksVersion = '1.0'; |
||
56 | |||
57 | /** @var string F-ticks federation identifier */ |
||
58 | private string $federation; |
||
59 | |||
60 | /** @var string A salt to apply when digesting usernames (defaults to config file salt) */ |
||
61 | private string $salt; |
||
62 | |||
63 | /** @var string The logging backend */ |
||
64 | private string $logdest = 'simplesamlphp'; |
||
65 | |||
66 | /** @var array Backend specific logging config */ |
||
67 | private array $logconfig = []; |
||
68 | |||
69 | /** @var string The username attribute to use */ |
||
70 | private string $identifyingAttribute = 'eduPersonPrincipalName'; |
||
71 | |||
72 | /** @var string|null The realm attribute to use */ |
||
73 | private ?string $realm = null; |
||
74 | |||
75 | /** @var string The hashing algorithm to use */ |
||
76 | private string $algorithm = 'sha256'; |
||
77 | |||
78 | /** @var array F-ticks attributes to exclude */ |
||
79 | private array $exclude = []; |
||
80 | |||
81 | /** @var bool Enable legacy handing of PN (for backwards compatibility) */ |
||
82 | private string $pnHashIsTargeted = 'none'; |
||
83 | |||
84 | |||
85 | /** |
||
86 | * Log a message to the desired destination |
||
87 | * |
||
88 | * @param string $msg message to log |
||
89 | */ |
||
90 | private function log(string $msg): void |
||
91 | { |
||
92 | switch ($this->logdest) { |
||
93 | /* local syslog call, avoiding SimpleSAMLphp's wrapping */ |
||
94 | case 'local': |
||
95 | case 'syslog': |
||
96 | Assert::keyExists($this->logconfig, 'processname'); |
||
97 | Assert::keyExists($this->logconfig, 'facility'); |
||
98 | |||
99 | openlog($this->logconfig['processname'], LOG_PID, $this->logconfig['facility']); |
||
100 | syslog(array_key_exists('priority', $this->logconfig) ? $this->logconfig['priority'] : LOG_INFO, $msg); |
||
101 | break; |
||
102 | |||
103 | /* remote syslog call via UDP */ |
||
104 | case 'remote': |
||
105 | Assert::keyExists($this->logconfig, 'processname'); |
||
106 | Assert::keyExists($this->logconfig, 'facility'); |
||
107 | |||
108 | /* assemble a syslog message per RFC 5424 */ |
||
109 | $rfc5424_message = sprintf( |
||
110 | '<%d>', |
||
111 | ((($this->logconfig['facility'] & 0x03f8) >> 3) * 8) + |
||
112 | (array_key_exists('priority', $this->logconfig) ? $this->logconfig['priority'] : LOG_INFO), |
||
113 | ); // pri |
||
114 | $rfc5424_message .= '1 '; // ver |
||
115 | $rfc5424_message .= gmdate('Y-m-d\TH:i:s.v\Z '); // timestamp |
||
116 | $rfc5424_message .= gethostname() . ' '; // hostname |
||
117 | $rfc5424_message .= $this->logconfig['processname'] . ' '; // app-name |
||
118 | $rfc5424_message .= posix_getpid() . ' '; // procid |
||
119 | $rfc5424_message .= '- '; // msgid |
||
120 | $rfc5424_message .= '- '; // structured-data |
||
121 | $rfc5424_message .= $msg; |
||
122 | /* send it to the remote host */ |
||
123 | $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); |
||
124 | socket_sendto( |
||
125 | $sock, |
||
126 | $rfc5424_message, |
||
127 | strlen($rfc5424_message), |
||
128 | 0, |
||
129 | gethostbyname(array_key_exists('host', $this->logconfig) ? $this->logconfig['host'] : 'localhost'), |
||
130 | array_key_exists('port', $this->logconfig) ? $this->logconfig['port'] : 514, |
||
131 | ); |
||
132 | break; |
||
133 | |||
134 | case 'errorlog': |
||
135 | error_log($msg); |
||
136 | break; |
||
137 | |||
138 | /* mostly for unit testing */ |
||
139 | case 'stdout': |
||
140 | echo $msg . "\n"; |
||
141 | break; |
||
142 | |||
143 | /* SimpleSAMLphp's builtin logging */ |
||
144 | case 'simplesamlphp': |
||
145 | default: |
||
146 | Logger::stats($msg); |
||
147 | break; |
||
148 | } |
||
149 | } |
||
150 | |||
151 | |||
152 | /** |
||
153 | * Generate a PN hash |
||
154 | * |
||
155 | * @param array $state |
||
156 | * @return string|null $hash |
||
157 | */ |
||
158 | private function generatePNhash(array &$state): ?string |
||
159 | { |
||
160 | /* get a user id */ |
||
161 | Assert::keyExists($state, 'Attributes'); |
||
162 | |||
163 | $uid = null; |
||
164 | if (array_key_exists($this->identifyingAttribute, $state['Attributes'])) { |
||
165 | if (is_array($state['Attributes'][$this->identifyingAttribute])) { |
||
166 | $uid = reset($state['Attributes'][$this->identifyingAttribute]); |
||
167 | } else { |
||
168 | $uid = $state['Attributes'][$this->identifyingAttribute]; |
||
169 | } |
||
170 | } |
||
171 | |||
172 | /* calculate a hash */ |
||
173 | if ($uid !== null) { |
||
174 | $userdata = $this->federation; |
||
175 | if (in_array($this->pnHashIsTargeted, ['source', 'both'])) { |
||
176 | if (array_key_exists('saml:sp:IdP', $state)) { |
||
177 | $userdata .= strlen($state['saml:sp:IdP']) . ':' . $state['saml:sp:IdP']; |
||
178 | } else { |
||
179 | $userdata .= strlen($state['Source']['entityid']) . ':' . $state['Source']['entityid']; |
||
180 | } |
||
181 | } |
||
182 | if (in_array($this->pnHashIsTargeted, ['destination', 'both'])) { |
||
183 | $userdata .= strlen($state['Destination']['entityid']) . ':' . $state['Destination']['entityid']; |
||
184 | } |
||
185 | $userdata .= strlen($uid) . ':' . $uid; |
||
186 | $userdata .= $this->salt; |
||
187 | |||
188 | return hash($this->algorithm, $userdata); |
||
189 | } |
||
190 | |||
191 | return null; |
||
192 | } |
||
193 | |||
194 | |||
195 | /** |
||
196 | * Escape F-ticks values |
||
197 | * |
||
198 | * value = 1*( ALPHA / DIGIT / '_' / '-' / ':' / '.' / ',' / ';') |
||
199 | * ... but add a / for entityIDs |
||
200 | * |
||
201 | * @param string $value |
||
202 | * @return string $value |
||
203 | */ |
||
204 | private function escapeFticks(string $value): string |
||
205 | { |
||
206 | return preg_replace('/[^A-Za-z0-9_\-:.,;\/]+/', '', $value); |
||
207 | } |
||
208 | |||
209 | |||
210 | /** |
||
211 | * Initialize this filter, parse configuration. |
||
212 | * |
||
213 | * @param array $config Configuration information about this filter. |
||
214 | * @param mixed $reserved For future use. |
||
215 | * @throws \SimpleSAML\Error\Exception |
||
216 | */ |
||
217 | public function __construct(array $config, $reserved) |
||
218 | { |
||
219 | parent::__construct($config, $reserved); |
||
220 | |||
221 | Assert::keyExists($config, 'federation', 'Federation identifier must be set', Error\Exception::class); |
||
222 | Assert::string($config['federation'], 'Federation identifier must be a string', Error\Exception::class); |
||
223 | $this->federation = $config['federation']; |
||
224 | |||
225 | if (array_key_exists('salt', $config)) { |
||
226 | Assert::string($config['salt'], 'Salt must be a string', Error\Exception::class); |
||
227 | $this->salt = $config['salt']; |
||
228 | } else { |
||
229 | $configUtils = new Utils\Config(); |
||
230 | $this->salt = $configUtils->getSecretSalt(); |
||
231 | } |
||
232 | |||
233 | if (array_key_exists('identifyingAttribute', $config)) { |
||
234 | Assert::string( |
||
235 | $config['identifyingAttribute'], |
||
236 | 'identifyingAttribute must be a string', |
||
237 | Error\Exception::class, |
||
238 | ); |
||
239 | $this->identifyingAttribute = $config['identifyingAttribute']; |
||
240 | } |
||
241 | |||
242 | if (array_key_exists('realm', $config)) { |
||
243 | Assert::string($config['realm'], 'Realm must be a string', Error\Exception::class); |
||
244 | $this->realm = $config['realm']; |
||
245 | } |
||
246 | |||
247 | if (array_key_exists('algorithm', $config)) { |
||
248 | if ( |
||
249 | is_string($config['algorithm']) |
||
250 | && in_array($config['algorithm'], hash_algos()) |
||
251 | ) { |
||
252 | $this->algorithm = $config['algorithm']; |
||
253 | } else { |
||
254 | throw new Error\Exception('algorithm must be a hash algorithm listed in hash_algos()'); |
||
255 | } |
||
256 | } |
||
257 | |||
258 | if (array_key_exists('exclude', $config)) { |
||
259 | if (is_array($config['exclude'])) { |
||
260 | $this->exclude = $config['exclude']; |
||
261 | } elseif (is_string($config['exclude'])) { |
||
262 | $this->exclude = [$config['exclude']]; |
||
263 | } else { |
||
264 | throw new Error\Exception('F-ticks exclude must be an array'); |
||
265 | } |
||
266 | } |
||
267 | |||
268 | if (array_key_exists('pnHashIsTargeted', $config)) { |
||
269 | if ( |
||
270 | is_string($config['pnHashIsTargeted']) && |
||
271 | in_array($config['pnHashIsTargeted'], ['source', 'destination', 'both', 'none']) |
||
272 | ) { |
||
273 | $this->pnHashIsTargeted = $config['pnHashIsTargeted']; |
||
274 | } else { |
||
275 | throw new Error\Exception( |
||
276 | 'F-ticks log pnHashIsTargeted must be one of [source, destnation, both, none]', |
||
277 | ); |
||
278 | } |
||
279 | } |
||
280 | |||
281 | if (array_key_exists('logdest', $config)) { |
||
282 | if ( |
||
283 | is_string($config['logdest']) && |
||
284 | in_array($config['logdest'], ['local', 'syslog', 'remote', 'stdout', 'errorlog', 'simplesamlphp']) |
||
285 | ) { |
||
286 | $this->logdest = $config['logdest']; |
||
287 | } else { |
||
288 | throw new Error\Exception( |
||
289 | 'F-ticks log destination must be one of [local, remote, stdout, errorlog, simplesamlphp]', |
||
290 | ); |
||
291 | } |
||
292 | } |
||
293 | |||
294 | /* match SSP config or we risk mucking up the openlog call */ |
||
295 | $globalConfig = Configuration::getInstance(); |
||
296 | $defaultFacility = $globalConfig->getOptionalInteger( |
||
297 | 'logging.facility', |
||
298 | defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, |
||
299 | ); |
||
300 | $defaultProcessName = $globalConfig->getOptionalString('logging.processname', 'SimpleSAMLphp'); |
||
301 | if (array_key_exists('logconfig', $config)) { |
||
302 | if (is_array($config['logconfig'])) { |
||
303 | $this->logconfig = $config['logconfig']; |
||
304 | } else { |
||
305 | throw new Error\Exception('F-ticks logconfig must be an array'); |
||
306 | } |
||
307 | } |
||
308 | if (!array_key_exists('facility', $this->logconfig)) { |
||
309 | $this->logconfig['facility'] = $defaultFacility; |
||
310 | } |
||
311 | if (!array_key_exists('processname', $this->logconfig)) { |
||
312 | $this->logconfig['processname'] = $defaultProcessName; |
||
313 | } |
||
314 | |||
315 | /* warn if we risk mucking up the openlog call (doesn't matter for remote syslog) */ |
||
316 | if (in_array($this->logdest, ['local', 'syslog'])) { |
||
317 | $this->warnRiskyLogSettings($defaultFacility, $defaultProcessName); |
||
318 | } |
||
319 | } |
||
320 | |||
321 | |||
322 | /** |
||
323 | * Warn about risky logger settings |
||
324 | * |
||
325 | * @param int $defaultFacility |
||
326 | * @param string $defaultProcessName |
||
327 | * @return void |
||
328 | */ |
||
329 | private function warnRiskyLogSettings(int $defaultFacility, string $defaultProcessName): void |
||
330 | { |
||
331 | if ( |
||
332 | array_key_exists('facility', $this->logconfig) |
||
333 | && ($this->logconfig['facility'] !== $defaultFacility) |
||
334 | ) { |
||
335 | Logger::warning( |
||
336 | 'F-ticks syslog facility differs from global config which may cause' |
||
337 | . ' SimpleSAMLphp\'s logging to behave inconsistently', |
||
338 | ); |
||
339 | } |
||
340 | if ( |
||
341 | array_key_exists('processname', $this->logconfig) |
||
342 | && ($this->logconfig['processname'] !== $defaultProcessName) |
||
343 | ) { |
||
344 | Logger::warning( |
||
345 | 'F-ticks syslog processname differs from global config which may cause' |
||
346 | . ' SimpleSAMLphp\'s logging to behave inconsistently', |
||
347 | ); |
||
348 | } |
||
349 | } |
||
350 | |||
351 | |||
352 | /** |
||
353 | * Process this filter |
||
354 | * |
||
355 | * @param mixed &$state |
||
356 | */ |
||
357 | public function process(array &$state): void |
||
358 | { |
||
359 | Assert::keyExists($state, 'Destination'); |
||
360 | Assert::keyExists($state['Destination'], 'entityid'); |
||
361 | Assert::keyExists($state, 'Source'); |
||
362 | Assert::keyExists($state['Source'], 'entityid'); |
||
363 | |||
364 | $fticks = []; |
||
365 | |||
366 | /* AFAIK the AuthProc will only execute if there is prior success */ |
||
367 | $fticks['RESULT'] = 'OK'; |
||
368 | |||
369 | /* SAML IdP entity Id */ |
||
370 | if (array_key_exists('saml:sp:IdP', $state)) { |
||
371 | $fticks['AP'] = $state['saml:sp:IdP']; |
||
372 | } else { |
||
373 | $fticks['AP'] = $state['Source']['entityid']; |
||
374 | } |
||
375 | |||
376 | /* SAML SP entity Id */ |
||
377 | $fticks['RP'] = $state['Destination']['entityid']; |
||
378 | |||
379 | /* SAML session id */ |
||
380 | $session = Session::getSessionFromRequest(); |
||
381 | $fticks['CSI'] = $session->getTrackID(); |
||
382 | |||
383 | /* Authentication method identifier */ |
||
384 | if ( |
||
385 | array_key_exists('saml:sp:State', $state) |
||
386 | && array_key_exists('saml:sp:AuthnContext', $state['saml:sp:State']) |
||
387 | ) { |
||
388 | $fticks['AM'] = $state['saml:sp:State']['saml:sp:AuthnContext']; |
||
389 | } elseif ( |
||
390 | array_key_exists('SimpleSAML_Auth_State.stage', $state) |
||
391 | && preg_match('/UserPass/', $state['SimpleSAML_Auth_State.stage']) |
||
392 | ) { |
||
393 | /* hack to try identify LDAP et al as Password */ |
||
394 | $fticks['AM'] = Constants::AC_PASSWORD; |
||
395 | } |
||
396 | |||
397 | /* ePTID */ |
||
398 | $pn = $this->generatePNhash($state); |
||
399 | if ($pn !== null) { |
||
400 | $fticks['PN'] = $pn; |
||
401 | } |
||
402 | |||
403 | /* timestamp */ |
||
404 | if ( |
||
405 | array_key_exists('saml:sp:State', $state) |
||
406 | && array_key_exists('saml:AuthnInstant', $state['saml:sp:State']) |
||
407 | ) { |
||
408 | $fticks['TS'] = $state['saml:sp:State']['saml:AuthnInstant']; |
||
409 | } else { |
||
410 | $fticks['TS'] = time(); |
||
411 | } |
||
412 | |||
413 | /* realm */ |
||
414 | if ($this->realm !== null) { |
||
415 | Assert::keyExists($state, 'Attributes'); |
||
416 | if (array_key_exists($this->realm, $state['Attributes'])) { |
||
417 | if (is_array($state['Attributes'][$this->realm])) { |
||
418 | $fticks['REALM'] = $state['Attributes'][$this->realm][0]; |
||
419 | } else { |
||
420 | $fticks['REALM'] = $state['Attributes'][$this->realm]; |
||
421 | } |
||
422 | } |
||
423 | } |
||
424 | |||
425 | /* allow some attributes to be excluded */ |
||
426 | if (!empty($this->exclude)) { |
||
427 | $fticks = array_filter($fticks, [$this, 'filterExcludedAttributes'], ARRAY_FILTER_USE_KEY); |
||
428 | } |
||
429 | |||
430 | /* assemble an F-ticks log string */ |
||
431 | $this->log($this->assembleFticksLogString($fticks)); |
||
432 | } |
||
433 | |||
434 | |||
435 | /** |
||
436 | * Callback method to filter excluded attributes |
||
437 | * |
||
438 | * @param string $attr |
||
439 | * @return bool |
||
440 | */ |
||
441 | private function filterExcludedAttributes(string $attr): bool |
||
442 | { |
||
443 | return !in_array($attr, $this->exclude); |
||
444 | } |
||
445 | |||
446 | |||
447 | /** |
||
448 | * Assemble fticks log string |
||
449 | * |
||
450 | * @param array $fticks |
||
451 | * @return string |
||
452 | */ |
||
453 | private function assembleFticksLogString(array $fticks): string |
||
472 | } |
||
473 | } |
||
474 |