3 namespace SimpleSAML\Module\fsfdrupalauth\Auth\Source
;
12 * Extension of simple SQL authentication source
14 * @package SimpleSAMLphp
17 class FSFDrupalAuth
extends \SimpleSAML\Module\core\Auth\UserPassBase
20 * The DSN we should connect to.
25 * The username we should connect to the database with.
30 * The password we should connect to the database with.
35 * The options that we should connect to the database with.
40 * The query we should use to retrieve the attributes for the user.
42 * The username and password will be available as :username and :password.
45 private $query_membership;
48 private $query_nomination_process_donations;
49 private $query_nomination_process_gift_receipt;
50 private $query_nomination_process_adhoc;
52 private $query_discussion_process_old_membership;
53 private $query_discussion_process_donations;
54 private $query_discussion_process_adhoc;
57 * SQL query parameters, or variables that help determine which attributes
61 private $gift_redeem_page_id;
63 private $nomination_process_active;
64 private $nomination_process_contrib_start_date;
65 private $nomination_process_contrib_end_date;
66 private $nomination_process_adhoc_access_group_id;
67 private $membership_monthly_rate;
68 private $student_membership_monthly_rate;
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 private $discussion_process_donation_amount;
78 * Constructor for this authentication source.
80 * @param array $info Information about this authentication source.
81 * @param array $config Configuration.
83 public function __construct($info, $config)
85 assert(is_array($info));
86 assert(is_array($config));
88 // Call the parent constructor first, as required by the interface
89 parent
::__construct($info, $config);
91 // Make sure that all required parameters are present.
100 'query_nomination_process_donations',
101 'query_nomination_process_gift_receipt',
102 'query_nomination_process_adhoc',
105 'gift_redeem_page_id',
107 'nomination_process_active',
108 'nomination_process_contrib_start_date',
109 'nomination_process_contrib_end_date',
110 'nomination_process_adhoc_access_group_id',
111 'membership_monthly_rate',
112 'student_membership_monthly_rate',
114 'query_discussion_process_old_membership',
115 'query_discussion_process_donations',
116 'query_discussion_process_adhoc',
118 'discussion_process_active',
119 'discussion_process_contrib_start_date',
120 'discussion_process_contrib_end_date',
121 'discussion_process_adhoc_access_group_id',
122 'discussion_process_adhoc_no_access_group_id',
123 'discussion_process_donation_amount',]
126 if (!array_key_exists($param, $config)) {
127 throw new Exception('Missing required attribute \''.$param.
128 '\' for authentication source '.$this->authId
);
131 if (!is_string($config[$param])) {
132 throw new Exception('Expected parameter \''.$param.
133 '\' for authentication source '.$this->authId
.
134 ' to be a string. Instead it was: '.
135 var_export($config[$param], true));
138 $this->$param = $config[$param];
141 if (isset($config['options'])) {
142 $this->options
= $config['options'];
148 * Create a database connection.
150 * @return PDO The database connection.
152 private function connect()
155 $db = new PDO($this->dsn
, $this->username
, $this->password
, $this->options
);
156 } catch (PDOException
$e) {
157 // Obfuscate the password if it's part of the dsn
158 $obfuscated_dsn = preg_replace('/(user|password)=(.*?([;]|$))/', '${1}=***', $this->dsn
);
160 throw new Exception('fsfdrupalauth:' . $this->authId
. ': - Failed to connect to \'' .
161 $obfuscated_dsn . '\': ' . $e->getMessage());
164 $db->setAttribute(PDO
::ATTR_ERRMODE
, PDO
::ERRMODE_EXCEPTION
);
166 $driver = explode(':', $this->dsn
, 2);
167 $driver = strtolower($driver[0]);
169 // Driver specific initialization
173 $db->exec("SET NAMES 'utf8mb4'");
177 $db->exec("SET NAMES 'UTF8'");
185 * Check the password against a Drupal hash
188 private function check_password($password, $hash) {
191 // The reason for running a separate process is so that the PHP global
192 // env doesn't get clobbered by include / require.
195 // pipes code based off of https://www.php.net/manual/en/function.proc-open.php
196 // CC-BY 3.0 or later
197 $descriptorspec = array(
198 0 => array("pipe", "r"), // stdin is a pipe that the child may read from
199 1 => array("pipe", "w"), // stdout is a pipe that the child may write to
200 2 => array("pipe", "w") // stderr is a pipe that the child may write to
203 $cwd = "../modules/fsfdrupalauth/extlib";
204 //$env = array('some_option' => 'aeiou');
207 $process = proc_open('php drupal-pw-check.php', $descriptorspec, $pipes, $cwd, $env);
209 if (is_resource($process)) {
210 // $pipes now looks like this:
211 // 0 => writeable handle connected to child stdin
212 // 1 => readable handle connected to child stdout
214 fwrite($pipes[0], json_encode([$password, $hash]));
217 $result = stream_get_contents($pipes[1]);
220 $errors = stream_get_contents($pipes[2]);
223 // It is important that you close any pipes before calling
224 // proc_close in order to avoid a deadlock
225 $return_value = proc_close($process);
227 //Logger::debug('fsfdrupalauth:'.$this->authId.': authenticator stdout: '.$result);
229 $errors_found_yet = false;
231 Logger
::error('fsfdrupalauth:'.$this->authId
.': authenticator stderr: '.$errors);
232 $errors_found_yet = true;
235 if ($return_value != 0) {
236 Logger
::error('fsfdrupalauth:'.$this->authId
.': authenticator non-zero return code: '.$return_value);
237 $errors_found_yet = true;
240 return (!$errors_found_yet && is_string($result) && rtrim($result) == "true");
244 Logger
::error('fsfdrupalauth:'.$this->authId
.': unable to launch authenticator');
252 * query the database with arbitrary queries that only require a user name.
255 private function query_db($queryname, $query_params)
257 assert(is_string($queryname));
258 assert(is_string($username));
260 $db = $this->connect();
263 $sth = $db->prepare($this->$queryname);
264 } catch (PDOException
$e) {
265 throw new Exception('fsfdrupalauth:'.$this->authId
.
266 ': - Failed to prepare queryname: '.$queryname.': '.$e->getMessage());
270 $sth->execute($query_params);
271 } catch (PDOException
$e) {
272 throw new Exception('fsfdrupalauth:'.$this->authId
.
273 ': - Failed to execute queryname: '.$queryname.': '.$e->getMessage());
277 $data = $sth->fetchAll(PDO
::FETCH_ASSOC
);
278 } catch (PDOException
$e) {
279 throw new Exception('fsfdrupalauth:'.$this->authId
.
280 ': - Failed to fetch result set: '.$e->getMessage());
283 Logger
::info('fsfdrupalauth:'.$this->authId
.': Got '.count($data).
284 ' rows from database');
290 * add more CAS attributes to user, such as is_staff and is_member
292 private function add_more_attributes(&$attributes, $username) {
298 $staff_data = $this->query_db('query_staff', ['username' => $username, 'fsf_org_id' => $this->fsf_org_id
]);
300 if (count($staff_data) === 0) {
301 // No rows returned - invalid username
302 Logger
::debug('fsfdrupalauth:'.$this->authId
.
303 ': No rows in result set. Probably not FSF staff.');
306 $attributes['is_fsf_staff'] = ['false'];
308 foreach ($staff_data as $row) {
309 foreach ($row as $key => $value) {
311 if ($value === null) {
314 $value = (string) $value;
316 if (strtolower($value) === strtolower($username)) {
318 $attributes['is_fsf_staff'] = ['true'];
325 // query on membership
328 $membership_data = $this->query_db('query_membership', ['username' => $username]);
330 if (count($membership_data) === 0) {
331 // No rows returned - invalid username
332 Logger
::debug('fsfdrupalauth:'.$this->authId
.
333 ': No rows in result set. Probably no membership.');
336 $attributes['is_member'] = ['false'];
337 $attributes['was_member'] = ['false'];
339 foreach ($membership_data as $row) {
340 foreach ($row as $key => $value) {
341 if ($value === null) {
344 $value = (string) $value;
346 if ($value === '1' ||
$value === '2' ||
$value === '3') {
347 $attributes['is_member'] = ['true'];
348 $attributes['was_member'] = ['true'];
349 } elseif ($value === '4') {
350 $attributes['was_member'] = ['true'];
356 // helper functions for access to board nomination / discussion process
360 * @param string $query_name Name of query in authsources
361 * @param array $extra_params Associative array of parameters to include in query
363 $donation_query = function ($query_name, $extra_params)
366 $parameters = ['username' => $username];
368 foreach ($extra_params as $key => $value) {
369 $parameters[$key] = $value;
372 return $this->query_db($query_name, $parameters);
375 $old_membership_query = $donation_query;
377 $compare_res = function ($result, $amount) {
378 foreach ($result[0] as $key => $value) {
379 if (intval($value) >= $amount) {
386 // set dates here, used by helper functions below
387 $nomination_process_start_date = $this->nomination_process_contrib_start_date
;
388 $nomination_process_end_date = $this->nomination_process_contrib_end_date
;
389 $discussion_process_start_date = $this->discussion_process_contrib_start_date
;
390 $discussion_process_end_date = $this->discussion_process_contrib_end_date
;
393 // looks for memberships / comparable donations in time window. also
394 // looks for a membership or donation (included as a param) that
395 // occurred up to a year before, and that would have carried over into
396 // the time window with a single donation. this approximates whether
397 // the person was, or would have been, a member during the configured
399 $nomination_process_analyze_history = function ($selective_donations_history)
400 use ($nomination_process_start_date, $nomination_process_end_date) {
404 Logger
::debug('fsfdrupalauth:'.$this->authId
.
405 ': start date: '.$nomination_process_start_date. " end date: ".$nomination_process_end_date);
407 $start_date_obj = new \
DateTime($nomination_process_start_date);
408 $end_date_obj = new \
DateTime($nomination_process_end_date);
410 foreach ($selective_donations_history as $row) {
412 $amount = intval($row['amount']);
413 $member_type_id = $row['member_type_id'];
414 $receive_date_obj = new \
DateTime($row['receive_date']);
419 } elseif ($receive_date_obj >= $start_date_obj and $receive_date_obj <= $end_date_obj) {
422 } elseif ($receive_date_obj < $start_date_obj) {
423 switch ($member_type_id) {
426 $rate = intval($this->student_membership_monthly_rate
);
432 $rate = intval($this->membership_monthly_rate
);
435 $membership_end_date_obj = new \
DateTime($row['receive_date']);
436 $membership_end_date_obj->add(new \
DateInterval("P" . ceil($amount / $rate) . "M"));
438 if ($membership_end_date_obj >= $start_date_obj) {
446 $discussion_process_analyze_history = function ($selective_donations_history)
447 use ($discussion_process_start_date, $discussion_process_end_date) {
452 Logger
::debug('fsfdrupalauth:'.$this->authId
.
453 ': start date: '.$discussion_process_start_date. " end date: ".$discussion_process_end_date);
455 $start_date_obj = new \
DateTime($discussion_process_start_date);
456 $end_date_obj = new \
DateTime($discussion_process_end_date);
458 foreach ($selective_donations_history as $row) {
460 $amount = intval($row['amount']);
461 $member_type_id = $row['member_type_id'];
462 $receive_date_obj = new \
DateTime($row['receive_date']);
464 if (($receive_date_obj > $start_date_obj) && ($receive_date_obj < $end_date_obj)) {
469 Logger
::debug('fsfdrupalauth:'.$this->authId
.
470 ': total amount: $'.$total);
472 if ($total >= $this->discussion_process_donation_amount
) {
480 // nomination form participation specific checks
483 $donation_params = ['start_date' => $nomination_process_start_date, 'end_date' => $nomination_process_end_date];
484 $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
)];
485 $adhoc_params = ['adhoc_access_group_id' => intval($this->nomination_process_adhoc_access_group_id
)];
487 if ($this->nomination_process_active
!= 'true' ) {
488 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Nomination board process checks not active');
489 $attributes['nomination_process'] = ['false'];
491 } elseif ($compare_res($donation_query('query_nomination_process_adhoc', $adhoc_params), 1)) {
492 Logger
::debug('fsfdrupalauth:'.$this->authId
.': In adhoc list of contacts for nomination board process');
493 $attributes['nomination_process'] = ['true'];
495 } elseif ($attributes['is_member'] != ['true']) {
496 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Not a current member for nomination board process');
497 $attributes['nomination_process'] = ['false'];
499 } elseif ($nomination_process_analyze_history($donation_query('query_nomination_process_donations', $donation_params))
500 ||
$compare_res($donation_query('query_nomination_process_gift_receipt', $gift_member_params), 1)) {
502 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Past membership / donations meet threshold for nomination board process');
503 $attributes['nomination_process'] = ['true'];
506 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Past membership / donations do not meet threshold for nomination board process');
507 $attributes['nomination_process'] = ['false'];
511 // discussion form participation specific checks
514 $donation_params = ['start_date' => $discussion_process_start_date, 'end_date' => $discussion_process_end_date];
515 $old_member_params = $donation_params;
516 $adhoc_params = ['adhoc_access_group_id' => intval($this->discussion_process_adhoc_access_group_id
)];
517 $adhoc_params_no = ['adhoc_access_group_id' => intval($this->discussion_process_adhoc_no_access_group_id
)];
519 if ($this->discussion_process_active
!= 'true' ) {
520 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Discussion board process checks not active');
521 $attributes['discussion_process'] = ['false'];
523 } elseif ($compare_res($donation_query('query_discussion_process_adhoc', $adhoc_params_no), 1)) {
524 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Nominee not allowed to participate in board discussion process.');
525 $attributes['discussion_process'] = ['false'];
527 } elseif ($compare_res($donation_query('query_discussion_process_adhoc', $adhoc_params), 1)) {
528 Logger
::debug('fsfdrupalauth:'.$this->authId
.': In adhoc list of contacts for discussion board process');
529 $attributes['discussion_process'] = ['true'];
531 } elseif ($attributes['is_member'] != ['true']) {
532 Logger
::debug('fsfdrupalauth :'.$this->authId
.': Not a member, so not eligible for board nominee discussion process.');
533 $attributes['discussion_process'] = ['false'];
535 } elseif ($compare_res($old_membership_query('query_discussion_process_old_membership', $old_member_params), 1)
536 ||
$discussion_process_analyze_history($donation_query('query_discussion_process_donations', $donation_params))) {
538 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Past membership / donations meet threshold for discussion board process');
539 $attributes['discussion_process'] = ['true'];
542 Logger
::debug('fsfdrupalauth:'.$this->authId
.': Past membership / donations do not meet threshold for discussion board process');
543 $attributes['discussion_process'] = ['false'];
547 // aggregate attribute
552 foreach ($attributes as $key => $value) {
553 if ($value == ['true']) {
555 $groups_list .= ', ';
557 $groups_list .= $key;
562 $attributes['groups_list'] = [$groups_list];
566 * Attempt to log in using the given username and password.
568 * On a successful login, this function should return the users attributes. On failure,
569 * it should throw an exception. If the error was caused by the user entering the wrong
570 * username or password, a Error\Error('WRONGUSERPASS') should be thrown.
572 * Note that both the username and the password are UTF-8 encoded.
574 * @param string $username The username the user wrote.
575 * @param string $password The password the user wrote.
576 * @return array Associative array with the users attributes.
578 protected function login($username, $password)
580 assert(is_string($username));
581 assert(is_string($password));
583 //// keep this commented when it's not in use. it prints user passwords to the log file
584 //Logger::debug('fsfdrupalauth:'.$this->authId.': entered password: '.$password);
587 $user_data = $this->query_db('query_main', ['username' => $username]);
590 if (count($user_data) === 0) {
591 // No rows returned - invalid username
592 Logger
::error('fsfdrupalauth:'.$this->authId
.
593 ': No rows in result set. Probably wrong username.');
594 throw new Error\
Error('WRONGUSERPASS');
597 /* Extract attributes. We allow the resultset to consist of multiple rows. Attributes
598 * which are present in more than one row will become multivalued. null values and
599 * duplicate values will be skipped. All values will be converted to strings.
603 // use the entered user name so we don't forcibly change it to all
604 // lower case. this is to preserve the behavior of the old cas server,
605 // and to remain compatible with our MW and Discourse sites that are
607 $attributes['name'][] = $username;
609 foreach ($user_data as $row) {
610 foreach ($row as $key => $value) {
611 if ($value === null) {
615 $value = (string) $value;
617 if (!array_key_exists($key, $attributes)) {
618 $attributes[$key] = [];
621 if (in_array($value, $attributes[$key], true)) {
622 // Value already exists in attribute
626 $attributes[$key][] = $value;
630 if (!$this->check_password($password, $attributes['pass'][0])) {
631 throw new Error\
Error('WRONGUSERPASS');
634 unset($attributes['pass']);
637 $this->add_more_attributes($attributes, $username);
640 Logger
::info('fsfdrupalauth:'.$this->authId
.': Attributes: '.
641 implode(',', array_keys($attributes)));