1#!/usr/bin/perl 2# -*- perl -*- 3# 4# $OpenBSD: rmuser.perl,v 1.6 2002/05/31 19:47:00 millert Exp $ 5# 6# Copyright 1995, 1996 Guy Helmer, Madison, South Dakota 57042. 7# All rights reserved. 8# 9# Redistribution and use in source and binary forms, with or without 10# modification, are permitted provided that the following conditions 11# are met: 12# 1. Redistributions of source code must retain the above copyright 13# notice, this list of conditions and the following disclaimer as 14# the first lines of this file unmodified. 15# 2. Redistributions in binary form must reproduce the above copyright 16# notice, this list of conditions and the following disclaimer in the 17# documentation and/or other materials provided with the distribution. 18# 3. The name of the author may not be used to endorse or promote products 19# derived from this software without specific prior written permission. 20# 21# THIS SOFTWARE IS PROVIDED BY GUY HELMER ``AS IS'' AND ANY EXPRESS OR 22# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 23# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24# IN NO EVENT SHALL GUY HELMER BE LIABLE FOR ANY DIRECT, INDIRECT, 25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 26# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 30# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31# 32# rmuser - Perl script to remove users 33# 34# Guy Helmer <ghelmer@alpha.dsu.edu>, 07/17/96 35# 36# $From: rmuser.perl,v 1.2 1996/12/07 21:25:12 ache Exp $ 37 38use Fcntl qw(:DEFAULT :flock); 39 40$ENV{"PATH"} = "/bin:/sbin:/usr/bin:/usr/sbin"; 41umask(022); 42$whoami = $0; 43$passwd_file = "/etc/master.passwd"; 44$passwd_tmp = "/etc/ptmp"; 45$group_file = "/etc/group"; 46$new_group_file = "${group_file}.new.$$"; 47$mail_dir = "/var/mail"; 48$crontab_dir = "/var/cron/tabs"; 49$atjob_dir = "/var/at/jobs"; 50 51#$debug = 1; 52 53END { 54 if (-e $passwd_tmp && defined(fileno(NEW_PW))) { 55 unlink($passwd_tmp) || 56 warn "\n${whoami}: warning: couldn't unlink $passwd_tmp ($!)\n\tPlease investigate, as this file should not be left in the filesystem\n"; 57 } 58} 59 60sub cleanup { 61 local($sig) = @_; 62 63 print STDERR "Caught signal SIG$sig -- cleaning up.\n"; 64 exit(0); 65} 66 67sub open_files { 68 open(GROUP, $group_file) || 69 die "\n${whoami}: Error: couldn't open ${group_file}: $!\n"; 70 if (!flock(GROUP, LOCK_EX|LOCK_NB)) { 71 print STDERR "\n${whoami}: Error: couldn't lock ${group_file}: $!\n"; 72 exit 1; 73 } 74 75 sysopen(NEW_PW, $passwd_tmp, O_RDWR|O_CREAT|O_EXCL, 0600) || 76 die "\n${whoami}: Error: Password file busy\n"; 77 78 if (!open(MASTER_PW, $passwd_file)) { 79 print STDERR "${whoami}: Error: Couldn't open ${passwd_file}: $!\n"; 80 exit(1); 81 } 82} 83 84$SIG{'INT'} = 'cleanup'; 85$SIG{'QUIT'} = 'cleanup'; 86$SIG{'HUP'} = 'cleanup'; 87$SIG{'TERM'} = 'cleanup'; 88 89if ($#ARGV > 0) { 90 print STDERR "usage: ${whoami} [username]\n"; 91 exit(1); 92} 93 94if ($< != 0) { 95 print STDERR "${whoami}: Error: you must be root to use ${whoami}\n"; 96 exit(1); 97} 98 99&open_files; 100 101if ($#ARGV == 0) { 102 # Username was given as a parameter 103 $login_name = pop(@ARGV); 104} else { 105 # Get the user name from the user 106 $login_name = &get_login_name; 107} 108 109if (($pw_ent = &check_login_name($login_name)) eq '0') { 110 print STDERR "${whoami}: Error: User ${login_name} not in password database\n"; 111 exit 1; 112} 113 114($name, $password, $uid, $gid, $class, $change, $expire, $gecos, $home_dir, 115 $shell) = split(/:/, $pw_ent); 116 117if ($uid == 0) { 118 print "${whoami}: Sorry, I'd rather not remove a user with a uid of 0.\n"; 119 exit 1; 120} 121 122print "Matching password entry:\n\n$pw_ent\n\n"; 123 124$ans = &get_yn("Is this the entry you wish to remove? "); 125 126if ($ans eq 'N') { 127 print "User ${login_name} not removed.\n"; 128 exit 0; 129} 130 131# 132# Get owner of user's home directory; don't remove home dir if not 133# owned by $login_name 134 135$remove_directory = 1; 136 137if (-l $home_dir) { 138 $real_home_dir = &resolvelink($home_dir); 139} else { 140 $real_home_dir = $home_dir; 141} 142 143# 144# If home_dir is a symlink and points to something that isn't a directory, 145# or if home_dir is not a symlink and is not a directory, don't remove 146# home_dir -- seems like a good thing to do, but probably isn't necessary... 147if (((-l $home_dir) && ((-e $real_home_dir) && !(-d $real_home_dir))) || 148 (!(-l $home_dir) && !(-d $home_dir))) { 149 print STDERR "${whoami}: Home ${home_dir} is not a directory, so it won't be removed\n"; 150 $remove_directory = 0; 151} 152 153if (length($real_home_dir) && -d $real_home_dir) { 154 $dir_owner = (stat($real_home_dir))[4]; # UID 155 if ($dir_owner != $uid) { 156 print STDERR "${whoami}: Home dir ${real_home_dir} is not owned by ${login_name} (uid ${dir_owner})\n"; 157 $remove_directory = 0; 158 } 159} 160 161if ($remove_directory) { 162 $ans = &get_yn("Remove user's home directory ($home_dir)? "); 163 if ($ans eq 'N') { 164 $remove_directory = 0; 165 } 166} 167 168#exit 0 if $debug; 169 170# 171# Remove the user's crontab, if there is one 172# (probably needs to be done before password databases are updated) 173 174if (-e "$crontab_dir/$login_name") { 175 print STDERR "Removing user's crontab:"; 176 system('/usr/bin/crontab', '-u', $login_name, '-r'); 177 print STDERR " done.\n"; 178} 179 180# 181# Remove the user's at jobs, if any 182# (probably also needs to be done before password databases are updated) 183 184&remove_at_jobs($login_name, $uid); 185 186# 187# Copy master password file to new file less removed user's entry 188 189&update_passwd_file; 190 191# 192# Remove the user from all groups in /etc/group 193 194&update_group_file($login_name); 195 196# 197# Remove the user's home directory 198 199if ($remove_directory) { 200 print STDERR "Removing user's home directory ($home_dir):"; 201 &remove_dir($home_dir); 202 print STDERR " done.\n"; 203} 204 205# 206# Remove the user's incoming mail file 207 208if (-e "$mail_dir/$login_name" || -l "$mail_dir/$login_name") { 209 print STDERR "Removing user's incoming mail file ($mail_dir/$login_name):"; 210 unlink "$mail_dir/$login_name" || 211 print STDERR "\n${whoami}: warning: unlink on $mail_dir/$login_name failed ($!) - continuing\n"; 212 print STDERR " done.\n"; 213} 214 215# 216# All done! 217 218exit 0; 219 220sub get_login_name { 221 # 222 # Get new user's name 223 local($done, $login_name); 224 225 for ($done = 0; ! $done; ) { 226 print "Enter login name for user to remove: "; 227 $login_name = <>; 228 chomp $login_name; 229 if (!($login_name =~ /^\w+$/)) { 230 print STDERR "Sorry, login name must contain alphanumeric characters only.\n"; 231 } elsif (length($login_name) > 31 || length($login_name) == 0) { 232 print STDERR "Sorry, login name must be 31 characters or less.\n"; 233 } else { 234 $done = 1; 235 } 236 } 237 238 print "User name is ${login_name}\n" if $debug; 239 return($login_name); 240} 241 242sub check_login_name { 243 # 244 # Check to see whether login name is in password file 245 local($login_name) = @_; 246 local($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire, 247 $Mgecos, $Mhome_dir, $Mshell); 248 local($i); 249 250 seek(MASTER_PW, 0, 0); 251 while ($i = <MASTER_PW>) { 252 chomp $i; 253 ($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire, 254 $Mgecos, $Mhome_dir, $Mshell) = split(/:/, $i); 255 if ($Mname eq $login_name) { 256 seek(MASTER_PW, 0, 0); 257 return($i); # User is in password database 258 } 259 } 260 seek(MASTER_PW, 0, 0); 261 262 return '0'; # User wasn't found 263} 264 265sub get_yn { 266 # 267 # Get a yes or no answer; return 'Y' or 'N' 268 local($prompt) = @_; 269 local($done, $ans); 270 271 for ($done = 0; ! $done; ) { 272 print $prompt; 273 $ans = <>; 274 chomp $ans; 275 $ans =~ tr/a-z/A-Z/; 276 if (!($ans =~ /^[YN]/)) { 277 print STDERR "Please answer (y)es or (n)o.\n"; 278 } else { 279 $done = 1; 280 } 281 } 282 283 return(substr($ans, 0, 1)); 284} 285 286sub update_passwd_file { 287 local($skipped, $i); 288 289 print STDERR "Updating password file,"; 290 seek(MASTER_PW, 0, 0); 291 $skipped = 0; 292 while ($i = <MASTER_PW>) { 293 chomp($i); 294 if ($i ne $pw_ent) { 295 print NEW_PW "$i\n"; 296 } else { 297 print STDERR "Dropped entry for $login_name\n" if $debug; 298 $skipped = 1; 299 } 300 } 301 close(NEW_PW); 302 seek(MASTER_PW, 0, 0); 303 304 if ($skipped == 0) { 305 print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n"; 306 exit 1; 307 } 308 309 # 310 # Run pwd_mkdb to install the updated password files and databases 311 312 print STDERR " updating databases,"; 313 system('/usr/sbin/pwd_mkdb', '-p', ${passwd_tmp}); 314 print STDERR " done.\n"; 315 316 close(MASTER_PW); # Not useful anymore 317} 318 319sub update_group_file { 320 local($login_name) = @_; 321 322 local($i, $j, $grmember_list, $new_grent); 323 local($grname, $grpass, $grgid, $grmember_list, @grmembers); 324 325 print STDERR "Updating group file:"; 326 local($group_perms, $group_uid, $group_gid) = 327 (stat(GROUP))[2, 4, 5]; # File Mode, uid, gid 328 open(NEW_GROUP, ">$new_group_file") || 329 die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n"; 330 chmod($group_perms, $new_group_file) || 331 printf STDERR "\n${whoami}: warning: could not set permissions of new group file to %o ($!)\n\tContinuing, but please check permissions of $group_file!\n", $group_perms; 332 chown($group_uid, $group_gid, $new_group_file) || 333 print STDERR "\n${whoami}: warning: could not set owner/group of new group file to ${group_uid}/${group_gid} ($!)\n\rContinuing, but please check ownership of $group_file!\n"; 334 while ($i = <GROUP>) { 335 if (!($i =~ /$login_name/)) { 336 # Line doesn't contain any references to the user, so just add it 337 # to the new file 338 print NEW_GROUP $i; 339 } else { 340 # 341 # Remove the user from the group 342 chomp $i; 343 ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i); 344 @grmembers = split(/,/, $grmember_list); 345 undef @new_grmembers; 346 local(@new_grmembers); 347 foreach $j (@grmembers) { 348 if ($j ne $login_name) { 349 push(@new_grmembers, $j); 350 } elsif ($debug) { 351 print STDERR "Removing $login_name from group $grname\n"; 352 } 353 } 354 if ($grname eq $login_name && $#new_grmembers == -1) { 355 # Remove a user's personal group if empty 356 print STDERR "Removing group $grname -- personal group is empty\n"; 357 } else { 358 $grmember_list = join(',', @new_grmembers); 359 $new_grent = join(':', $grname, $grpass, $grgid, $grmember_list); 360 print NEW_GROUP "$new_grent\n"; 361 } 362 } 363 } 364 close(NEW_GROUP); 365 rename($new_group_file, $group_file) || # Replace old group file with new 366 die "\n${whoami}: error: couldn't rename $new_group_file to $group_file ($!)\n"; 367 close(GROUP); # File handle is worthless now 368 print STDERR " done.\n"; 369} 370 371sub remove_dir { 372 # Remove the user's home directory 373 local($dir) = @_; 374 local($linkdir); 375 376 if (-l $dir) { 377 $linkdir = &resolvelink($dir); 378 # Remove the symbolic link 379 unlink($dir) || 380 warn "${whoami}: Warning: could not unlink symlink $dir: $!\n"; 381 if (!(-e $linkdir)) { 382 # 383 # Dangling symlink - just return now 384 return; 385 } 386 # Set dir to be the resolved pathname 387 $dir = $linkdir; 388 } 389 if (!(-d $dir)) { 390 print STDERR "${whoami}: Warning: $dir is not a directory\n"; 391 unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n"; 392 return; 393 } 394 system('/bin/rm', '-rf', $dir); 395} 396 397sub remove_at_jobs { 398 local($login_name, $uid) = @_; 399 local($i, $owner, $found); 400 401 $found = 0; 402 opendir(ATDIR, $atjob_dir) || return; 403 while ($i = readdir(ATDIR)) { 404 next if $i eq '.'; 405 next if $i eq '..'; 406 next if $i eq '.lockfile'; 407 408 $owner = (stat("$atjob_dir/$i"))[4]; # UID 409 if ($uid == $owner) { 410 if (!$found) { 411 print STDERR "Removing user's at jobs:"; 412 $found = 1; 413 } 414 # Use atrm to remove the job 415 print STDERR " $i"; 416 system('/usr/bin/atrm', $i); 417 } 418 } 419 closedir(ATDIR); 420 if ($found) { 421 print STDERR " done.\n"; 422 } 423} 424 425sub resolvelink { 426 local($path) = @_; 427 local($l); 428 429 while (-l $path && -e $path) { 430 if (!defined($l = readlink($path))) { 431 die "${whoami}: readlink on $path failed (but it should have worked!): $!\n"; 432 } 433 if ($l =~ /^\//) { 434 # Absolute link 435 $path = $l; 436 } else { 437 # Relative link 438 $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path 439 } 440 } 441 return $path; 442} 443