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;
47 private $query_nomination_process_donations;
48 private $query_nomination_process_gift_receipt;
49 private $query_nomination_process_adhoc;
52 * SQL query parameters, or variables that help determine which attributes
56 private $gift_redeem_page_id;
58 private $nomination_process_contrib_start_date;
59 private $nomination_process_contrib_end_date;
60 private $nomination_process_adhoc_access_group_id;
61 private $membership_monthly_rate;
62 private $student_membership_monthly_rate;
65 * Constructor for this authentication source.
67 * @param array $info Information about this authentication source.
68 * @param array $config Configuration.
70 public function __construct($info, $config)
72 assert(is_array($info));
73 assert(is_array($config));
75 // Call the parent constructor first, as required by the interface
76 parent
::__construct($info, $config);
78 // Make sure that all required parameters are present.
87 'query_nomination_process_donations',
88 'query_nomination_process_gift_receipt',
89 'query_nomination_process_adhoc',
92 'gift_redeem_page_id',
94 'nomination_process_contrib_start_date',
95 'nomination_process_contrib_end_date',
96 'nomination_process_adhoc_access_group_id',
97 'membership_monthly_rate',
98 'student_membership_monthly_rate']
101 if (!array_key_exists($param, $config)) {
102 throw new Exception('Missing required attribute \''.$param.
103 '\' for authentication source '.$this->authId
);
106 if (!is_string($config[$param])) {
107 throw new Exception('Expected parameter \''.$param.
108 '\' for authentication source '.$this->authId
.
109 ' to be a string. Instead it was: '.
110 var_export($config[$param], true));
113 $this->$param = $config[$param];
116 if (isset($config['options'])) {
117 $this->options
= $config['options'];
123 * Create a database connection.
125 * @return PDO The database connection.
127 private function connect()
130 $db = new PDO($this->dsn
, $this->username
, $this->password
, $this->options
);
131 } catch (PDOException
$e) {
132 // Obfuscate the password if it's part of the dsn
133 $obfuscated_dsn = preg_replace('/(user|password)=(.*?([;]|$))/', '${1}=***', $this->dsn
);
135 throw new Exception('fsfdrupalauth:' . $this->authId
. ': - Failed to connect to \'' .
136 $obfuscated_dsn . '\': ' . $e->getMessage());
139 $db->setAttribute(PDO
::ATTR_ERRMODE
, PDO
::ERRMODE_EXCEPTION
);
141 $driver = explode(':', $this->dsn
, 2);
142 $driver = strtolower($driver[0]);
144 // Driver specific initialization
148 $db->exec("SET NAMES 'utf8mb4'");
152 $db->exec("SET NAMES 'UTF8'");
160 * Check the password against a Drupal hash
163 private function check_password($password, $hash) {
166 // The reason for running a separate process is so that the PHP global
167 // env doesn't get clobbered by include / require.
170 // pipes code based off of https://www.php.net/manual/en/function.proc-open.php
171 // CC-BY 3.0 or later
172 $descriptorspec = array(
173 0 => array("pipe", "r"), // stdin is a pipe that the child may read from
174 1 => array("pipe", "w"), // stdout is a pipe that the child may write to
175 2 => array("pipe", "w") // stderr is a pipe that the child may write to
178 $cwd = "../modules/fsfdrupalauth/extlib";
179 //$env = array('some_option' => 'aeiou');
182 $process = proc_open('php drupal-pw-check.php', $descriptorspec, $pipes, $cwd, $env);
184 if (is_resource($process)) {
185 // $pipes now looks like this:
186 // 0 => writeable handle connected to child stdin
187 // 1 => readable handle connected to child stdout
189 fwrite($pipes[0], json_encode([$password, $hash]));
192 $result = stream_get_contents($pipes[1]);
195 $errors = stream_get_contents($pipes[2]);
198 // It is important that you close any pipes before calling
199 // proc_close in order to avoid a deadlock
200 $return_value = proc_close($process);
202 //Logger::debug('fsfdrupalauth:'.$this->authId.': authenticator stdout: '.$result);
204 $errors_found_yet = false;
206 Logger
::error('fsfdrupalauth:'.$this->authId
.': authenticator stderr: '.$errors);
207 $errors_found_yet = true;
210 if ($return_value != 0) {
211 Logger
::error('fsfdrupalauth:'.$this->authId
.': authenticator non-zero return code: '.$return_value);
212 $errors_found_yet = true;
215 return (!$errors_found_yet && is_string($result) && rtrim($result) == "true");
219 Logger
::error('fsfdrupalauth:'.$this->authId
.': unable to launch authenticator');
227 * query the database with arbitrary queries that only require a user name.
230 private function query_db($queryname, $query_params)
232 assert(is_string($queryname));
233 assert(is_string($username));
235 $db = $this->connect();
238 $sth = $db->prepare($this->$queryname);
239 } catch (PDOException
$e) {
240 throw new Exception('fsfdrupalauth:'.$this->authId
.
241 ': - Failed to prepare queryname: '.$queryname.': '.$e->getMessage());
245 $sth->execute($query_params);
246 } catch (PDOException
$e) {
247 throw new Exception('fsfdrupalauth:'.$this->authId
.
248 ': - Failed to execute queryname: '.$queryname.': '.$e->getMessage());
252 $data = $sth->fetchAll(PDO
::FETCH_ASSOC
);
253 } catch (PDOException
$e) {
254 throw new Exception('fsfdrupalauth:'.$this->authId
.
255 ': - Failed to fetch result set: '.$e->getMessage());
258 Logger
::info('fsfdrupalauth:'.$this->authId
.': Got '.count($data).
259 ' rows from database');
265 * add more CAS attributes to user, such as is_staff and is_member
267 private function add_more_attributes(&$attributes, $username) {
270 // query on membership
273 $membership_data = $this->query_db('query_membership', ['username' => $username]);
275 if (count($membership_data) === 0) {
276 // No rows returned - invalid username
277 Logger
::debug('fsfdrupalauth:'.$this->authId
.
278 ': No rows in result set. Probably no membership.');
281 $attributes['is_member'] = ['false'];
282 $attributes['was_member'] = ['false'];
284 foreach ($membership_data as $row) {
285 foreach ($row as $key => $value) {
286 if ($value === null) {
289 $value = (string) $value;
291 if ($value === '1' ||
$value === '2' ||
$value === '3') {
292 $attributes['is_member'] = ['true'];
293 $attributes['was_member'] = ['true'];
294 } elseif ($value === '4') {
295 $attributes['was_member'] = ['true'];
301 // query for access to board nomination process
304 $start_date = $this->nomination_process_contrib_start_date
;
305 $end_date = $this->nomination_process_contrib_end_date
;
308 * @param string $query_name Name of query in authsources
309 * @param array $extra_params Associative array of parameters to include in query
311 $donation_query = function ($query_name, $extra_params)
314 $parameters = ['username' => $username];
316 foreach ($extra_params as $key => $value) {
317 $parameters[$key] = $value;
320 return $this->query_db($query_name, $parameters);
323 $compare_res = function ($result, $amount) {
324 foreach ($result[0] as $key => $value) {
325 if (intval($value) >= $amount) {
332 // looks for memberships / comparable donations in time window. also
333 // looks for a membership or donation (included as a param) that
334 // occurred up to a year before, and that would have carried over into
335 // the time window with a single donation. this approximates whether
336 // the person was, or would have been, a member during the configured
338 $analyze_history = function ($selective_donations_history)
339 use ($start_date, $end_date) {
343 $start_date_obj = new \
DateTime($start_date);
344 $end_date_obj = new \
DateTime($end_date);
346 foreach ($selective_donations_history as $row) {
348 $amount = intval($row['amount']);
349 $member_type_id = $row['member_type_id'];
350 $receive_date_obj = new \
DateTime($row['receive_date']);
355 } elseif ($receive_date_obj >= $start_date_obj and $receive_date_obj <= $end_date_obj) {
358 } elseif ($receive_date_obj < $start_date_obj) {
359 switch ($member_type_id) {
362 $rate = intval($this->student_membership_monthly_rate
);
368 $rate = intval($this->membership_monthly_rate
);
371 $membership_end_date_obj = new \
DateTime($row['receive_date']);
372 $membership_end_date_obj->add(new \
DateInterval("P" . ceil($amount / $rate) . "M"));
374 if ($membership_end_date_obj >= $start_date_obj) {
382 $donation_params = ['start_date' => $start_date, 'end_date' => $end_date];
383 $gift_member_params = ['start_date' => $start_date, 'end_date' => $end_date, 'gift_redeem_page_id' => intval($this->gift_redeem_page_id
)];
384 $adhoc_params = ['adhoc_access_group_id' => intval($this->nomination_process_adhoc_access_group_id
)];
386 if ($compare_res($donation_query('query_nomination_process_adhoc', $adhoc_params), 1) ||
($attributes['is_member'] == ['true']
387 && ($analyze_history($donation_query('query_nomination_process_donations', $donation_params))
388 ||
$compare_res($donation_query('query_nomination_process_gift_receipt', $gift_member_params), 1)))) {
390 $attributes['nomination_process'] = ['true'];
392 Logger
::debug('fsfdrupalauth:'.$this->authId
.
393 ': Not a member / comparable donor during window for board process.');
394 $attributes['nomination_process'] = ['false'];
401 $staff_data = $this->query_db('query_staff', ['username' => $username, 'fsf_org_id' => $this->fsf_org_id
]);
403 if (count($staff_data) === 0) {
404 // No rows returned - invalid username
405 Logger
::debug('fsfdrupalauth:'.$this->authId
.
406 ': No rows in result set. Probably not FSF staff.');
409 $attributes['is_fsf_staff'] = ['false'];
411 foreach ($staff_data as $row) {
412 foreach ($row as $key => $value) {
414 if ($value === null) {
417 $value = (string) $value;
419 if ($value === $username) {
421 $attributes[$key] = ['true'];
428 // aggregate attribute
433 foreach ($attributes as $key => $value) {
434 if ($value == ['true']) {
436 $groups_list .= ', ';
438 $groups_list .= $key;
443 $attributes['groups_list'] = [$groups_list];
447 * Attempt to log in using the given username and password.
449 * On a successful login, this function should return the users attributes. On failure,
450 * it should throw an exception. If the error was caused by the user entering the wrong
451 * username or password, a Error\Error('WRONGUSERPASS') should be thrown.
453 * Note that both the username and the password are UTF-8 encoded.
455 * @param string $username The username the user wrote.
456 * @param string $password The password the user wrote.
457 * @return array Associative array with the users attributes.
459 protected function login($username, $password)
461 assert(is_string($username));
462 assert(is_string($password));
464 //// keep this commented when it's not in use. it prints user passwords to the log file
465 //Logger::debug('fsfdrupalauth:'.$this->authId.': entered password: '.$password);
468 $user_data = $this->query_db('query_main', ['username' => $username]);
471 if (count($user_data) === 0) {
472 // No rows returned - invalid username
473 Logger
::error('fsfdrupalauth:'.$this->authId
.
474 ': No rows in result set. Probably wrong username.');
475 throw new Error\
Error('WRONGUSERPASS');
478 /* Extract attributes. We allow the resultset to consist of multiple rows. Attributes
479 * which are present in more than one row will become multivalued. null values and
480 * duplicate values will be skipped. All values will be converted to strings.
484 // use the entered user name so we don't forcibly change it to all
485 // lower case. this is to preserve the behavior of the old cas server,
486 // and to remain compatible with our MW and Discourse sites that are
488 $attributes['name'][] = $username;
490 foreach ($user_data as $row) {
491 foreach ($row as $key => $value) {
492 if ($value === null) {
496 $value = (string) $value;
498 if (!array_key_exists($key, $attributes)) {
499 $attributes[$key] = [];
502 if (in_array($value, $attributes[$key], true)) {
503 // Value already exists in attribute
507 $attributes[$key][] = $value;
511 if (!$this->check_password($password, $attributes['pass'][0])) {
512 throw new Error\
Error('WRONGUSERPASS');
515 unset($attributes['pass']);
518 $this->add_more_attributes($attributes, $username);
521 Logger
::info('fsfdrupalauth:'.$this->authId
.': Attributes: '.
522 implode(',', array_keys($attributes)));