--- loncom/Attic/lcuseradd	2000/10/30 02:42:17	1.9
+++ loncom/Attic/lcuseradd	2011/10/24 21:30:09	1.42
@@ -1,25 +1,69 @@
 #!/usr/bin/perl
+
+# The Learning Online Network with CAPA
+#
+# lcuseradd - LON-CAPA setuid script to coordinate all actions
+#             with adding a user with filesystem privileges (e.g. author)
 #
-# lcuseradd
 #
-# Scott Harrison
-# October 27, 2000
+# $Id: lcuseradd,v 1.42 2011/10/24 21:30:09 www Exp $
+###
+
+###############################################################################
+##                                                                           ##
+## ORGANIZATION OF THIS PERL SCRIPT                                          ##
+##                                                                           ##
+## 1. Description of script                                                  ##
+## 2. Invoking script (standard input)                                       ##
+## 3. Example usage inside another piece of code                             ##
+## 4. Description of functions                                               ##
+## 5. Exit codes                                                             ##
+## 6. Initializations                                                        ##
+## 7. Make sure this process is running from user=www                        ##
+## 8. Start running script with www permissions                              ##
+## 9. Handle case of another lcpasswd process (locking)                      ##
+## 10. Error-check input, need 3 values (user name, password 1, password 2)  ##
+## 11. Start running script with root permissions                            ##
+## 12. Add user and make www a member of the user-specific group             ##
+## 13. Set password                                                          ##
+## 14. Make final modifications to the user directory                        ##
+## 15. Exit script (unlock)                                                  ##
+##                                                                           ##
+###############################################################################
 
 use strict;
+use File::Find;
 
+
+# ------------------------------------------------------- Description of script
+#
 # This script is a setuid script that should
-# be run by user 'www'.  It creates a /home/USERNAME directory
-# as well as a /home/USERNAME/public_html directory.
-# It adds user entries to
-# /etc/passwd and /etc/groups.
+# be run by user 'www'.  It creates a /home/USERNAME directory.
+# It adds a user to the unix system.
 # Passwords are set with lcpasswd.
 # www becomes a member of this user group.
 
-# Standard input usage
+# -------------- Invoking script (standard input versus command-line arguments)
+#                Otherwise sensitive information will be available to ps-ers for
+#                a small but exploitable time window.
+#
+# Standard input (STDIN) usage
 # First line is USERNAME
-# Second line is PASSWORD
+# Second line is DOMAIN
 # Third line is PASSWORD
-
+# Fourth line is PASSWORD
+# Fifth line is the name of a file to which an error code will be written.
+#            If the fourth line is omitted, no error file will be written.
+#            In either case, the program Exits with the code as its Exit status.
+#            The error file will just be a single line containing an
+#            error code.
+#            
+#  
+#
+# Command-line arguments [USERNAME] [DOMAIN] [PASSWORD] [PASSWORD]
+# Yes, but be very careful here (don't pass shell commands)
+# and this is only supported to allow perl-system calls.
+#
 # Valid passwords must consist of the
 # ascii characters within the inclusive
 # range of 0x20 (32) to 0x7E (126).
@@ -27,247 +71,317 @@ use strict;
 # SPACE and
 # !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO
 # PQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
-
+#
 # Valid user names must consist of ascii
 # characters that are alphabetical characters
 # (A-Z,a-z), numeric (0-9), or the underscore
 # mark (_). (Essentially, the perl regex \w).
+# User names must begin with an alphabetical character
+# (A-Z,a-z).
 
-# Command-line arguments [USERNAME] [PASSWORD] [PASSWORD]
-# Yes, but be very careful here (don't pass shell commands)
-# and this is only supported to allow perl-system calls.
-
+# ---------------------------------- Example usage inside another piece of code
 # Usage within code
 #
-# $exitcode=system("/home/httpd/perl/lcuseradd","NAME","PASSWORD1","PASSWORD2")/256;
-# print "uh-oh" if $exitcode;
-
-# These are the exit codes.
+# $Exitcode=
+#      system("/home/httpd/perl/lcuseradd","NAME","DOMAIN","PASSWORD1","PASSWORD2")/256;
+# print "uh-oh" if $Exitcode;
+
+# ---------------------------------------------------- Description of functions
+# enable_root_capability() : have setuid script run as root
+# disable_root_capability() : have setuid script run as www
+# try_to_lock() : make sure that another lcpasswd process isn't running
+
+# ------------------------------------------------------------------ Exit codes
+# These are the Exit codes.
+# ( (0,"ok"),
+# (1,"User ID mismatch.  This program must be run as user 'www'"),
+# (2,"Error. This program needs 3 command-line arguments (username, ".
+#    "password 1, password 2)."),
+# (3,"Error. Three lines should be entered into standard input."),
+# (4,"Error. Too many other simultaneous password change requests being ".
+#    "made."),
+# (5,"Error. User $username does not exist."),
+# (6,"Error. Could not make www a member of the group \"$safeusername\"."),
+# (7,"Error. Root was not successfully enabled.),
+# (8,"Error. Cannot set password."),
+# (9,"Error. The user name specified has invalid characters."),
+# (10,"Error. A password entry had an invalid character."),
+# (11,"Error. User already exists.),
+# (12,"Error. Something went wrong with the addition of user ".
+#     "\"$safeusername\"."),
+# (13,"Error. Password mismatch."),
+# (14, "Error filename is invalid"),
+# (15, "Error. Could not add home directory.")
 
+# ------------------------------------------------------------- Initializations
 # Security
-$ENV{'PATH'}=""; # Nullify path information.
-$ENV{'BASH_ENV'}=""; # Nullify shell environment information.
+$ENV{'PATH'}='/bin/:/usr/bin:/usr/local/sbin:/home/httpd/perl'; # Nullify path
+                                                                # information
+delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # nullify potential taints
 
-# Do not print error messages if there are command-line arguments
-my $noprint=0;
-if (@ARGV) {
-    $noprint=1;
-}
-
-# Read in /etc/passwd, and make sure this process is running from user=www
-open (IN, "</etc/passwd");
-my @lines=<IN>;
-close IN;
-my $wwwid;
-for my $l (@lines) {
-    chop $l;
-    my @F=split(/\:/,$l);
-    if ($F[0] eq 'www') {$wwwid=$F[2];}
-}
-if ($wwwid!=$<) {
-    print("User ID mismatch.  This program must be run as user 'www'\n") unless $noprint;
-    exit 1;
+# Do not print error messages.
+my $noprint=1;
+
+print "In lcuseradd\n" unless $noprint;
+
+# ----------------------------- Make sure this process is running from user=www
+my $wwwid=getpwnam('www');
+&disable_root_capability;
+if ($wwwid!=$>) {
+    print("User ID mismatch.  This program must be run as user 'www'\n")
+	unless $noprint;
+    &Exit(1);
 }
+
+# ----------------------------------- Start running script with www permissions
 &disable_root_capability;
 
-# Handle case of another lcpasswd process
+# --------------------------- Handle case of another lcpasswd process (locking)
 unless (&try_to_lock("/tmp/lock_lcpasswd")) {
-    print "Error. Too many other simultaneous password change requests being made.\n" unless $noprint;
-    exit 4;
+    print "Error. Too many other simultaneous password change requests being ".
+	"made.\n" unless $noprint;
+    &Exit(4);
 }
 
-# Gather input.  Should be 3 values (user name, password 1, password 2).
+# ------- Error-check input, need 3 values (user name, password 1, password 2).
 my @input;
-if (@ARGV==3) {
+if (@ARGV>=3) {
     @input=@ARGV;
-}
-elsif (@ARGV) {
-    print("Error. This program needs 3 command-line arguments (username, password 1, password 2).\n") unless $noprint;
+} elsif (@ARGV) {
+    print("Error. This program needs at least 3 command-line arguments (username, ".
+	  "password 1, password 2 [errorfile]).\n") unless $noprint;
     unlink('/tmp/lock_lcpasswd');
-    exit 2;
-}
-else {
+    &Exit(2);
+} else {
     @input=<>;
-    if (@input!=3) {
-	print("Error. Three lines should be entered into standard input.\n") unless $noprint;
+    if (@input < 3) {
+	print("Error. At least three lines should be entered into standard input.\n")
+	    unless $noprint;
 	unlink('/tmp/lock_lcpasswd');
-	exit 3;
+	&Exit(3);
     }
-    map {chop} @input;
+    foreach (@input) {chomp;}
 }
 
-my ($username,$password1,$password2)=@input;
+my ($username,$domain,$password1,$password2, $error_file)=@input;
+print "Username = ".$username."\n" unless $noprint;
 $username=~/^(\w+)$/;
+print "Username after substitution - ".$username unless $noprint;
 my $safeusername=$1;
-if ($username ne $safeusername) {
-    print "Error. The user name specified has invalid characters.\n";
+print "Safe username = $safeusername \n" unless $noprint;
+
+print "Domain = ".$domain."\n" unless $noprint;
+
+if (($username ne $safeusername) or ($safeusername!~/^[A-Za-z]/)) {
+    print "Error. The user name specified $username $safeusername  has invalid characters.\n"
+	unless $noprint;
     unlink('/tmp/lock_lcpasswd');
-    exit 9;
+    &Exit(9);
 }
 my $pbad=0;
-map {if ((ord($_)<32)||(ord($_)>126)){$pbad=1;}} (split(//,$password1));
-map {if ((ord($_)<32)||(ord($_)>126)){$pbad=1;}} (split(//,$password2));
+foreach (split(//,$password1)) {if ((ord($_)<32)||(ord($_)>126)){$pbad=1;}}
+foreach (split(//,$password2)) {if ((ord($_)<32)||(ord($_)>126)){$pbad=1;}}
 if ($pbad) {
-    print "Error. A password entry had an invalid character.\n";
+    print "Error. A password entry had an invalid character.\n" unless $noprint;
     unlink('/tmp/lock_lcpasswd');
-    exit 10;
+    &Exit(10);
+}
+
+#
+#   Safe the filename.  For our case, it must only have alpha, numeric, period
+#   and path sparators..
+#
+
+print "Error file is $error_file \n" unless $noprint;
+
+if($error_file) {
+    if($error_file =~ /^([(\w)(\d)\.\/]+)$/) {
+	print "Error file matched pattern $error_file : $1\n" unless $noprint;
+	my $safe_error_file = $1;	# Untainted I think.
+	print "Error file after transform $safe_error_file\n"
+	    unless $noprint;
+	if($error_file == $safe_error_file) {
+	    $error_file = $safe_error_file; # untainted error_file.
+	} else {
+	    $error_file ="";
+	    print "Invalid error filename\n" unless $noprint;
+	    Exit(14);
+	}
+
+    } else {
+	$error_file="";
+	print "Invalid error filename\n" unless $noprint;
+	Exit(14);
+    }
 }
 
-# Only add user if we can create a brand new home directory (/home/username).
-if (-e "/home/$safeusername") {
-    print "Error. User already exists.\n" unless $noprint;
+
+# -- Only add the user if they are >not< in /etc/passwd.
+#    Used to look for the ability to create a new directory for the
+#    user, however that disallows authentication changes from i
+#    internal->fs.. so just check the passwd file instead.
+#
+my $not_found = system("cut -d: -f1 /etc/passwd | grep -q \"^$safeusername\$\" ");
+if (!$not_found) {
+    print "Error user already exists\n" unless $noprint;
     unlink('/tmp/lock_lcpasswd');
-    exit 8;
+    &Exit(11);
 }
 
-# Only add user if the two password arguments match.
+
+
+# -- Only add user if the two password arguments match.
+
 if ($password1 ne $password2) {
     print "Error. Password mismatch.\n" unless $noprint;
     unlink('/tmp/lock_lcpasswd');
-    exit 7;
+    &Exit(13);
 }
-
+print "enabling root\n" unless $noprint;
+# ---------------------------------- Start running script with root permissions
 &enable_root_capability;
 
-# Add user entry to /etc/passwd and /etc/groups in such
-# a way that www is a member of the user-specific group
+# ------------------- Add group and user, and make www a member of the group
+# -- Add group
 
-if (system('/usr/sbin/useradd','-c','LON-CAPA user',$safeusername)) {
-    print "Error.  Something went wrong with the addition of user \"$safeusername\".\n" unless $noprint;
+print "adding group: $safeusername \n" unless $noprint;
+my $status = system('/usr/sbin/groupadd', $safeusername);
+if ($status) {
+    print "Error.  Something went wrong with the addition of group ".
+          "\"$safeusername\".\n" unless $noprint;
+    print "Final status of groupadd = $status\n";
+    unlink('/tmp/lock_lcpasswd');
+    &Exit(12);
+}
+my $gid = getgrnam($safeusername);
+                                                                                
+# -- Add user
+
+print "adding user: $safeusername \n" unless $noprint;
+my $status = system('/usr/sbin/useradd','-c','LON-CAPA user','-g',$gid,$safeusername);
+if ($status) {
+    print "Error.  Something went wrong with the addition of user ".
+	  "\"$safeusername\".\n" unless $noprint;
+    system("/usr/sbin/groupdel $safeusername");
+    print "Final status of useradd = $status\n";
     unlink('/tmp/lock_lcpasswd');
-    exit 5;
+    &Exit(12);
 }
 
+print "Done adding user\n" unless $noprint;
 # Make www a member of that user group.
-if (system('/usr/sbin/usermod','-G',$safeusername,'www')) {
-    print "Error. Could not make www a member of the group \"$safeusername\".\n" unless $noprint;
+my $groups=`/usr/bin/groups www` or &Exit(6);
+# untaint
+my ($safegroups)=($groups=~/:\s*([\s\w]+)/);
+$groups=$safegroups;
+chomp $groups; $groups=~s/^\S+\s+\:\s+//;
+my @grouplist=split(/\s+/,$groups);
+my @ugrouplist=grep {!/www|$safeusername/} @grouplist;
+my $gl=join(',',(@ugrouplist,$safeusername));
+print "Putting www in user's group\n" unless $noprint;
+if (system('/usr/sbin/usermod','-G',$gl,'www')) {
+    print "Error. Could not make www a member of the group ".
+	  "\"$safeusername\".\n" unless $noprint;
     unlink('/tmp/lock_lcpasswd');
-    exit 6;
+    &Exit(6);
 }
 
-# Set password with lcpasswd-style algorithm (which creates smbpasswd entry).
-# I cannot place a direct shell call to lcpasswd since I have to allow
-# for crazy characters in the password, and the setuid environment of perl
-# requires me to make everything safe.
-
-# Grab the line corresponding to username
-open (IN, "</etc/passwd");
-@lines=<IN>;
-close IN;
-my ($userid,$useroldcryptpwd);
-my @F; my @U;
-for my $l (@lines) {
-    @F=split(/\:/,$l);
-    if ($F[0] eq $username) {($userid,$useroldcryptpwd)=($F[2],$F[1]); @U=@F;}
-}
-
-# Verify existence of user
-if (!defined($userid)) {
-    print "Error. User $username does not exist.\n" unless $noprint;
-    unlink('/tmp/lock_lcpasswd');
-    exit 5;
+# ---------------------------------------------------------------- Set password
+# Set password with lcpasswd (which creates smbpasswd entry).
+
+unlink('/tmp/lock_lcpasswd');
+&disable_root_capability;
+($>,$<)=($wwwid,$wwwid);
+print "Opening lcpasswd pipeline\n" unless $noprint;
+open OUT,"|/home/httpd/perl/lcpasswd";
+print OUT $safeusername;
+print OUT "\n";
+print OUT $password1;
+print OUT "\n";
+print OUT $password1;
+print OUT "\n";
+close OUT;
+if ($?) {
+    print "abnormal Exit from close lcpasswd\n" unless $noprint;
+    &Exit(8);
 }
+($>,$<)=($wwwid,0);
+&enable_root_capability;
 
-# Construct new password entry (random salt)
-my $newcryptpwd=crypt($newpwd,(join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]));
-$U[1]=$newcryptpwd;
-my $userline=join(':',@U);
-my $rootid=&enable_root_capability;
-if ($rootid!=0) {
-    print "Error.  Root was not successfully enabled.\n" unless $noprint;
-    unlink('/tmp/lock_lcpasswd');
-    exit 7;
+# Check if home directory exists for user
+# If not, create one.
+if (!-e "/home/$safeusername") {
+    if (!mkdir("/home/$safeusername",0710)) {
+        print "Error. Could not add home directory for ".
+          "\"$safeusername\".\n" unless $noprint;
+        unlink('/tmp/lock_lcpasswd');
+        &Exit(15);
+    }
 }
-open PASSWORDFILE, '>/etc/passwd' or (print("Error.  Cannot open /etc/passwd.\n") && unlink('/tmp/lock_lcpasswd') && exit(8));
-for my $l (@lines) {
-    @F=split(/\:/,$l);
-    if ($F[0] eq $username) {print PASSWORDFILE "$userline\n";}
-    else {print PASSWORDFILE "$l\n";}
-}
-close PASSWORDFILE;
-
-($>,$<)=(0,0); # fool smbpasswd here to think this is not a setuid environment
-unless (-e '/etc/smbpasswd') {
-    open (OUT,'>/etc/smbpasswd'); close OUT;
-}
-my $smbexist=0;
-open (IN, '</etc/smbpasswd');
-my @lines=<IN>;
-close IN;
-for my $l (@lines) {
-    chop $l;
-    my @F=split(/\:/,$l);
-    if ($F[0] eq $username) {$smbexist=1;}
-}
-unless ($smbexist) {
-    open(OUT,'>>/etc/smbpasswd');
-    print OUT join(':',($safeusername,$userid,'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX','','/home/'.$safeusername,'/bin/bash')) . "\n";
-    close OUT;
-}
-open(OUT,"|/usr/bin/smbpasswd -s $safeusername>/dev/null");
-print OUT $newpwd; print OUT "\n";
-print OUT $newpwd; print OUT "\n";
-close OUT;
-$<=$wwwid; # unfool the program
 
-# Make final modifications to the user directory.
-# Add a public_html file with a stand-in index.html file.
+# ------------------------------ Make final modifications to the user directory
+# -- Add a construction space
+        
+my $path="/home/httpd/html/priv/".$domain;
+unless (-e $path) {
+   mkdir($path);
+}
+unless (-e $path.'/'.$safeusername) {
+   mkdir($path.'/'.$safeusername);
+}
 
-system('/bin/chmod','-R','0640',"/home/$safeusername");
-system('/bin/chmod','0750',"/home/$safeusername");
-mkdir "/home/$safeusername/public_html",0750;
-open OUT,">/home/$safeusername/public_html";
-print OUT<<END;
-<HTML>
-<HEAD>
-<TITLE>$safeusername</TITLE>
-</HEAD>
-<BODY>
-<H1>$safeusername</H1>
-<P>
-Learning Online Network
-</P>
-<P>
-This area provides for:
-</P>
-<UL>
-<LI>resource construction
-<LI>resource publication
-<LI>record-keeping
-</UL>
-</BODY>
-</HTML>
-END
-close OUT;
 
+# ---------------------------------------------------- Gracefull Apache Restart
+my $pidfile;
+if (-e '/var/run/httpd.pid') {
+    $pidfile = '/var/run/httpd.pid';
+} elsif (-e '/var/run/httpd2.pid') {   #Apache 2 on SuSE 10.1 and SLES10 
+    $pidfile = '/var/run/httpd2.pid';
+} 
+
+if ($pidfile) {
+    print "lcuseradd Apache restart\n" unless $noprint;
+    open(PID,"<$pidfile");
+    my $pid=<PID>;
+    close(PID);
+    $pid=~ /(\D+)/;
+    my $safepid = $1;
+    if ($pid) {
+	system('kill','-USR1',"$safepid");
+    }
+}
+# -------------------------------------------------------- Exit script
+print "lcuseradd Exiting\n" unless $noprint;
 &disable_root_capability;
-unlink('/tmp/lock_lcpasswd');
-exit 0;
+&Exit(0);
 
-# ----------------------------------------------------------- have setuid script run as root
+# ---------------------------------------------- Have setuid script run as root
 sub enable_root_capability {
     if ($wwwid==$>) {
-	($<,$>)=($>,$<);
-	($(,$))=($),$();
-    }
-    else {
+	($<,$>)=($>,0);
+	($(,$))=($),0);
+    } else {
 	# root capability is already enabled
     }
+    if ($wwwid == $>) {
+	print("Failed to become root\n") unless $noprint;
+    } else {
+	print("Became root\n") unless $noprint;
+    }
     return $>;
 }
 
-# ----------------------------------------------------------- have setuid script run as www
+# ----------------------------------------------- Have setuid script run as www
 sub disable_root_capability {
     if ($wwwid==$<) {
 	($<,$>)=($>,$<);
 	($(,$))=($),$();
-    }
-    else {
+    } else {
 	# root capability is already disabled
     }
 }
 
-# ----------------------------------- make sure that another lcpasswd process isn't running
+# ----------------------- Make sure that another lcpasswd process isn't running
 sub try_to_lock {
     my ($lockfile)=@_;
     my $currentpid;
@@ -291,8 +405,7 @@ sub try_to_lock {
 	    }
 	    sleep 3;
 	    $lastpid=$currentpid;
-	}
-	else {
+	} else {
 	    last;
 	}
 	if ($_==10) {
@@ -304,3 +417,70 @@ sub try_to_lock {
     close LOCK;
     return 1;
 }
+#    Called by File::Find::find for each file examined.
+#
+#     Untaint the file and, if it is a directory,
+#     chmod it to 02770
+#
+sub set_permission {
+    $File::Find::name =~ /^(.*)$/;
+    my $safe_name = $1;		# Untainted filename...
+    
+    print "$safe_name" unless $noprint;
+    if(-d $safe_name) {
+	print " - directory" unless $noprint;
+	chmod(02770, $safe_name);
+    }
+    print "\n" unless $noprint;
+
+}
+
+#-------------------------- Exit...
+#
+#   Write the file if the error_file is defined.  Regardless
+#   Exit with the status code.
+#
+sub Exit {
+    my ($code) = @_;		# Status code.
+
+    # TODO: Ensure the error file is owned/deletable by www:www:
+
+    &disable_root_capability();	# We run unprivileged to write the error file.
+
+    print "Exiting with status $code error file is $error_file\n" unless $noprint;
+    if($error_file) {
+	open(FH, ">$error_file");
+	print FH  "$code\n";
+	close(FH);
+    }
+    exit $code;
+}
+
+=head1 NAME
+
+lcuseradd - LON-CAPA setuid script to coordinate all actions
+            with adding a user with filesystem privileges (e.g. author)
+
+=head1 DESCRIPTION
+
+lcuseradd - LON-CAPA setuid script to coordinate all actions
+            with adding a user with filesystem privileges (e.g. author)
+
+=head1 README
+
+lcuseradd - LON-CAPA setuid script to coordinate all actions
+            with adding a user with filesystem privileges (e.g. author)
+
+=head1 PREREQUISITES
+
+=head1 COREQUISITES
+
+=pod OSNAMES
+
+linux
+
+=pod SCRIPT CATEGORIES
+
+LONCAPA/Administrative
+
+=cut