3 # Copyright 1995, 1996, 1997 Guy Helmer, Ames, Iowa 50014.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer as
11 # the first lines of this file unmodified.
12 # 2. Redistributions in binary form must reproduce the above copyright
13 # notice, this list of conditions and the following disclaimer in the
14 # documentation and/or other materials provided with the distribution.
15 # 3. The name of the author may not be used to endorse or promote products
16 # derived from this software without specific prior written permission.
18 # THIS SOFTWARE IS PROVIDED BY GUY HELMER ``AS IS'' AND ANY EXPRESS OR
19 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 # IN NO EVENT SHALL GUY HELMER BE LIABLE FOR ANY DIRECT, INDIRECT,
22 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 # rmuser - Perl script to remove users
31 # Guy Helmer <ghelmer@cs.iastate.edu>, 02/23/97
41 $ENV{"PATH"} = "/bin:/sbin:/usr/bin:/usr/sbin";
44 $passwd_file = "/etc/master.passwd";
45 $new_passwd_file = "${passwd_file}.new.$$";
46 $group_file = "/etc/group";
47 $new_group_file = "${group_file}.new.$$";
48 $mail_dir = "/var/mail";
49 $crontab_dir = "/var/cron/tabs";
50 $atjob_dir = "/var/at/jobs";
58 print STDERR "Caught signal SIG$sig -- cleaning up.\n";
60 if (-e $new_passwd_file) {
61 unlink $new_passwd_file;
67 # Open the password file for reading
68 if (!open(MASTER_PW, "$passwd_file")) {
69 print STDERR "${whoami}: Error: Couldn't open ${passwd_file}: $!\n";
72 # Set the close-on-exec flag just in case
73 fcntl(MASTER_PW, &F_SETFD, 1);
74 # Apply an advisory lock the password file
75 if (!flock(MASTER_PW, &LOCK_EX|&LOCK_NB)) {
76 print STDERR "${whoami}: Error: Couldn't lock ${passwd_file}: $!\n";
82 flock(MASTER_PW, &LOCK_UN);
85 $SIG{'INT'} = 'cleanup';
86 $SIG{'QUIT'} = 'cleanup';
87 $SIG{'HUP'} = 'cleanup';
88 $SIG{'TERM'} = 'cleanup';
90 if ($#ARGV == 1 && $ARGV[0] eq '-y') {
96 print STDERR "usage: ${whoami} [-y] [username]\n";
101 print STDERR "${whoami}: Error: you must be root to use ${whoami}\n";
108 # Username was given as a parameter
109 $login_name = pop(@ARGV);
112 print STDERR "${whoami}: Error: -y option given without username!\n";
116 # Get the user name from the user
117 $login_name = &get_login_name;
120 ($name, $password, $uid, $gid, $change, $class, $gecos, $home_dir, $shell) =
121 (getpwnam("$login_name"));
124 print STDERR "${whoami}: Error: User ${login_name} not in password database\n";
130 print "${whoami}: Error: I'd rather not remove a user with a uid of 0.\n";
136 print "Matching password entry:\n\n$name\:$password\:$uid\:$gid\:$class\:$change\:0\:$gecos\:$home_dir\:$shell\n\n";
138 $ans = &get_yn("Is this the entry you wish to remove? ");
141 print "${whoami}: Informational: User ${login_name} not removed.\n";
148 # Get owner of user's home directory; don't remove home dir if not
149 # owned by $login_name
151 $remove_directory = 1;
154 $real_home_dir = &resolvelink($home_dir);
156 $real_home_dir = $home_dir;
160 # If home_dir is a symlink and points to something that isn't a directory,
161 # or if home_dir is not a symlink and is not a directory, don't remove
162 # home_dir -- seems like a good thing to do, but probably isn't necessary...
164 if (((-l $home_dir) && ((-e $real_home_dir) && !(-d $real_home_dir))) ||
165 (!(-l $home_dir) && !(-d $home_dir))) {
166 print STDERR "${whoami}: Informational: Home ${home_dir} is not a directory, so it won't be removed\n";
167 $remove_directory = 0;
170 if (length($real_home_dir) && -d $real_home_dir) {
171 $dir_owner = (stat($real_home_dir))[4]; # UID
172 if ($dir_owner != $uid) {
173 print STDERR "${whoami}: Informational: Home dir ${real_home_dir} is" .
174 " not owned by ${login_name} (uid ${dir_owner})\n," .
175 "\tso it won't be removed\n";
176 $remove_directory = 0;
180 if ($remove_directory && ! $affirm) {
181 $ans = &get_yn("Remove user's home directory ($home_dir)? ");
183 $remove_directory = 0;
190 # Remove the user's crontab, if there is one
191 # (probably needs to be done before password databases are updated)
193 if (-e "$crontab_dir/$login_name") {
194 print STDERR "Removing user's crontab:";
195 system('/usr/bin/crontab', '-u', $login_name, '-r');
196 print STDERR " done.\n";
200 # Remove the user's at jobs, if any
201 # (probably also needs to be done before password databases are updated)
203 &remove_at_jobs($login_name, $uid);
206 # Kill all the user's processes
208 &kill_users_processes($login_name, $uid);
211 # Copy master password file to new file less removed user's entry
216 # Remove the user from all groups in /etc/group
218 &update_group_file($login_name);
221 # Remove the user's home directory
223 if ($remove_directory) {
224 print STDERR "Removing user's home directory ($home_dir):";
225 &remove_dir($home_dir);
226 print STDERR " done.\n";
230 # Remove files related to the user from the mail directory
232 #&remove_files_from_dir($mail_dir, $login_name, $uid);
233 $file = "$mail_dir/$login_name";
234 if (-e $file || -l $file) {
235 print STDERR "Removing user's incoming mail file ${file}:";
237 print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
238 print STDERR " done.\n";
242 # Remove some pop daemon's leftover file
244 $file = "$mail_dir/.${login_name}.pop";
245 if (-e $file || -l $file) {
246 print STDERR "Removing pop daemon's temporary mail file ${file}:";
248 print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
249 print STDERR " done.\n";
253 # Remove files belonging to the user from the directories /tmp, /var/tmp,
254 # and /var/tmp/vi.recover. Note that this doesn't take care of the
255 # problem where a user may have directories or symbolic links in those
256 # directories -- only regular files are removed.
258 &remove_files_from_dir('/tmp', $login_name, $uid);
259 &remove_files_from_dir('/var/tmp', $login_name, $uid);
260 &remove_files_from_dir('/var/tmp/vi.recover', $login_name, $uid)
261 if (-e '/var/tmp/vi.recover');
270 # Get new user's name
271 local($done, $login_name);
273 for ($done = 0; ! $done; ) {
274 print "Enter login name for user to remove: ";
277 if (not getpwnam("$login_name")) {
278 print STDERR "Sorry, login name not in password database.\n";
284 print "User name is ${login_name}\n" if $debug;
290 # Get a yes or no answer; return 'Y' or 'N'
294 for ($done = 0; ! $done; ) {
299 if (!($ans =~ /^[YN]/)) {
300 print STDERR "Please answer (y)es or (n)o.\n";
306 return(substr($ans, 0, 1));
309 sub update_passwd_file {
312 print STDERR "Updating password file,";
313 seek(MASTER_PW, 0, 0);
314 open(NEW_PW, ">$new_passwd_file") ||
315 die "\n${whoami}: Error: Couldn't open file ${new_passwd_file}:\n $!\n";
316 chmod(0600, $new_passwd_file) ||
317 print STDERR "\n${whoami}: Warning: couldn't set mode of $new_passwd_file to 0600 ($!)\n\tcontinuing, but please check mode of /etc/master.passwd!\n";
319 while (<MASTER_PW>) {
320 if (not /^\Q$login_name:/io) {
323 print STDERR "Dropped entry for $login_name\n" if $debug;
328 seek(MASTER_PW, 0, 0);
331 print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n";
332 unlink($new_passwd_file) ||
333 print STDERR "\n${whoami}: Warning: couldn't unlink $new_passwd_file ($!)\n\tPlease investigate, as this file should not be left in the filesystem\n";
339 # Run pwd_mkdb to install the updated password files and databases
341 print STDERR " updating databases,";
342 system('/usr/sbin/pwd_mkdb', '-p', ${new_passwd_file});
343 print STDERR " done.\n";
345 close(MASTER_PW); # Not useful anymore
348 sub update_group_file {
349 local($login_name) = @_;
351 local($i, $j, $grmember_list, $new_grent, $changes);
352 local($grname, $grpass, $grgid, $grmember_list, @grmembers);
355 print STDERR "Updating group file:";
356 open(GROUP, $group_file) ||
357 die "\n${whoami}: Error: couldn't open ${group_file}: $!\n";
358 if (!flock(GROUP, &LOCK_EX|&LOCK_NB)) {
359 print STDERR "\n${whoami}: Error: couldn't lock ${group_file}: $!\n";
362 local($group_perms, $group_uid, $group_gid) =
363 (stat(GROUP))[2, 4, 5]; # File Mode, uid, gid
364 open(NEW_GROUP, ">$new_group_file") ||
365 die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n";
366 chmod($group_perms, $new_group_file) ||
367 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;
368 chown($group_uid, $group_gid, $new_group_file) ||
369 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";
370 while ($i = <GROUP>) {
371 if (!($i =~ /$login_name/)) {
372 # Line doesn't contain any references to the user, so just add it
377 # Remove the user from the group
381 ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i);
382 @grmembers = split(/,/, $grmember_list);
383 undef @new_grmembers;
384 local(@new_grmembers);
385 foreach $j (@grmembers) {
386 if ($j ne $login_name) {
387 push(@new_grmembers, $j);
389 print STDERR " $grname";
393 if ($grname eq $login_name && $#new_grmembers == -1) {
394 # Remove a user's personal group if empty
395 print STDERR " (removing group $grname -- personal group is empty)";
398 $grmember_list = join(',', @new_grmembers);
399 $new_grent = join(':', $grname, $grpass, $grgid, $grmember_list);
400 print NEW_GROUP "$new_grent\n";
405 rename($new_group_file, $group_file) || # Replace old group file with new
406 die "\n${whoami}: Error: couldn't rename $new_group_file to $group_file ($!)\n";
407 close(GROUP); # File handle is worthless now
408 print STDERR " (no changes)" if (! $changes);
409 print STDERR " done.\n";
413 # Remove the user's home directory
418 $linkdir = &resolvelink($dir);
419 # Remove the symbolic link
421 warn "${whoami}: Warning: could not unlink symlink $dir: $!\n";
422 if (!(-e $linkdir)) {
424 # Dangling symlink - just return now
427 # Set dir to be the resolved pathname
431 print STDERR "${whoami}: Warning: $dir is not a directory\n";
432 unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n";
435 system('/bin/rm', '-rf', $dir);
438 sub remove_files_from_dir {
439 local($dir, $login_name, $uid) = @_;
440 local($path, $i, $owner);
442 print STDERR "Removing files belonging to ${login_name} from ${dir}:";
444 if (!opendir(DELDIR, $dir)) {
445 print STDERR "\n${whoami}: Warning: couldn't open directory ${dir} ($!)\n";
448 while ($i = readdir(DELDIR)) {
452 $owner = (stat("$dir/$i"))[4]; # UID
453 if ($uid == $owner) {
457 print STDERR "\n${whoami}: Warning: unlink on ${dir}/${i} failed ($!) - continuing\n";
459 print STDERR " ($i not a regular file - skipped)";
465 printf STDERR " done.\n";
469 local($login_name, $uid) = @_;
470 local($i, $owner, $found);
473 opendir(ATDIR, $atjob_dir) || return;
474 while ($i = readdir(ATDIR)) {
477 next if $i eq '.lockfile';
479 $owner = (stat("$atjob_dir/$i"))[4]; # UID
480 if ($uid == $owner) {
482 print STDERR "Removing user's at jobs:";
485 # Use atrm to remove the job
487 system('/usr/bin/atrm', $i);
492 print STDERR " done.\n";
500 while (-l $path && -e $path) {
501 if (!defined($l = readlink($path))) {
502 die "${whoami}: readlink on $path failed (but it should have worked!): $!\n";
509 $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path
515 sub kill_users_processes {
516 local($login_name, $uid) = @_;
517 local($pid, $result);
520 # Do something a little complex: fork a child that changes its
521 # real and effective UID to that of the removed user, then issues
522 # a "kill(9, -1)" to kill all processes of the same uid as the sender
523 # (see kill(2) for details).
524 # The parent waits for the exit of the child and then returns.
529 } elsif (defined $pid) {
533 if ($< != $uid || $> != $uid) {
534 print STDERR "${whoami}: Error (kill_users_processes):\n" .
535 "\tCouldn't reset uid/euid to ${uid}: current uid/euid's are $< and $>\n";
538 $result = kill(9, -1);
539 print STDERR "Killed process(es) belonging to $login_name.\n"
544 print STDERR "${whoami}: Error: couldn't fork to kill ${login_name}'s processes - continuing\n";