#!/usr/bin/perl -w ################################################################################ # user addition program, modified for LDAP # by Danny Sauer, June 2000 # # parses standard "useradd" args: # usage: useradd [-u uid [-o]] [-g group] [-G group,...] # [-d home] [-s shell] [-c comment] [-m [-k template]] # [-f inactive] [-e expire ] [-p passwd] name # and behaves similarly (I hope...) :) # except that I've added the -C option to specify students in # the CSC program / CSC classes (no argument to -C) # and the -L and -S options to DISable adding to the (-L) LDAP database # or the (-S) SYStem databases. -v adds some verbosity... # # revision 1.0 - 06/15/2000 # first public release, apears to work OK. # remember to add "help" display sometime if no args (or -h) # revision 1.1 - 8/03/2000 # fixed user directory creation, # fixed permissions on created directories # revision 1.2 - 08/07/2000 # added the 'S' and 'L' options to getopts (whoops) # fixed the case where '-p' is not supplied (I think) # revision 1.3 - 09/10/2000 # added the '-v' option to print more detailed info # exit with status 2 if -S and -L specified (although that's not # really an error, it's just dumb) ################################################################################ use Net::LDAP; use Getopt::Std; $definactive=7; $defexpire=''; $defhomebase = "/home/students"; $defloginshell = "/bin/bash"; $defgroup = "students"; $minchange=0; $maxchange=99999; $warnexpire=14; $hmode="0755"; # home directories (use quotes) $pmode=0755; # parent of home directories (don't use quotes) $minuid = 1000; $maxuid = 65534; # change this $defskeldir = "/etc/skel"; $passwdfile = "/etc/passwd"; $shadowfile = "/etc/shadow"; $groupfile = "/etc/group"; $basedn = "dc=parkland,dc=cc,dc=il,dc=us"; $maildomain = "stu.parkland.cc.il.us"; $ou = "People"; # this should be OK, usually... @objectclass = [ 'person', 'organizationalPerson', 'inetOrgPerson', 'account', 'posixAccount', 'top', 'shadowAccount' ]; $LDAPserver = "csc.parkland.cc.il.us"; # dn and passwd to bind to LDAP server with $LDAPbinddn = "cn=manager,$basedn"; $LDAPpasswd = "password"; ################################################################################ # end configuration params / begin code 'n stuff ################################################################################ showusage() unless($ARGV); $allow2uid = 0; $createhome = 0; $LDAPupdate = 1; $SYSupdate = 1; $nextuid = findnextuid(); $lastchange = int(time()/(60*60*24)); #num days since 1970 ################################################################################ # parse arguments ################################################################################ $username = $ARGV[-1] || die "no username!"; #last arg had better be a username... getopts("og:G:d:s:c:mk:f:e:p:u:CSLv", \%options); # -u uid $uid = $options{'u'} ? $options{'u'} : $nextuid; # -o (allow repeated uid) $allow2uid++ if($options{'o'}); # -g primarygroup # maybe set this to auto-detect gid or grnam? $gid = $options{'g'} ? getgrnam($options{'g'}) : getgrnam($defgroup); # -G 'other,groups,to,add,user' @othergroups = split(/,/, $options{'G'}) if($options{'G'}); # -d /home/directory $homedirectory = $options{'d'} ? $options{'d'} : "$defhomebase/$username"; # -s /shell/path $loginshell = $options{'s'} ? $options{'s'} : $defloginshell; # -c "Full Name" $fullname = $options{'c'} || die "No Fullname Specified"; ($firstname, $lastname) = split(' ', $options{'c'}, 2); # -m make home? $createhome++ if($options{'m'}); # -k skel files location $skeldir = $options{'k'} ? $options{'k'} : $defskeldir; # -f inactive $inactive = $options{'f'} ? $options{'f'} : $definactive; # -e expire if($options{'e'}){ die "Expiration date setting does not yet work. :(\n"; } $expire = $options{'e'} ? $options{'e'} : $defexpire; # -p password $password = $options{'p'} ? $options{'p'} : '!'; push(@objectclass, 'cscperson') if($options{'C'}); undef $SYSupdate if($options{'S'}); undef $LDAPupdate if ($options{'L'}); exit(2) unless(defined($LDAPupdate) or defined($SYSupdate)); ################################################################################ # done with arguments... ################################################################################ exit(1) unless(checkvals()); # checkvals returns 1 on success printverify() if($options{'v'}); LDAPadd() if($LDAPupdate); systemadd() if($SYSupdate); mkhome() if($createhome); exit(0); ################################################################################ # end main code / begin subroutines... ################################################################################ ################################################################################ # checkvals # check values for validity # values are global vars that should exist BEFORE this function call (ie, # after parsing the command line and setting defaults) # returns false (0) if it finds a value that's not valid, # returns true (1) otherwise. ################################################################################ sub checkvals{ # I really can't use defaults for these... die "no uid" unless($uid); die "no username" unless($username); die "no fullname" unless($fullname); $ldap = Net::LDAP->new("$LDAPserver") || die "can't connect to $LDAPserver!: $@"; $ldap->bind || die "can't anonymously bind to $LDAPserver!: $!"; # check LDAP for username $mesg = $ldap->search(base => $basedn, filter => "(uid=$username)"); die "$username already exists in LDAP" if($mesg->count != 0); # check LDAP for uid $mesg = $ldap->search(base => $basedn, filter => "(uidnumber=$uid)"); die "$uid already exists in LDAP" if($mesg->count != 0); $ldap->unbind; # system checks shouldn't be needed, but I guess I'll do it anyway # check system for username die "$username already exists in $passwdfile" if(getpwnam($username)); # check system for uid die "$uid already exists in $passwdfile" if(getpwuid($uid)); return 1; } ################################################################################ # LDAPadd # add user to LDAP database # uses settings as defined above, and uses the scheme that padl's # MigrationTools with ExtendedSchema=1 do (approximately). # http://www.padl.com/migrationtools.html or something like that... # errors are assumed to have already been checked, I hope. :) # Shoultd I add a "created by" and "created on" attribute? ################################################################################ sub LDAPadd{ $ldap = Net::LDAP->new($LDAPserver) || die "Can't connect to $LDAPserver!: $!"; $ldap->bind(dn => $LDAPbinddn, password => $LDAPpasswd) || die "Aw, crap. $!"; $result = $ldap->add(dn => "uid=$username,ou=$ou,$basedn", attrs => [ 'uid' => $username, 'cn' => $fullname, 'givenname' => $firstname, 'sn' => $lastname, 'mail' => "$username\@$maildomain", 'objectclass' => @objectclass, 'shadowlastchange' => $lastchange, # fix this, damn it 'shadowmax' => $maxchange, 'shadowwarning' => $inactive, # this too 'loginshell' => $loginshell, 'uidnumber' => $uid, 'gidnumber' => $gid, 'homedirectory' => $homedirectory, 'gecos' => $fullname, 'userPassword' => "{crypt}$password" ] ); $result->code && warn "failed to add entry: ", $result->error ; # add to supplemental groups too? foreach $newgroup (@othergroups){ $result = $ldap->modify( dn => "cn=$newgroup,ou=Group,$basedn", add => { 'memberuid' => $username }); $result->code && warn "failed to update grp: ", $result->error ; } } ################################################################################ # systemadd # add to system databases (/etc/passwd + /etc/shadow) # I guess I *could* use "useradd", but I'd rather just stick my lines into # the files myself. Since I'm a replacement for useradd, I don't really # wanna depend on useradd itself existing. ;) # # I need to make a temporary passwd file named $passwdfile.tmp. This is # because on NIs systems, there are lines with "+" that need to be at the # end of the passwd file, so I need to insert in there. # # See also shadow(5) and password(5) ################################################################################ sub systemadd{ $passwdline = "$username:x:$uid:$gid:$fullname:$homedirectory:$loginshell"; $shadowline = "$username:$password:$lastchange:$minchange"; $shadowline .= ":$maxchange:$warnexpire:$inactive:$expire:"; open(PASSWD, "$passwdfile") || die "Can't open $passwdfile: $!"; open(TEMPPASSWD, ">$passwdfile.tmp") || die "Can't open $passwdfile.tmp: $!"; select(TEMPPASSWD); #while( !~ /^\+.*$/){ print; }; print $passwdline , "\n"; while(){ print; }; select(STDOUT); close(TEMPPASSWD); close(PASSWD); rename($passwdfile, "$passwdfile.backup"); rename("$passwdfile.tmp", $passwdfile); open(SHADOW, ">>$shadowfile") || die "Can't open $shadowfile: $!"; print SHADOW $shadowline . "\n"; close(SHADOW); # append to group line? foreach $grp (@othergroups){ open(GROUPS, $groupfile) || die "Can't open $groupfile: $!"; open(TEMPGROUP, ">$groupfile.tmp") || die "Can't open $groupfile.tmp: $!"; select(TEMPGROUP); while(){ chomp; print; if(index($_, "$grp:") != -1){ print ',' if(substr($_,-1,1) ne ':'); print $username; } print "\n"; } rename("$groupfile.tmp",$groupfile); } } ################################################################################ # findnextuid # locates the lowest available uid that is greater than $minuid but less than # $maxuid by checking the system database first, then LDAP ################################################################################ sub findnextuid{ $min = $maxuid; # find lowest uid in systemdb for($i=$minuid;$i<$maxuid;$i++){ if(!getpwuid($i)){ $min = $i; $i = $maxuid; } } # find lowest uid (greater than lowest system uid) in LDAP $ldap = Net::LDAP->new("$LDAPserver") || die "can't connect to $LDAPserver!: $@"; $ldap->bind || die "can't anonymously bind to $LDAPserver!: $!"; while($ldap->search(base=>$basedn, filter=>"(uidnumber=$min)")->count != 0){ $min++; } # we now have the lowest free uid... return $min; } ################################################################################ # printverify # prints values about to be added ################################################################################ sub printverify{ print "adding: 'uid' = $uid 'cn' = $fullname 'givenname' = $firstname 'sn' = $lastname 'mail' = $username\@$maildomain 'objectclass' = @objectclass 'shadowlastchange' = $lastchange 'shadowmax' = $maxchange 'shadowwarning' = $inactive 'loginshell' = $loginshell 'uidnumber' = $uid 'homedirectory' = $homedirectory 'gecos' = $fullname 'gidnumber' = $gid 'allow2uid' = $allow2uid 'nextuid' = $nextuid 'password' = $password 'group' = $gid \n"; if($SYSupdate){ print "to system files"; print " and to LDAP" if($LDAPupdate); }elsif($LDAPupdate){ print "to LDAP"; } print "\n"; } ################################################################################ # mkhome() # create user home directory if needed, copy skeleton files, and fix # permissions on copied files # uses $homebase in the case of adding a usaer ################################################################################ sub mkhome{ umask(0022); $homebase = substr($homedirectory,0,rindex($homedirectory,'/')); $temppath = ""; foreach $pathpart (split(/\//,$homebase)){ next unless($pathpart); $temppath .= "/$pathpart"; if (mkdir($temppath, $pmode)){ chown(0,$gid,$temppath); chmod($pmode, $temppath); #mkdir doesn't seem to do it. } } system("cp -a $skeldir $homedirectory"); system("chown -R $uid.$gid $homedirectory"); system("chmod -R $hmode $homedirectory"); } ################################################################################ # showusage() # print out the usage message, as close to stock useradd as possible ################################################################################ sub showusage{ print "usage: useradd [-v] [-u uid [-o]] [-g group] [-G group,...] [-L] [-S] [-d home] [-s shell] [-c comment] [-m [-k template]] [-f inactive] [-e expire ] [-p passwd] [-C] name\n" exit(1); }