remove redundant gift membership check
[fsfdrupalauth.git] / lib / Auth / Source / FSFDrupalAuth.php
CommitLineData
395539d7
AE
1<?php
2
3namespace SimpleSAML\Module\fsfdrupalauth\Auth\Source;
4
cf44092c
AE
5use Exception;
6use PDO;
7use PDOException;
8use SimpleSAML\Error;
9use SimpleSAML\Logger;
10
395539d7
AE
11/**
12 * Extension of simple SQL authentication source
13 *
14 * @package SimpleSAMLphp
15 */
16
17class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
18{
19 /**
20 * The DSN we should connect to.
21 */
22 private $dsn;
23
24 /**
25 * The username we should connect to the database with.
26 */
27 private $username;
28
29 /**
30 * The password we should connect to the database with.
31 */
32 private $password;
33
34 /**
35 * The options that we should connect to the database with.
36 */
37 private $options;
38
39 /**
40 * The query we should use to retrieve the attributes for the user.
41 *
42 * The username and password will be available as :username and :password.
43 */
44 private $query_main;
45 private $query_membership;
46 private $query_staff;
794d92ca 47
78e03f98 48 private $query_nomination_process_donations;
29854532 49 private $query_nomination_process_gift_receipt;
3fa64def 50 private $query_nomination_process_adhoc;
395539d7 51
1ae1ff15 52 private $query_discussion_process_old_membership;
794d92ca 53 private $query_discussion_process_donations;
794d92ca
AE
54 private $query_discussion_process_adhoc;
55
c0d116a9 56 /**
e12acfe1
AE
57 * SQL query parameters, or variables that help determine which attributes
58 * someone has
c0d116a9 59 */
e12acfe1 60 private $fsf_org_id;
e9db6ecd
AE
61 private $gift_redeem_page_id;
62
2d61361e 63 private $nomination_process_active;
78e03f98
AE
64 private $nomination_process_contrib_start_date;
65 private $nomination_process_contrib_end_date;
e9db6ecd 66 private $nomination_process_adhoc_access_group_id;
f38130e1
AE
67 private $membership_monthly_rate;
68 private $student_membership_monthly_rate;
c0d116a9 69
794d92ca
AE
70 private $discussion_process_active;
71 private $discussion_process_contrib_start_date;
72 private $discussion_process_contrib_end_date;
73 private $discussion_process_adhoc_access_group_id;
74 private $discussion_process_adhoc_no_access_group_id;
75
395539d7
AE
76 /**
77 * Constructor for this authentication source.
78 *
79 * @param array $info Information about this authentication source.
80 * @param array $config Configuration.
81 */
82 public function __construct($info, $config)
83 {
84 assert(is_array($info));
85 assert(is_array($config));
86
87 // Call the parent constructor first, as required by the interface
88 parent::__construct($info, $config);
89
90 // Make sure that all required parameters are present.
e9db6ecd
AE
91 foreach (['dsn',
92 'username',
93 'password',
94
95 'query_main',
96 'query_membership',
97 'query_staff',
98
e12acfe1 99 'query_nomination_process_donations',
3fa64def 100 'query_nomination_process_gift_receipt',
e9db6ecd
AE
101 'query_nomination_process_adhoc',
102
103 'fsf_org_id',
104 'gift_redeem_page_id',
105
2d61361e 106 'nomination_process_active',
e12acfe1 107 'nomination_process_contrib_start_date',
e9db6ecd
AE
108 'nomination_process_contrib_end_date',
109 'nomination_process_adhoc_access_group_id',
110 'membership_monthly_rate',
794d92ca
AE
111 'student_membership_monthly_rate',
112
1ae1ff15 113 'query_discussion_process_old_membership',
794d92ca 114 'query_discussion_process_donations',
794d92ca
AE
115 'query_discussion_process_adhoc',
116
117 'discussion_process_active',
118 'discussion_process_contrib_start_date',
119 'discussion_process_contrib_end_date',
120 'discussion_process_adhoc_access_group_id',
121 'discussion_process_adhoc_no_access_group_id',]
3fa64def 122 as $param) {
e12acfe1 123
395539d7 124 if (!array_key_exists($param, $config)) {
cf44092c 125 throw new Exception('Missing required attribute \''.$param.
395539d7
AE
126 '\' for authentication source '.$this->authId);
127 }
128
129 if (!is_string($config[$param])) {
cf44092c 130 throw new Exception('Expected parameter \''.$param.
395539d7
AE
131 '\' for authentication source '.$this->authId.
132 ' to be a string. Instead it was: '.
133 var_export($config[$param], true));
134 }
e12acfe1
AE
135
136 $this->$param = $config[$param];
395539d7
AE
137 }
138
395539d7
AE
139 if (isset($config['options'])) {
140 $this->options = $config['options'];
141 }
142 }
143
144
145 /**
146 * Create a database connection.
147 *
f58b2b6b 148 * @return PDO The database connection.
395539d7
AE
149 */
150 private function connect()
151 {
152 try {
cf44092c
AE
153 $db = new PDO($this->dsn, $this->username, $this->password, $this->options);
154 } catch (PDOException $e) {
155 // Obfuscate the password if it's part of the dsn
156 $obfuscated_dsn = preg_replace('/(user|password)=(.*?([;]|$))/', '${1}=***', $this->dsn);
157
158 throw new Exception('fsfdrupalauth:' . $this->authId . ': - Failed to connect to \'' .
159 $obfuscated_dsn . '\': ' . $e->getMessage());
395539d7
AE
160 }
161
cf44092c 162 $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
395539d7
AE
163
164 $driver = explode(':', $this->dsn, 2);
165 $driver = strtolower($driver[0]);
166
167 // Driver specific initialization
168 switch ($driver) {
169 case 'mysql':
170 // Use UTF-8
171 $db->exec("SET NAMES 'utf8mb4'");
172 break;
173 case 'pgsql':
174 // Use UTF-8
175 $db->exec("SET NAMES 'UTF8'");
176 break;
177 }
178
179 return $db;
180 }
181
182 /*
183 * Check the password against a Drupal hash
184 *
185 */
186 private function check_password($password, $hash) {
187
188 //
189 // The reason for running a separate process is so that the PHP global
190 // env doesn't get clobbered by include / require.
191 //
192
193 // pipes code based off of https://www.php.net/manual/en/function.proc-open.php
194 // CC-BY 3.0 or later
195 $descriptorspec = array(
59f90414
AE
196 0 => array("pipe", "r"), // stdin is a pipe that the child may read from
197 1 => array("pipe", "w"), // stdout is a pipe that the child may write to
198 2 => array("pipe", "w") // stderr is a pipe that the child may write to
395539d7
AE
199 );
200
201 $cwd = "../modules/fsfdrupalauth/extlib";
202 //$env = array('some_option' => 'aeiou');
203 $env = array();
204
205 $process = proc_open('php drupal-pw-check.php', $descriptorspec, $pipes, $cwd, $env);
206
207 if (is_resource($process)) {
208 // $pipes now looks like this:
209 // 0 => writeable handle connected to child stdin
210 // 1 => readable handle connected to child stdout
211
212 fwrite($pipes[0], json_encode([$password, $hash]));
213 fclose($pipes[0]);
214
215 $result = stream_get_contents($pipes[1]);
216 fclose($pipes[1]);
217
218 $errors = stream_get_contents($pipes[2]);
219 fclose($pipes[2]);
220
221 // It is important that you close any pipes before calling
222 // proc_close in order to avoid a deadlock
223 $return_value = proc_close($process);
224
cf44092c 225 //Logger::debug('fsfdrupalauth:'.$this->authId.': authenticator stdout: '.$result);
3fa64def 226
e57e2393 227 $errors_found_yet = false;
395539d7 228 if ($errors != "") {
cf44092c 229 Logger::error('fsfdrupalauth:'.$this->authId.': authenticator stderr: '.$errors);
e57e2393 230 $errors_found_yet = true;
395539d7 231 }
3fa64def 232
395539d7 233 if ($return_value != 0) {
cf44092c 234 Logger::error('fsfdrupalauth:'.$this->authId.': authenticator non-zero return code: '.$return_value);
e57e2393 235 $errors_found_yet = true;
395539d7 236 }
3fa64def 237
6921b9d4 238 return (!$errors_found_yet && is_string($result) && rtrim($result) == "true");
395539d7
AE
239
240 } else {
241
cf44092c 242 Logger::error('fsfdrupalauth:'.$this->authId.': unable to launch authenticator');
395539d7
AE
243
244 return false;
245 }
246 }
247
248 /**
249 *
250 * query the database with arbitrary queries that only require a user name.
251 *
252 */
2e644466 253 private function query_db($queryname, $query_params)
395539d7
AE
254 {
255 assert(is_string($queryname));
256 assert(is_string($username));
257
258 $db = $this->connect();
259
260 try {
261 $sth = $db->prepare($this->$queryname);
cf44092c
AE
262 } catch (PDOException $e) {
263 throw new Exception('fsfdrupalauth:'.$this->authId.
395539d7
AE
264 ': - Failed to prepare queryname: '.$queryname.': '.$e->getMessage());
265 }
266
267 try {
2e644466 268 $sth->execute($query_params);
cf44092c
AE
269 } catch (PDOException $e) {
270 throw new Exception('fsfdrupalauth:'.$this->authId.
395539d7
AE
271 ': - Failed to execute queryname: '.$queryname.': '.$e->getMessage());
272 }
273
274 try {
f58b2b6b 275 $data = $sth->fetchAll(PDO::FETCH_ASSOC);
cf44092c
AE
276 } catch (PDOException $e) {
277 throw new Exception('fsfdrupalauth:'.$this->authId.
395539d7
AE
278 ': - Failed to fetch result set: '.$e->getMessage());
279 }
280
cf44092c 281 Logger::info('fsfdrupalauth:'.$this->authId.': Got '.count($data).
395539d7
AE
282 ' rows from database');
283
284 return $data;
285 }
286
287 /**
288 * add more CAS attributes to user, such as is_staff and is_member
289 */
290 private function add_more_attributes(&$attributes, $username) {
291
f33432a6
AE
292 //
293 // query on membership
294 //
395539d7 295
2e644466 296 $membership_data = $this->query_db('query_membership', ['username' => $username]);
395539d7
AE
297
298 if (count($membership_data) === 0) {
299 // No rows returned - invalid username
cf44092c 300 Logger::debug('fsfdrupalauth:'.$this->authId.
395539d7
AE
301 ': No rows in result set. Probably no membership.');
302 }
303
f33432a6 304 $attributes['is_member'] = ['false'];
395539d7
AE
305 $attributes['was_member'] = ['false'];
306
307 foreach ($membership_data as $row) {
308 foreach ($row as $key => $value) {
309 if ($value === null) {
310 continue;
311 }
312 $value = (string) $value;
313
f33432a6 314 if ($value === '1' || $value === '2' || $value === '3') {
395539d7
AE
315 $attributes['is_member'] = ['true'];
316 $attributes['was_member'] = ['true'];
f33432a6 317 } elseif ($value === '4') {
395539d7 318 $attributes['was_member'] = ['true'];
f33432a6 319 }
395539d7
AE
320 }
321 }
322
c0d116a9 323 //
0782e0aa 324 // helper functions for access to board nomination / discussion process
c0d116a9
AE
325 //
326
29854532
AE
327 /**
328 * @param string $query_name Name of query in authsources
f38130e1 329 * @param array $extra_params Associative array of parameters to include in query
29854532 330 */
f38130e1
AE
331 $donation_query = function ($query_name, $extra_params)
332 use ($username) {
29854532 333
3fa64def 334 $parameters = ['username' => $username];
29854532 335
3fa64def
AE
336 foreach ($extra_params as $key => $value) {
337 $parameters[$key] = $value;
29854532 338 }
78e03f98 339
f38130e1
AE
340 return $this->query_db($query_name, $parameters);
341 };
78e03f98 342
1ae1ff15
AE
343 $old_membership_query = $donation_query;
344
f38130e1 345 $compare_res = function ($result, $amount) {
29854532 346 foreach ($result[0] as $key => $value) {
f38130e1
AE
347 if (intval($value) >= $amount) {
348 return true;
349 }
350 }
351 return false;
352 };
353
32c7abac
AE
354 // set dates here, used by helper functions below
355 $nomination_process_start_date = $this->nomination_process_contrib_start_date;
356 $nomination_process_end_date = $this->nomination_process_contrib_end_date;
357 $discussion_process_start_date = $this->discussion_process_contrib_start_date;
358 $discussion_process_end_date = $this->discussion_process_contrib_end_date;
359
360
f38130e1
AE
361 // looks for memberships / comparable donations in time window. also
362 // looks for a membership or donation (included as a param) that
363 // occurred up to a year before, and that would have carried over into
364 // the time window with a single donation. this approximates whether
365 // the person was, or would have been, a member during the configured
366 // time window.
0782e0aa 367 $nomination_process_analyze_history = function ($selective_donations_history)
32c7abac 368 use ($nomination_process_start_date, $nomination_process_end_date) {
f38130e1
AE
369
370 $eligible = false;
371
32c7abac
AE
372 $start_date_obj = new \DateTime($nomination_process_start_date);
373 $end_date_obj = new \DateTime($nomination_process_end_date);
f38130e1
AE
374
375 foreach ($selective_donations_history as $row) {
376
377 $amount = intval($row['amount']);
378 $member_type_id = $row['member_type_id'];
379 $receive_date_obj = new \DateTime($row['receive_date']);
380
381 if ($amount < 5) {
382 continue;
383
384 } elseif ($receive_date_obj >= $start_date_obj and $receive_date_obj <= $end_date_obj) {
385 return true;
386
387 } elseif ($receive_date_obj < $start_date_obj) {
388 switch ($member_type_id) {
389 case '1':
390 case '2':
391 $rate = intval($this->student_membership_monthly_rate);
392 break;
393 case '8':
394 case '9':
395 case null:
396 default:
397 $rate = intval($this->membership_monthly_rate);
398 break;
399 }
400 $membership_end_date_obj = new \DateTime($row['receive_date']);
401 $membership_end_date_obj->add(new \DateInterval("P" . ceil($amount / $rate) . "M"));
402
403 if ($membership_end_date_obj >= $start_date_obj) {
404 return true;
405 }
406 }
78e03f98 407 }
78e03f98
AE
408 return false;
409 };
410
794d92ca 411 $discussion_process_analyze_history = function ($selective_donations_history)
32c7abac 412 use ($discussion_process_start_date, $discussion_process_end_date) {
794d92ca
AE
413
414 $eligible = false;
415 $total = 0;
416
32c7abac
AE
417 $start_date_obj = new \DateTime($discussion_process_start_date);
418 $end_date_obj = new \DateTime($discussion_process_end_date);
794d92ca
AE
419
420 foreach ($selective_donations_history as $row) {
421
422 $amount = intval($row['amount']);
423 $member_type_id = $row['member_type_id'];
424 $receive_date_obj = new \DateTime($row['receive_date']);
425
32c7abac 426 if (($receive_date_obj > $start_date_obj) && ($receive_date_obj < $end_date_obj)) {
794d92ca
AE
427 $total += $amount;
428 }
429 }
430
431 if ($total >= 120) {
432 return true;
433 } else {
434 return false;
435 }
436 };
437
0782e0aa
AE
438 //
439 // nomination form participation specific checks
440 //
441
32c7abac
AE
442 $donation_params = ['start_date' => $nomination_process_start_date, 'end_date' => $nomination_process_end_date];
443 $gift_member_params = ['start_date' => $nomination_process_start_date, 'end_date' => $nomination_process_end_date, 'gift_redeem_page_id' => intval($this->gift_redeem_page_id)];
e9db6ecd 444 $adhoc_params = ['adhoc_access_group_id' => intval($this->nomination_process_adhoc_access_group_id)];
3fa64def 445
16858322
AE
446 if ($this->nomination_process_active != 'true' ) {
447 $attributes['nomination_process'] = ['false'];
0782e0aa 448
16858322
AE
449 } elseif ($compare_res($donation_query('query_nomination_process_adhoc', $adhoc_params), 1)) {
450 $attributes['nomination_process'] = ['true'];
451
452 } elseif ($attributes['is_member'] != ['true']) {
453 Logger::debug('fsfdrupalauth:'.$this->authId.
454 ': Not a member / comparable donor during window for board process.');
455 $attributes['nomination_process'] = ['false'];
456
457 } elseif ($nomination_process_analyze_history($donation_query('query_nomination_process_donations', $donation_params))
458 || $compare_res($donation_query('query_nomination_process_gift_receipt', $gift_member_params), 1)) {
459
460 $attributes['nomination_process'] = ['true'];
29854532 461
78e03f98 462 } else {
16858322
AE
463 Logger::debug('fsfdrupalauth:'.$this->authId.
464 ': Not a member / comparable donor during window for board process.');
78e03f98
AE
465 $attributes['nomination_process'] = ['false'];
466 }
c0d116a9 467
794d92ca
AE
468 //
469 // discussion form participation specific checks
470 //
471
32c7abac 472 $donation_params = ['start_date' => $discussion_process_start_date, 'end_date' => $discussion_process_end_date];
1ae1ff15 473 $old_member_params = $donation_params;
794d92ca
AE
474 $adhoc_params = ['adhoc_access_group_id' => intval($this->discussion_process_adhoc_access_group_id)];
475 $adhoc_params_no = ['adhoc_access_group_id' => intval($this->discussion_process_adhoc_no_access_group_id)];
476
16858322
AE
477 if ($this->discussion_process_active != 'true' ) {
478 $attributes['discussion_process'] = ['false'];
794d92ca 479
16858322
AE
480 } elseif ($compare_res($donation_query('query_discussion_process_adhoc', $adhoc_params_no), 1)) {
481 Logger::debug('fsfdrupalauth:'.$this->authId.
482 ': Nominee not eligible for board nominee discussion process.');
483 $attributes['discussion_process'] = ['false'];
794d92ca 484
16858322
AE
485 } elseif ($compare_res($donation_query('query_discussion_process_adhoc', $adhoc_params), 1)) {
486 $attributes['discussion_process'] = ['true'];
794d92ca 487
16858322
AE
488 } elseif ($attributes['is_member'] != ['true']) {
489 Logger::debug('fsfdrupalauth:'.$this->authId.
490 ': Not eligible for board nominee discussion process.');
491 $attributes['discussion_process'] = ['false'];
492
1ae1ff15 493 } elseif ($compare_res($old_membership_query('query_discussion_process_old_membership', $old_member_params), 1)
5ba93058 494 || $discussion_process_analyze_history($donation_query('query_discussion_process_donations', $donation_params))) {
16858322
AE
495
496 $attributes['discussion_process'] = ['true'];
794d92ca 497
794d92ca 498 } else {
16858322
AE
499 Logger::debug('fsfdrupalauth:'.$this->authId.
500 ': Not eligible for board nominee discussion process.');
794d92ca
AE
501 $attributes['discussion_process'] = ['false'];
502 }
503
f33432a6
AE
504 //
505 // query on staff
506 //
395539d7 507
e12acfe1 508 $staff_data = $this->query_db('query_staff', ['username' => $username, 'fsf_org_id' => $this->fsf_org_id]);
395539d7
AE
509
510 if (count($staff_data) === 0) {
511 // No rows returned - invalid username
cf44092c 512 Logger::debug('fsfdrupalauth:'.$this->authId.
395539d7
AE
513 ': No rows in result set. Probably not FSF staff.');
514 }
515
f33432a6 516 $attributes['is_fsf_staff'] = ['false'];
395539d7
AE
517
518 foreach ($staff_data as $row) {
519 foreach ($row as $key => $value) {
520
521 if ($value === null) {
522 continue;
523 }
524 $value = (string) $value;
525
bdc16e06 526 if (strtolower($value) === strtolower($username)) {
395539d7 527 // they are staff
bdc16e06 528 $attributes['is_fsf_staff'] = ['true'];
395539d7
AE
529 break;
530 }
531 }
532 }
429471b4
AE
533
534 //
535 // aggregate attribute
536 //
537
538 $groups_list = '';
539 $first = true;
540 foreach ($attributes as $key => $value) {
541 if ($value == ['true']) {
542 if (!$first) {
543 $groups_list .= ', ';
544 }
545 $groups_list .= $key;
546 $first = false;
547 }
548 }
549
550 $attributes['groups_list'] = [$groups_list];
395539d7
AE
551 }
552
553 /**
554 * Attempt to log in using the given username and password.
555 *
556 * On a successful login, this function should return the users attributes. On failure,
557 * it should throw an exception. If the error was caused by the user entering the wrong
cf44092c 558 * username or password, a Error\Error('WRONGUSERPASS') should be thrown.
395539d7
AE
559 *
560 * Note that both the username and the password are UTF-8 encoded.
561 *
562 * @param string $username The username the user wrote.
563 * @param string $password The password the user wrote.
564 * @return array Associative array with the users attributes.
565 */
566 protected function login($username, $password)
567 {
568 assert(is_string($username));
569 assert(is_string($password));
570
571 //// keep this commented when it's not in use. it prints user passwords to the log file
cf44092c 572 //Logger::debug('fsfdrupalauth:'.$this->authId.': entered password: '.$password);
395539d7
AE
573
574
2e644466 575 $user_data = $this->query_db('query_main', ['username' => $username]);
395539d7
AE
576
577
578 if (count($user_data) === 0) {
579 // No rows returned - invalid username
cf44092c 580 Logger::error('fsfdrupalauth:'.$this->authId.
395539d7 581 ': No rows in result set. Probably wrong username.');
cf44092c 582 throw new Error\Error('WRONGUSERPASS');
395539d7
AE
583 }
584
585 /* Extract attributes. We allow the resultset to consist of multiple rows. Attributes
586 * which are present in more than one row will become multivalued. null values and
587 * duplicate values will be skipped. All values will be converted to strings.
588 */
589 $attributes = [];
590
f33432a6
AE
591 // use the entered user name so we don't forcibly change it to all
592 // lower case. this is to preserve the behavior of the old cas server,
593 // and to remain compatible with our MW and Discourse sites that are
594 // case sensitive.
595 $attributes['name'][] = $username;
395539d7
AE
596
597 foreach ($user_data as $row) {
598 foreach ($row as $key => $value) {
599 if ($value === null) {
600 continue;
601 }
602
603 $value = (string) $value;
604
605 if (!array_key_exists($key, $attributes)) {
606 $attributes[$key] = [];
607 }
608
609 if (in_array($value, $attributes[$key], true)) {
610 // Value already exists in attribute
611 continue;
612 }
613
614 $attributes[$key][] = $value;
615 }
616 }
617
618 if (!$this->check_password($password, $attributes['pass'][0])) {
cf44092c 619 throw new Error\Error('WRONGUSERPASS');
395539d7
AE
620 }
621
622 unset($attributes['pass']);
623
624
625 $this->add_more_attributes($attributes, $username);
626
627
cf44092c 628 Logger::info('fsfdrupalauth:'.$this->authId.': Attributes: '.
395539d7
AE
629 implode(',', array_keys($attributes)));
630
631 return $attributes;
632 }
633}