#!/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; use Symbol (); use DBI (); use Getopt::Long (); ############################################################################ # # Constant variables # ############################################################################ my $PATH_PASSWD = "/etc/passwd"; my $PATH_ALIASES = "/etc/mail/aliases"; my $PATH_NEWALIASES = "/usr/bin/newaliases"; my $PATH_SQUID_CONF = -f "/etc/squid/squid.conf" ? "/etc/squid/squid.conf" : "/etc/squid.conf"; my $PATH_SQUID = (-x "/usr/sbin/squid") ? "/usr/sbin/squid" : "/usr/bin/squid"; 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") if $verbose; system @_ unless $debug; } ############################################################################ # # 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 = ''; if (@$ranges) { 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)) { print "Adding acl $name $from-$to\n" if $verbose; $acl .= "acl $name src $from-$to\n"; } $names{$n} = 1; last; } } } } else { $acl = "\n" . "http_access deny all\n" . "icp_access deny all\n" . "miss_access deny all\n"; } $acl .= "\n"; foreach my $aclname (keys %names) { print "Adding http_access $aclname\n" if $verbose; $acl .= "http_access allow $aclname\n"; } $acl .= "\n"; foreach my $aclname (keys %names) { print "Adding icp_access $aclname\n" if $verbose; $acl .= "icp_access allow $aclname\n"; } $acl .= "\n"; foreach my $aclname (keys %names) { print "Adding miss_access $aclname\n" if $verbose; $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 } } Command($PATH_SQUID, "-k", "reconfigure") } ############################################################################ # # Name: Usage # # Purpose: Print usage message # # Inputs: None # # Returns: Nothing # ############################################################################ sub Usage { my $sqfile = $PATH_SQUID_CONF; print qq{ Usage: $0 [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 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}) { ConfigureSquid($o{range}); } else { my $users_file = ReadUserListFromFile(); my($users_db, $groups) = ReadUserListFromDB(); UpdateEtcPasswd($users_file, $users_db); UpdateEtcAliases($users_db, $groups); } }