Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 224 additions & 87 deletions pam_accounts/accounts.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
*
* "30 * * * * /var/local/submitty/bin/accounts.php"
*
* You may specify the term on the command line with "-t".
* You may specify on the command line:
* "-t [term code]" make auth accounts for [term code].
* "-g" can be used to guess the term by the server's calendar month and year.
* "-a" will make auth accounts for all instructors and all active courses.
* "-r" will remove grader and student auth accounts from inactive courses.
* For example:
*
* ./accounts.php -t s18
Expand All @@ -26,22 +29,22 @@
* @author Peter Bailie, Systems Programmer (RPI dept of computer science)
*/

error_reporting(0);
ini_set('display_errors', 0);
error_reporting(null);
ini_set('display_errors', '0');

//Database access
define('DB_LOGIN', 'hsdbu');
define('DB_PASSWD', 'hsdbu_pa55w0rd');
define('DB_LOGIN', 'submitty_dbuser');
define('DB_PASSWD', 'submitty_dbuser_pa55W0rd');
define('DB_HOST', 'localhost');
define('DB_NAME', 'submitty');

//Location of accounts creation error log file
define('ERROR_LOG_FILE', '/var/local/submitty/bin/accounts_errors.log');
define('ERROR_LOG_FILE', 'accounts_script_error.log');

//Where to email error messages so they can get more immediate attention.
//Set to null to not send email.
define('ERROR_EMAIL', 'sysadmins@lists.myuniversity.edu');


/* SUGGESTED SETTINGS FOR TIMEZONES IN USA -------------------------------------
*
* Eastern ........... America/New_York
Expand All @@ -61,90 +64,227 @@
date_default_timezone_set('America/New_York');

//Start process
main();
exit(0);

/** Main process */
function main() {
//IMPORTANT: This script needs to be run as root!
if (posix_getuid() !== 0) {
exit("This script must be run as root." . PHP_EOL);
new make_accounts();

/** Class constructor manages script workflow */
class make_accounts {

/** @static @var resource pgsql database connection */
private static $db_conn;
/** @static @var string what workflow to process */
private static $workflow;
/** @static @var array paramater list for DB query */
private static $db_params;
/** @static @var string DB query to be run */
private static $db_query;
/** @static @var string function to call to process $workflow */
private static $workflow_function;
/** @static @var array user_id list of 'auth only accounts', read from /etc/passwd */
private static $auth_only_accounts;

public function __construct() {
//IMPORTANT: This script needs to be run as root!
if (posix_getuid() !== 0) {
exit("This script must be run as root." . PHP_EOL);
}

//Init class properties, quit on error.
if ($this->init() === false) {
exit(1);
}

//Do workflow, quit on error.
if ($this->process() === false) {
exit(1);
}

//All done.
exit(0);
}

//Check for semester among CLI arguments.
$semester = cli_args::parse_args();
if ($semester === false) {
exit(1);
public function __destruct() {
//Close DB connection, if it exists.
if (pg_connection_status(self::$db_conn) === PGSQL_CONNECTION_OK) {
pg_close(self::$db_conn);
}
}

/**
* Initialize class properties, based on self::$workflow
*
* @access private
* @return boolean TRUE on success, FALSE when there is a problem.
*/
private function init() {
//Check CLI args.
if (($cli_args = cli_args::parse_args()) === false) {
return false;
}
self::$workflow = $cli_args[0];
self::$db_params = is_null($cli_args[1]) ? array() : array($cli_args[1]);

//Define database query AND system call based on workflow.
switch(self::$workflow) {
case 'term':
self::$db_query = <<<SQL
SELECT DISTINCT user_id
FROM courses_users
WHERE semester=$1
SQL;
self::$workflow_function = 'add_user';
break;

case 'active':
self::$db_query = <<<SQL
SELECT DISTINCT cu.user_id
FROM courses_users as cu
LEFT OUTER JOIN courses as c ON cu.course=c.course AND cu.semester=c.semester
WHERE cu.user_group=1 OR (cu.user_group<>1 AND c.status=1)
SQL;
self::$workflow_function = 'add_user';
break;

case 'clean':
//'clean' workflow requires list of 'auth only accounts' from /etc/passwd.
self::$auth_only_accounts = array();
if (($fh = fopen('/etc/passwd', 'r')) === false) {
$this->logit("Cannot open '/etc/passwd' to check for auth only accounts.");
return false;
}
while (($row = fgetcsv($fh, 0, ':')) !== false) {
if (strpos($row[4], 'auth only account') !== false) {
self::$auth_only_accounts[] = $row[0];
}
}
fclose($fh);
self::$db_query = <<<SQL
SELECT DISTINCT cu.user_id
FROM courses_users as cu
LEFT OUTER JOIN courses as c ON cu.course=c.course AND cu.semester=c.semester
WHERE cu.user_group<>1 AND c.status<>1
SQL;
self::$workflow_function = 'remove_user';
break;

default:
$this->log_it("Invalid self::$workflow during init()");
return false;
}

//Signal success
return true;
}

$user_list = get_user_list($semester);
if ($user_list !== false) {
foreach($user_list as $user) {
//We don't care if user already exists as adduser will skip over any account that already exists.
system ("/usr/sbin/adduser --quiet --home /tmp --gecos 'RCS auth account' --no-create-home --disabled-password --shell /usr/sbin/nologin {$user} > /dev/null 2>&1");
/**
* Process workflow
*
* @access private
* @return boolean TRUE on success, FALSE when there is a problem.
*/
private function process() {
//Connect to database. Quit on failure.
if ($this->db_connect() === false) {
$this->log_it("Submitty Auto Account Creation: Cannot connect to DB {$db_name}.");
return false;
}

//Get user list based on command. Quit on failure.
if (($result = pg_query_params(self::$db_conn, self::$db_query, self::$db_params)) === false) {
$this->log_it("Submitty Auto Account Creation: Cannot read user list from {$db_name}.");
return false;
}

$num_rows = pg_num_rows($result);
for ($i = 0; $i < $num_rows; $i++) {
$user = pg_fetch_result($result, $i, 'user_id');
call_user_func(array($this, self::$workflow_function), $user);
}

//Signal success
return true;
}
}

/**
* Retrieve user list from courses_users
*
* @param string $semester
* @return array of user ids on success, boolean false on failure.
*/
function get_user_list($semester) {
$db_user = DB_LOGIN;
$db_pass = DB_PASSWD;
$db_host = DB_HOST;
$db_name = DB_NAME;
$db_conn = pg_connect("host={$db_host} dbname={$db_name} user={$db_user} password={$db_pass}");
if ($db_conn === false) {
log_it("Submitty Auto Account Creation: Cannot connect to DB {$db_name}.");
return false;
/**
* Establish connection to Submitty Database
*
* @access private
* @return boolean TRUE on success, FALSE when there is a problem.
*/
private function db_connect() {
$db_user = DB_LOGIN;
$db_pass = DB_PASSWD;
$db_host = DB_HOST;
self::$db_conn = pg_connect("host={$db_host} dbname=submitty user={$db_user} password={$db_pass} sslmode=prefer");
if (pg_connection_status(self::$db_conn) !== PGSQL_CONNECTION_OK) {
$this->log_it(pg_last_error(self::$db_conn));
return false;
}

//Signal success
return true;
}

/**
* Add a user for authentication with PAM.
*
* @access private
* @param string $user User ID to added.
*/
private function add_user($user) {
system("/usr/sbin/adduser --quiet --home /tmp --gecos 'auth only account' --no-create-home --disabled-password --shell /usr/sbin/nologin {$user} > /dev/null 2>&1");
}

$db_query = pg_query_params($db_conn, "SELECT DISTINCT user_id FROM courses_users WHERE semester=$1;", array($semester));
if ($db_query === false) {
log_it("Submitty Auto Account Creation: Cannot read user list from {$db_name}.");
return false;
/**
* Remove an 'auth only user' from authenticating with PAM
*
* @access private
* @param string $user User ID to be checked/removed.
*/
private function remove_user($user) {
//Make sure $user is an "auth only account" before removing.
if (array_search($user, self::$auth_only_accounts) !== false) {
system("/usr/sbin/deluser --quiet {$user} > /dev/null 2>&1");
}
}

$user_list = pg_fetch_all_columns($db_query, 0);
pg_close($db_conn);
return $user_list;
}
/**
* Log message to email and text files
*
* @access private
* @param string $msg
*/
private function log_it($msg) {
$msg = date('m/d/y H:i:s : ', time()) . $msg . PHP_EOL;
error_log($msg, 3, ERROR_LOG_FILE);

if (!is_null(ERROR_EMAIL)) {
error_log($msg, 1, ERROR_EMAIL);
}
}
} //END class make_accounts

/**
* Log message to email and text files
* class to parse command line arguments
*
* @param string $msg
* @static
*/
function log_it($msg) {
$msg = date('m/d/y H:i:s : ', time()) . $msg . PHP_EOL;
error_log($msg, 3, ERROR_LOG_FILE);

if (!is_null(ERROR_EMAIL)) {
error_log($msg, 1, ERROR_EMAIL);
}
}

/** @static class to parse command line arguments */
class cli_args {

/** @var array holds all CLI argument flags and their values */
private static $args;
/** @var string usage help message */
private static $help_usage = "Usage: accounts.php [-h | --help] (-t [term code] | -g)" . PHP_EOL;
private static $help_usage = "Usage: accounts.php [-h | --help] (-a | -t [term code] | -g | -r)" . PHP_EOL;
/** @var string short description help message */
private static $help_short_desc = "Read student enrollment from Submitty DB and create accounts for PAM auth." . PHP_EOL;
/** @var string argument list help message */
private static $help_args_list = <<<HELP
Arguments
-h --help Show this help message.
-t [term code] Term code associated with student enrollment.
-g Guess the term code based on calendar month and year.
-a Make auth accounts for all active courses.
-t [term code] Make auth accounts for specified term code.
-g Make auth accounts for guessed term code, based on calendar
month and year.
-r Remove auth accounts from inactive courses.

NOTE: -t and -g are mutally exclusive. One is required.
NOTE: Argument precedence order is -a, -t, -g, -r. One is required.

HELP;

Expand All @@ -154,32 +294,29 @@ class cli_args {
* Called with 'cli_args::parse_args()'
*
* @access public
* @return mixed term code as string or boolean false when no term code is present.
* @return array consisting of process command (string) and possibly associated term code (string or null) or boolean false on error.
*/
public static function parse_args() {

self::$args = getopt('hgt:', array('help'));
$args = getopt('t:agrh', array('help'));

switch(true) {
case array_key_exists('h', self::$args):
case array_key_exists('help', self::$args):
case array_key_exists('h', $args):
case array_key_exists('help', $args):
self::print_help();
return false;
case array_key_exists('g', self::$args):
if (array_key_exists('t', self::$args)) {
//-t and -g are mutually exclusive
print "-g and -t cannot be used together." . PHP_EOL;
return false;
} else {
//Guess current term
//(s)pring is month <= 5, (f)all is month >= 8, s(u)mmer are months 6 and 7.
//if ($month <= 5) {...} else if ($month >= 8) {...} else {...}
$month = intval(date("m", time()));
$year = date("y", time());
return ($month <= 5) ? "s{$year}" : (($month >= 8) ? "f{$year}" : "u{$year}");
}
case array_key_exists('t', self::$args):
return self::$args['t'];
case array_key_exists('a', $args):
return array("active", null);
case array_key_exists('t', $args):
return array("term", $args['t']);
case array_key_exists('g', $args):
//Guess current term
//(s)pring is month <= 5, (f)all is month >= 8, s(u)mmer are months 6 and 7.
//if ($month <= 5) {...} else if ($month >= 8) {...} else {...}
$month = intval(date("m", time()));
$year = date("y", time());
return ($month <= 5) ? array("term", "s{$year}") : (($month >= 8) ? array("term", "f{$year}") : array("term", "u{$year}"));
case array_key_exists('r', $args):
return array("clean", null);
default:
print self::$help_usage . PHP_EOL;
return false;
Expand All @@ -200,7 +337,7 @@ private static function print_help() {
//Arguments list
print self::$help_args_list . PHP_EOL;
}
}
} //END class parse_args

/* EOF ====================================================================== */
?>
Loading