#!/usr/bin/perl -w
#
#   Apply modifications from the user database.
#

use strict;
$ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
$) = $( = 0;
$> = $< = 0;

require Symbol;
require DBI;
require Getopt::Long;


############################################################################
#
#   Constant variables
#
############################################################################

my $PATH_PASSWD = "/etc/passwd";
my $PATH_ALIASES = "/etc/aliases";
my $PATH_NEWALIASES = "/usr/bin/newaliases";
my $PATH_SQUID_CONF = "/etc/squid.conf";

my $DBI_DSN = 'DBI:mysql:user';
my $DBI_USER = 'nobody';
#my $DBI_PASS = 'my_name_is_nobody';
my $DBI_PASS = '';

my $ALIAS_START =
    "#\n# Automatically generated by /usr/local/bin/usersModified\n#\n";
my $ALIAS_END =
    "#\n# End of automatically generated by /usr/local/bin/usersModified\n#\n";


############################################################################
#
#   Global variables
#
############################################################################

use vars qw($debug $verbose);
$debug = 0;
$verbose = 0;


############################################################################
#
#   Name:    ReadUserListFromFile
#
#   Purpose: Read list of existing users from file.
#
#   Inputs:  Nothing
#
#   Returns: Hash ref of users
#
############################################################################

sub ReadUserListFromFile {
    my %users;
    if (-f $PATH_PASSWD) {
	my $fh = Symbol::gensym();
	if (!open($fh, "<$PATH_PASSWD")) {
	    die "Cannot open $PATH_PASSWD: $!";
	}
	while (defined(my $line = <$fh>)) {
	    chomp $line;
	    my($user, $passwd, $uid, $gid, $realname, $homedir, $shell) =
		split(/:/, $line);
	    if ($uid > 599  &&  $uid < 60000) {
		$users{$user} = {
		    'USER' => $user,
		    'PASSWORD' => $passwd,
		    'UID' => $uid,
		    'GID' => $gid,
		    'REALNAME' => $realname,
		    'HOMEDIR' => $homedir,
		    'SHELL' => $shell
		    };
	    }
	}
    }
    \%users;
}


############################################################################
#
#   Name:    ReadUserListFromDB
#
#   Purpose: Read list of current users from database.
#
#   Inputs:  Nothing
#   Returns: Hash ref of users
#
############################################################################

sub ReadUserListFromDB {
    my %users;
    my %groups;
    my $dbh = DBI->connect($DBI_DSN, $DBI_USER, $DBI_PASS,
			   { 'PrintError' => 1, 'RaiseError' => 1,
			     'Warn' => 1 });

    my $sth = $dbh->prepare("SELECT * FROM USERS");
    $sth->execute();
    while (my $ref = $sth->fetchrow_hashref()) {
	$users{$ref->{'USER'}} = $ref;
    }
    $sth->finish();

    $sth = $dbh->prepare("SELECT * FROM GROUPS");
    $sth->execute();
    while (my $ref = $sth->fetchrow_arrayref()) {
	if (!exists($groups{$ref->[0]})) {
	    $groups{$ref->[0]} = [];
	}
	push(@{$groups{$ref->[0]}}, $ref->[1]);
    }
    $sth->finish();

    $dbh->disconnect();
    (\%users, \%groups);
}

############################################################################
#
#   Name:    UpdateEtcPasswd
#
#   Purpose: Updates the user list in /etc/passwd
#
#   Inputs:  $users_file - Hash ref of current users in /etc/passwd, only
#                users of UID's between 600 and 60000 must appear!
#            $users_db - Hash ref of current users in database
#
#   Returns: Nothing
#
############################################################################

sub UpdateEtcPasswd {
    my($users_file, $users_db) = @_;
    my(@deleted_users);
    my(%done_users);

    # First step: Verify for modifications of currently existing users
    foreach my $uf (values (%$users_file)) {
	if (!exists($users_db->{$uf->{'USER'}})) {
	    push(@deleted_users, $uf->{'USER'});
	    next;
	}
	my $udb = $users_db->{$uf->{'USER'}};
	if ($uf->{'UID'} ne $udb->{'UID'}) {
	    printf STDERR ("Warning: Cannot update UID of user %s\n.",
			   $uf->{'USER'});
	}
	if ($uf->{'HOMEDIR'} ne $udb->{'HOMEDIR'}) {
	    printf STDERR ("Warning: Cannot update home directory of user"
			   . " %s.\n",, $uf->{'USER'});
	}
	if ($uf->{'REALNAME'} ne $udb->{'REALNAME'}) {
	    printf("Changing real name of user %s:\n", $udb->{'USER'});
	    Command("chfn", "-f", $udb->{'REALNAME'}, $udb->{'USER'});
	}
	if ($uf->{'SHELL'} ne $udb->{'SHELL'}) {
	    printf("Changing shell of user %s:\n", $udb->{'USER'});
	    Command("chsh", "-s", $udb->{'SHELL'}, $udb->{'USER'});
	}
	{
	    my $pwd = $uf->{'PASSWORD'};
	    my $salt = substr($pwd, 0, 2);
	    if (crypt($udb->{'PASSWORD'}, $salt) ne $pwd) {
		printf("Changing password of user %s:\n", $udb->{'USER'});
		SCommand("echo '%s:%s' | chpasswd",
			 $udb->{'USER'}, $udb->{'PASSWORD'});
	    }
	}
	$done_users{$uf->{'USER'}} = 1;
    }

    # Second step: Delete users we no longer need
    foreach my $user (@deleted_users) {
	printf("Deleting user %s\n", $user);
	Command("userdel",  $user);
    }

    # Third step: Create any new users
    foreach my $user (values(%$users_db)) {
	if ($done_users{$user->{'USER'}}) {
	    next;
	}
	printf("Creating user %s\n", $user->{'USER'});
	Command("useradd", "-c", $user->{'REALNAME'},
		"-d", $user->{'HOMEDIR'}, "-u", $user->{'UID'},
		"-s", $user->{'SHELL'},
		$user->{'USER'});
	printf("Changing password of user %s:\n", $user->{'USER'});
	SCommand("echo '%s:%s' | chpasswd",
		 $user->{'USER'}, $user->{'PASSWORD'});
    }
}


############################################################################
#
#   Name:    UpdateEtcAliases
#
#   Purpose: Updates /etc/aliases
#
#   Inputs:  $user_db - Hash ref of users currently in database
#
#   Returns: Nothing
#
############################################################################

sub UpdateEtcAliases ($$) {
    my $user_db = shift; my $groups = shift;

    my $db_aliases = '';
    foreach my $user (values(%$user_db)) {
	foreach my $alias (split(/,/, $user->{'ALIASES'})) {
	    $alias =~ s/^\s+//;
	    $alias =~ s/\s+$//;
	    if ($alias  &&  (lc $alias) ne (lc $user->{'USER'})) {
		$db_aliases .= sprintf("%s: %s\n", $alias, $user->{'USER'});
	    }
	}
	if ($user->{'FORWARD'}) {
	    $db_aliases .= sprintf("%s: %s\n", $user->{'USER'},
				   $user->{'FORWARD'});
	}
    }
    foreach my $group (keys(%$groups)) {
	$db_aliases .= sprintf("%s: %s\n",
			       $group, join(",", @{$groups->{$group}}));
    }

    my $alias_file;
    my $fh = Symbol::gensym();
    {
	local $/ = undef;
	if (!open($fh, "<$PATH_ALIASES")  ||
	    !defined($alias_file = <$fh>)  ||
	    !close($fh)) {
	    die "Error while reading $PATH_ALIASES: $!";
	}
    }

    $alias_file =~ s/\Q$ALIAS_START\E.*\Q$ALIAS_END\E//s;
    $alias_file .= "$ALIAS_START$db_aliases$ALIAS_END";

    require IO::AtomicFile;
    if (!($fh = IO::AtomicFile->new($PATH_ALIASES, "w"))  ||
	!$fh->print($alias_file)  ||  !$fh->close()) {
	printf STDERR (<<'END_OF_MESSAGE', $PATH_ALIASES, $!);

An error occurred while writing the file %s: $!

This can be fatal for the email system, please take immediate action!

END_OF_MESSAGE
        exit(1);
    }
    chmod 0644, $PATH_ALIASES;

    Command($PATH_NEWALIASES);
}


############################################################################
#
#   Name:    Command
#
#   Purpose: Issue a shell command
#
#   Inputs:  Command line arguments
#
#   Returns: Nothing
#
############################################################################

sub SCommand {
    my $fmt = shift;
    my @args = map { my $c = $_; $c =~ s/([^a-zA-Z0-9_\/\-])/\\$1/g; $c } @_;
    Command(sprintf($fmt, @args));
}
sub Command {
    print ("Executing command: ", join(" ", @_), "\n");
    system @_;
}


############################################################################
#
#   Name:    ConfigureSquid
#
#   Purpose: Modify Squid's config file to allow access for a given range
#            of users.
#
#   Input:   $range - Array of IP number ranges
#
#   Returns: Nothing, exits in case of problems
#
############################################################################

sub ConfigureSquid {
    my $ranges = shift;

    # Make range names unique
    my %names;
    my $acl = '';
    for (my $i = 0;  $i < @$ranges;  $i++) {
	my $name = $ranges->[$i];
	my $from;
	my $to;
	if ($name =~ /(.*?),(.*?),(.*)/) {
	    $from = $1;
	    $to = $2;
	    $name = $3;
	    if ($from !~ /^\d+\.\d+\.\d+\.\d+$/) {
		die "Invalid From IP: $from";
	    }
	    if ($to !~ /^\d+\.\d+\.\d+\.\d+$/) {
		die "Invalid To IP: $to";
	    }
	}
        $name =~ s/\s+/_/g;
        $name =~ s/[^\w\d_]//g;
        for (my $j = 0;  1;  $j++) {
	    my $n = $j ? ($name . "_$j") : $name;
	    if (!$names{$n}) {
		$name = $n;
		if (defined($from)) {
		    if ($verbose) {
			print "Adding acl $name $from-$to\n";
		    }
		    $acl .= "acl $name src $from-$to\n";
		}
		$names{$n} = 1;
		last;
	    }
	}
    }

    $acl .= "\n";
    foreach my $aclname (keys %names) {
	print "Adding http_access $aclname\n";
	$acl .= "http_access allow $aclname\n";
    }
    $acl .= "\n";
    foreach my $aclname (keys %names) {
	print "Adding icp_access $aclname\n";
	$acl .= "icp_access allow $aclname\n";
    }
    $acl .= "\n";
    foreach my $aclname (keys %names) {
	print "Adding miss_access $aclname\n";
	$acl .= "miss_access allow $aclname\n";
    }
    $acl .= "\n";

    my $contents;
    my $fh = Symbol::gensym();
    if (!open($fh, "<$PATH_SQUID_CONF")) {
	die "Cannot open $PATH_SQUID_CONF: $!";
    }
    {
	local $/ = undef;
	if (!defined($contents = <$fh>)  ||  !close($fh)) {
	    die "Error while reading $PATH_SQUID_CONF: $!";
	}
    }

    $contents =~ s/\Q$ALIAS_START\E.*\Q$ALIAS_END\E//s;
    $contents .= "$ALIAS_START$acl$ALIAS_END";

    if ($verbose) {
	print "Saving $PATH_SQUID_CONF.\n";
    }
    if ($debug) {
	print $contents;
    } else {
	require IO::AtomicFile;
	if (!($fh = IO::AtomicFile->new($PATH_SQUID_CONF, "w"))
	    or  !$fh->print($contents)
	    or  !$fh->close()) {
	    printf STDERR (<<'END_OF_MESSAGE', $PATH_SQUID_CONF, $!);

An error occurred while writing the file %s: $!

This can be fatal for the WWW proxy system, please take immediate action!

END_OF_MESSAGE
	}
    }
}


############################################################################
#
#   Name:    Usage
#
#   Purpose: Print usage message
#
#   Inputs:  None
#
#   Returns: Nothing
#
############################################################################

sub Usage {
    my $sqfile = $PATH_SQUID_CONF;
    print qq{
Usage: $0 <action> [options]

Possible actions are:

  --squid	Modify $PATH_SQUID_CONF to accept requests from machines
                given by --range options.
  --users       Read user data from DBI database and store it in /etc/passwd,
                /etc/group, ...


Possible options are:

  --debug       Run in debugging mode: Perform nothing, but print what would
                be done.
  --range <f,t,n> Set a range of IP numbers for Squid's config file. Example:
                --range "192.168.1.64,192.168.1.127,Local LAN". A range of
                "all" means public access.
  --verbose     Enter verbose mode and log actions on stderr. Defaults to
                quiet mode.
};
}


############################################################################
#
#   This is main().
#
############################################################################

{
    my %o = (
	'debug' => \$debug,
	'verbose' => \$verbose,
	'squid' => 0,
	'range' => []
    );
    Getopt::Long::GetOptions(\%o, 'debug', 'verbose', 'squid', 'range=s@');
    ++$verbose if $debug;

    if ($o{squid}) {
	if (!@{$o{range}}) {
	    Usage();
	}
	ConfigureSquid($o{range});
    } else {
	my $users_file = ReadUserListFromFile();
	my($users_db, $groups) = ReadUserListFromDB();
	UpdateEtcPasswd($users_file, $users_db);
	UpdateEtcAliases($users_db, $groups);
    }
}
