FS-Mod/StudiRegistration/StudiRegistration.php

394 lines
11 KiB
PHP

<?php
/**
*
* @file
* @author Bennet Bleßmann
* @author Einhard Leichtfuß
* @copyright © 2020 Bennet Bleßmann, Einhard Leichtfuß
* @license GNU General Public Licence 2.0 or later
*/
require_once(dirname(__FILE__) . "/StudiRegistrationCommon.php");
class StudiRegistration {
static function connect_to_ldap()
{
global $wgSRLdapServer, $wgSRLdapPwd, $wgSRLdapUser;
if (($ldap_conn = ldap_connect($wgSRLdapServer)) === false)
{
throw new Exception('Bad LDAP URI.');
}
if (! ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3))
{
throw new Exception('Could not set LDAP protocol version.');
}
if (! ldap_start_tls($ldap_conn))
{
throw new Exception('Could not start TLS.');
}
if (! ldap_bind($ldap_conn, $wgSRLdapUser, $wgSRLdapPwd))
{
throw new Exception('Could not bind to server. Error is "'
. ldap_error($ldap_conn) . '"');
}
return $ldap_conn;
}
static function ldap_search_wrapper($ldap_conn, $filter,
$required_attributes = array('*'))
{
global $wgSRLdapBase;
$result = ldap_search($ldap_conn, "{$wgSRLdapBase}", $filter);
if ($result === false)
{
throw new Exception('LDAP Error: ' . ldap_error($ldap_conn));
}
return $result;
}
static function ldap_count_entries_wrapper($ldap_conn, $result)
{
$n_entries = ldap_count_entries($ldap_conn, $result);
if ($n_entries === false)
{
throw new Exception('LDAP Error: ' . ldap_error($ldap_conn));
}
return $n_entries;
}
static function ldap_get_entries_wrapper($ldap_conn, $result)
{
$entries = ldap_get_entries($ldap_conn, $result);
if ($entries === false)
{
throw new Exception('LDAP Error: ' . ldap_error($ldap_conn));
}
return $entries;
}
/**
* Search for a stu-name in the math-studis LDAP directory.
*
* Optionally, only consider active users or users with the specified token.
* Note that such a token is only considered, if $only_active is set to
* false.
*
* Both $stu_name and $token_hash may be null in which case they are not
* used in the filter.
*/
static function ldap_search_stu($ldap_conn, $stu_name, $only_active,
$token_hash2, $required_attributes = array('dn'))
{
$filters = array();
if ($stu_name !== null)
{
$secure_name = ldap_escape(ACC_PREFIX . $stu_name);
array_push($filters, "(uid={$secure_name})");
}
if ($only_active)
{
array_push($filters, '(!(' . LDAP_TOKEN_ATTR . '=*))');
}
else if ($token_hash2 !== null)
{
array_push($filters, '('
. LDAP_TOKEN_ATTR . '=' . ldap_escape($token_hash2)
. ')');
}
$merged_filter = '(&' . implode('', $filters) . ')';
return self::ldap_search_wrapper($ldap_conn, $merged_filter,
$required_attributes);
}
static function ldap_exists_stu($ldap_conn, $stu_name, $only_active = false,
$token_hash2 = null)
{
$result = self::ldap_search_stu($ldap_conn, $stu_name, $only_active,
$token_hash2);
return self::ldap_count_entries_wrapper($ldap_conn, $result) != 0;
}
static function ldap_find_stu_by_token($ldap_conn, $only_active,
$token_hash2, $required_attributes = array('dn'))
{
$result = self::ldap_search_stu($ldap_conn, null, $only_active,
$token_hash2, $required_attributes);
return self::ldap_get_entries_wrapper($ldap_conn, $result);
}
/**
* Verify whether an account has been created and enabled for a specific
* stu-name.
*
* Function to be called from the outside.
* Sets up an LDAP connection and closes it in the end.
*/
static function stu_exists($stu_name)
{
$ldap_conn = false;
try
{
$ldap_conn = self::connect_to_ldap();
return self::ldap_exists_stu($ldap_conn, $stu_name, true);
}
catch (Exception $err)
{
error_log("{$err}");
}
finally
{
// Unbind from LDAP if connecting was somehow unsuccessfull.
if ($ldap_conn)
ldap_unbind($ldap_conn);
}
}
static function create_registration($stu_name, $pw_hash, $token_hash,
$ml_req)
{
global $wgSRLdapBase;
$ldap_conn = false;
try
{
$ldap_conn = self::connect_to_ldap();
$stu_number = (int)(explode('stu', $stu_name)[1]);
$acc_name = ACC_PREFIX . $stu_name;
$secure_name = ldap_escape($acc_name);
$token_hash2 = hash(TOKEN_HASH_ALGO, $token_hash);
$entry = array(
'objectClass' => 'inetOrgPerson',
'uid' => $acc_name,
'cn' => $acc_name,
'sn' => $acc_name,
LDAP_TIMESTAMP_ATTR => time(),
'roomNumber' => ROOM_NUMBER_OFFSET + $stu_number,
// Prepend <!> to pw hash to disable account until
// registration is complete.
'userPassword' => "{CRYPT}!{$pw_hash}",
'employeeType' => 'Mathematik',
'mail' => $stu_name . MAIL_SUFFIX,
LDAP_TOKEN_ATTR => $token_hash2,
LDAP_ML_REQ_ATTR => $ml_req ? 'yes' : 'no'
);
$dn = "uid={$secure_name},{$wgSRLdapBase}";
/*
* If the account exists already (but has not been enabled9, delete
* it first. One might alternatively consider to replace values;
* note however, that any potentially superfluous attributes would
* not be deleted in this case.
*/
if (self::ldap_exists_stu($ldap_conn, $stu_name, false))
{
if (! ldap_delete($ldap_conn, $dn))
return false;
}
return ldap_add($ldap_conn, $dn, $entry);
}
catch (Exception $err)
{
error_log("{$err}");
}
finally
{
if ($ldap_conn)
ldap_unbind($ldap_conn);
}
}
static function complete_registration($token_hash, &$ml_error)
{
$ldap_conn = false;
$ml_error = false;
try
{
$ldap_conn = self::connect_to_ldap();
$token_hash2 = hash(TOKEN_HASH_ALGO, $token_hash);
// Note that PHP's ldap_search() is awkward in that it requires and
// returns all attribute names in lower case.
// This does not seem to apply to ldap_mod_replace().
$entries = self::ldap_find_stu_by_token($ldap_conn, false,
$token_hash2,
array('dn', 'uid', 'userpassword', 'mail',
strtolower(LDAP_TIMESTAMP_ATTR),
strtolower(LDAP_ML_REQ_ATTR))
);
if (count($entries) != 0)
{
$time_now = time();
$time_then = $entries[0][strtolower(LDAP_TIMESTAMP_ATTR)][0];
if ($time_now < $time_then)
{
throw new Exception("Expiry date set in the future.");
return false;
}
else if ($time_now - $time_then > TOKEN_EXPIRY_SECONDS)
{
return false;
}
// Remove <!> before pw hash to enable account.
$enabledPassword = str_replace('{CRYPT}!', '{CRYPT}',
$entries[0]['userpassword'][0]);
// Associating an ettribute with the empty list / array
// effectively deletes the respective attribute.
$update = array( LDAP_TOKEN_ATTR => array()
, LDAP_TIMESTAMP_ATTR => array()
, LDAP_ML_REQ_ATTR => array()
, 'userPassword' => $enabledPassword
);
ldap_mod_replace($ldap_conn, $entries[0]['dn'], $update);
$acc_name = $entries[0]['uid'][0];
$ml_req = $entries[0][strtolower(LDAP_ML_REQ_ATTR)][0]
=== 'yes';
if ($ml_req)
{
$stu_mail = $entries[0]['mail'][0];
if (! self::add_user_to_ml($stu_mail))
{
$ml_error = true;
}
}
return $acc_name;
}
else
{
return false;
}
}
catch (Exception $err)
{
error_log("{$err}");
}
finally
{
if ($ldap_conn)
ldap_unbind($ldap_conn);
}
}
static function add_user_to_ml($addr)
{
$pipe_desc = array(
0 => array("pipe", "r"), //STDIN
1 => array("pipe", "w") //STDOUT
);
$ssh_proc = proc_open('ssh bosch', $pipe_desc, $pipes);
fwrite($pipes[0], $addr);
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
//make sure pipes are closed first otherwise risk deadlock
$retval = proc_close($ssh_proc);
// If already subscribed, a corresponding message is printed
// (i.e., $output non-empty).
return $retval === 0 && $output === '';
}
}
if (! defined('MEDIAWIKI')) {
global $wgSRLdapServer, $wgSRLdapPwd, $wgSRLdapUser,$wgSRLdapBase;
//TODO init globals
//TODO: verify input string lengths.
// This should probably be outsourced to a common third file.
if ($argc < 2)
die('Missing argument.\n');
try
{
switch ($argv[1])
{
case 'exists':
if ($argc !== 3)
{
die("'exists' requires one additional argument: stu_name\n");
}
self::stu_exists($argv[2]);
break;
case 'create_registration':
if ($argc !== 5)
{
die("'create_registration' requires three additional"
. " arguments: stu_name, password_hash and token_hash\n");
}
self::create_registration($argv[2], $argv[3], $argv[4]);
break;
case 'complete_registration':
if ($argc !== 4)
{
die("'complete_registration' requires two additional "
. " arguments: stu_name, token_hash\n");
}
self::complete_registration($argv[2]);
break;
default:
die("Expected first argument to be one of: 'exists',"
. " 'create_registration', 'complete_registration'\n");
}
}
catch(Exception $err)
{
error_log("{$err}");
}
}
# vi: tw=80 fo+=t ts=8 sts=0 et sw=4 sta ai
?>