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