1#!/usr/bin/perl 2# -*- perl -*- 3# 4# $OpenBSD: rmuser.perl,v 1.7 2005/06/07 05:07:54 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($login_name); 224 225 print "Enter login name for user to remove: "; 226 $login_name = <>; 227 chomp $login_name; 228 229 print "User name is ${login_name}\n" if $debug; 230 return($login_name); 231} 232 233sub check_login_name { 234 # 235 # Check to see whether login name is in password file 236 local($login_name) = @_; 237 local($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire, 238 $Mgecos, $Mhome_dir, $Mshell); 239 local($i); 240 241 seek(MASTER_PW, 0, 0); 242 while ($i = <MASTER_PW>) { 243 chomp $i; 244 ($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire, 245 $Mgecos, $Mhome_dir, $Mshell) = split(/:/, $i); 246 if ($Mname eq $login_name) { 247 seek(MASTER_PW, 0, 0); 248 return($i); # User is in password database 249 } 250 } 251 seek(MASTER_PW, 0, 0); 252 253 return '0'; # User wasn't found 254} 255 256sub get_yn { 257 # 258 # Get a yes or no answer; return 'Y' or 'N' 259 local($prompt) = @_; 260 local($done, $ans); 261 262 for ($done = 0; ! $done; ) { 263 print $prompt; 264 $ans = <>; 265 chomp $ans; 266 $ans =~ tr/a-z/A-Z/; 267 if (!($ans =~ /^[YN]/)) { 268 print STDERR "Please answer (y)es or (n)o.\n"; 269 } else { 270 $done = 1; 271 } 272 } 273 274 return(substr($ans, 0, 1)); 275} 276 277sub update_passwd_file { 278 local($skipped, $i); 279 280 print STDERR "Updating password file,"; 281 seek(MASTER_PW, 0, 0); 282 $skipped = 0; 283 while ($i = <MASTER_PW>) { 284 chomp($i); 285 if ($i ne $pw_ent) { 286 print NEW_PW "$i\n"; 287 } else { 288 print STDERR "Dropped entry for $login_name\n" if $debug; 289 $skipped = 1; 290 } 291 } 292 close(NEW_PW); 293 seek(MASTER_PW, 0, 0); 294 295 if ($skipped == 0) { 296 print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n"; 297 exit 1; 298 } 299 300 # 301 # Run pwd_mkdb to install the updated password files and databases 302 303 print STDERR " updating databases,"; 304 system('/usr/sbin/pwd_mkdb', '-p', ${passwd_tmp}); 305 print STDERR " done.\n"; 306 307 close(MASTER_PW); # Not useful anymore 308} 309 310sub update_group_file { 311 local($login_name) = @_; 312 313 local($i, $j, $grmember_list, $new_grent); 314 local($grname, $grpass, $grgid, $grmember_list, @grmembers); 315 316 print STDERR "Updating group file:"; 317 local($group_perms, $group_uid, $group_gid) = 318 (stat(GROUP))[2, 4, 5]; # File Mode, uid, gid 319 open(NEW_GROUP, ">$new_group_file") || 320 die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n"; 321 chmod($group_perms, $new_group_file) || 322 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; 323 chown($group_uid, $group_gid, $new_group_file) || 324 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"; 325 while ($i = <GROUP>) { 326 if (!($i =~ /$login_name/)) { 327 # Line doesn't contain any references to the user, so just add it 328 # to the new file 329 print NEW_GROUP $i; 330 } else { 331 # 332 # Remove the user from the group 333 chomp $i; 334 ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i); 335 @grmembers = split(/,/, $grmember_list); 336 undef @new_grmembers; 337 local(@new_grmembers); 338 foreach $j (@grmembers) { 339 if ($j ne $login_name) { 340 push(@new_grmembers, $j); 341 } elsif ($debug) { 342 print STDERR "Removing $login_name from group $grname\n"; 343 } 344 } 345 if ($grname eq $login_name && $#new_grmembers == -1) { 346 # Remove a user's personal group if empty 347 print STDERR "Removing group $grname -- personal group is empty\n"; 348 } else { 349 $grmember_list = join(',', @new_grmembers); 350 $new_grent = join(':', $grname, $grpass, $grgid, $grmember_list); 351 print NEW_GROUP "$new_grent\n"; 352 } 353 } 354 } 355 close(NEW_GROUP); 356 rename($new_group_file, $group_file) || # Replace old group file with new 357 die "\n${whoami}: error: couldn't rename $new_group_file to $group_file ($!)\n"; 358 close(GROUP); # File handle is worthless now 359 print STDERR " done.\n"; 360} 361 362sub remove_dir { 363 # Remove the user's home directory 364 local($dir) = @_; 365 local($linkdir); 366 367 if (-l $dir) { 368 $linkdir = &resolvelink($dir); 369 # Remove the symbolic link 370 unlink($dir) || 371 warn "${whoami}: Warning: could not unlink symlink $dir: $!\n"; 372 if (!(-e $linkdir)) { 373 # 374 # Dangling symlink - just return now 375 return; 376 } 377 # Set dir to be the resolved pathname 378 $dir = $linkdir; 379 } 380 if (!(-d $dir)) { 381 print STDERR "${whoami}: Warning: $dir is not a directory\n"; 382 unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n"; 383 return; 384 } 385 system('/bin/rm', '-rf', $dir); 386} 387 388sub remove_at_jobs { 389 local($login_name, $uid) = @_; 390 local($i, $owner, $found); 391 392 $found = 0; 393 opendir(ATDIR, $atjob_dir) || return; 394 while ($i = readdir(ATDIR)) { 395 next if $i eq '.'; 396 next if $i eq '..'; 397 next if $i eq '.lockfile'; 398 399 $owner = (stat("$atjob_dir/$i"))[4]; # UID 400 if ($uid == $owner) { 401 if (!$found) { 402 print STDERR "Removing user's at jobs:"; 403 $found = 1; 404 } 405 # Use atrm to remove the job 406 print STDERR " $i"; 407 system('/usr/bin/atrm', $i); 408 } 409 } 410 closedir(ATDIR); 411 if ($found) { 412 print STDERR " done.\n"; 413 } 414} 415 416sub resolvelink { 417 local($path) = @_; 418 local($l); 419 420 while (-l $path && -e $path) { 421 if (!defined($l = readlink($path))) { 422 die "${whoami}: readlink on $path failed (but it should have worked!): $!\n"; 423 } 424 if ($l =~ /^\//) { 425 # Absolute link 426 $path = $l; 427 } else { 428 # Relative link 429 $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path 430 } 431 } 432 return $path; 433} 434