--- loncom/Attic/lcuseradd	2000/10/29 22:50:56	1.5
+++ loncom/Attic/lcuseradd	2013/08/10 01:15:13	1.43
@@ -1,145 +1,375 @@
 #!/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.43 2013/08/10 01:15:13 raeburn 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
-
-# Command-line arguments [USERNAME] [PASSWORD] [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).
+# These characters are:
+# 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).
 
+# ---------------------------------- 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;
+print "Safe username = $safeusername \n" unless $noprint;
 
-if ($password1 ne $password2) {
-    print "Error. Password mismatch.\n";
+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 7;
+    &Exit(9);
+}
+my $pbad=0;
+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" unless $noprint;
+    unlink('/tmp/lock_lcpasswd');
+    &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 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(11);
+}
+
+
+
+# -- 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(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";
+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 5;
+    &Exit(12);
 }
-if (system('/usr/sbin/usermod','-G',$safeusername,'www')) {
-    print "Error. Could not make www a member of the group \"$safeusername\".\n";
+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 6;
+    &Exit(12);
 }
 
-# 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.
+print "Done adding user\n" unless $noprint;
+# Make www a member of that user group.
+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);
+}
 
+# ---------------------------------------------------------------- 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;
+
+# 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);
+    }
+}
 
-# ----------------------------------------------------------- have setuid script run as root
+# ---------------------------------------------------- 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;
+&Exit(0);
+
+# ---------------------------------------------- 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;
@@ -163,8 +393,7 @@ sub try_to_lock {
 	    }
 	    sleep 3;
 	    $lastpid=$currentpid;
-	}
-	else {
+	} else {
 	    last;
 	}
 	if ($_==10) {
@@ -176,3 +405,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