-.\" Copyright 1995, 1996
-.\" Guy Helmer, Madison, South Dakota 57042. All rights reserved.
+.\" Copyright 1995, 1996, 1997
+.\" Guy Helmer, Ames, Iowa 50014. All rights reserved.
.\"
.\" Redistribution and use in source and binary forms, with or without
.\" modification, are permitted provided that the following conditions
.\"
.\" $Id$
.\"
-.Dd July 16, 1996
+.Dd February 23, 1997
.Dt RMUSER 8
.Os
.Sh NAME
.Nm rmuser
-.Nd remove users from the system
+.Nd removes users from the system
.Sh SYNOPSIS
.Nm rmuser
+.Op Fl y
.Op Ar username
.Sh DESCRIPTION
-The
+The utility
.Nm rmuser
-utility removes a user's
+.Pp
+.Bl -enum
+.It
+Removes the user's
.Xr crontab 1
-entry (if any) and any
+entry (if any)
+.It
+Removes any
.Xr at 1
-jobs belonging to the user,
-then removes a user from the system's local password file, removes
-the user's home directory if it is owned by the user, and removes
-the user's incoming mail file if it exists. The username is removed
-from any groups to which it belongs in the file
+jobs belonging to the user
+.It
+Sends a SIGKILL signal to all processes owned by the user
+.It
+Removes the user from the system's local password file
+.It
+Removes the user's home directory (if it is owned by the user),
+including handling of symbolic links in the path to the actual home
+directory
+.It
+Removes the incoming mail and pop daemon mail files belonging to the
+user from
+.Pa /var/mail
+.It
+Removes all files owned by the user from
+.Pa /tmp ,
+.Pa /var/tmp ,
+and
+.Pa /var/tmp/vi.recover .
+.It
+Removes the username from all groups to which it belongs in
.Pa /etc/group .
-If a group becomes empty and the group name is the same as the username,
-the group is removed (this complements
+(If a group becomes empty and the group name is the same as the username,
+the group is removed; this complements
.Xr adduser 8 's
per-user unique groups).
+.El
.Pp
.Nm rmuser
politely refuses to remove users whose uid is 0 (typically root), since
-it seemed like a good idea at the time
-.Nm rmuser
-was written.
+certain actions (namely, killing all the user's processes, and perhaps
+removing the user's home directory) would cause damage to a running system.
+If it is necessary to remove a user whose uid is 0, see
+.Xr vipw 8
+for information on directly editing the password file, by which the desired
+user's
+.Xr passwd 5
+entry may be removed manually.
.Pp
+If not running "affirmitively" (i.e., option
+.Fl y
+is not specified),
.Nm rmuser
shows the selected user's password file entry and asks for confirmation
that you wish to remove the user. If the user's home directory is owned
-by the user (and not by any other user),
+by the user,
.Nm rmuser
asks whether you wish to remove the user's home directory and everything
below.
.Pp
+As
+.Nm rmuser
+operates, it informs the user regarding the current activity. If any
+errors occur, they are posted to standard error and, if it is possible for
+.Nm rmuser
+to continue, it will.
+.Pp
Available options:
.Pp
.Bl -tag -width username
+.It Fl y
+Affirm - any question that would be asked is answered implicitly in
+the affirmative (i.e., yes). A username must also be specified on the
+command line if this option is used.
.It Ar \&username
Identifies the user to be removed; if not present,
.Nm rmuser
.Sh HISTORY
The
.Nm
-command appeared in
-.Fx 2.1.5 .
+command appeared in
+.Fx 2.2 .
.\" .Sh AUTHOR
-.\" Guy Helmer, Madison, South Dakota
+.\" Guy Helmer, Ames, Iowa
+.Sh BUGS
+.Nm rmuser
+does not comprehensively search the filesystem for all files
+owned by the removed user and remove them; to do so on a system
+of any size is prohibitively slow and I/O intensive.
+.Nm rmuser
+also is unable to remove symbolic links that were created by the
+user in
+.Pa /tmp
+or
+.Pa /var/tmp
+as symbolic links on 4.4BSD filesystems do not contain information
+as to who created them. Also, there may be other files created in
+.Pa /var/mail
+other than
+.Pa /var/mail/username
+and
+.Pa /var/mail/.pop.username
+that are not owned by the removed user but should be removed.
+.Pp
+.Nm rmuser
+has no knowledge of NIS (Yellow Pages), and it operates only on the
+local password file.
#!/usr/bin/perl
# -*- perl -*-
-# Copyright 1995, 1996 Guy Helmer, Madison, South Dakota 57042.
+# Copyright 1995, 1996, 1997 Guy Helmer, Ames, Iowa 50014.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
#
# rmuser - Perl script to remove users
#
-# Guy Helmer <ghelmer@alpha.dsu.edu>, 07/17/96
+# Guy Helmer <ghelmer@cs.iastate.edu>, 02/23/97
#
# $Id$
$mail_dir = "/var/mail";
$crontab_dir = "/var/cron/tabs";
$atjob_dir = "/var/at/jobs";
+$affirm = 0;
#$debug = 1;
fcntl(MASTER_PW, &F_SETFD, 1);
# Apply an advisory lock the password file
if (!flock(MASTER_PW, &LOCK_EX|&LOCK_NB)) {
- print STDERR "Couldn't lock ${passwd_file}: $!\n";
+ print STDERR "${whoami}: Error: Couldn't lock ${passwd_file}: $!\n";
exit(1);
}
}
$SIG{'HUP'} = 'cleanup';
$SIG{'TERM'} = 'cleanup';
+if ($#ARGV == 1 && $ARGV[0] eq '-y') {
+ shift @ARGV;
+ $affirm = 1;
+}
+
if ($#ARGV > 0) {
- print STDERR "usage: ${whoami} [username]\n";
+ print STDERR "usage: ${whoami} [-y] [username]\n";
exit(1);
}
die "Sorry, login name must contain alphanumeric characters only.\n"
if ($login_name !~ /^[a-z0-9_][a-z0-9_\-]*$/);
} else {
+ if ($affirm) {
+ print STDERR "${whoami}: Error: -y option given without username!\n";
+ &unlockpw;
+ exit 1;
+ }
# Get the user name from the user
$login_name = &get_login_name;
}
$shell) = split(/:/, $pw_ent);
if ($uid == 0) {
- print "${whoami}: Sorry, I'd rather not remove a user with a uid of 0.\n";
+ print "${whoami}: Error: I'd rather not remove a user with a uid of 0.\n";
&unlockpw;
exit 1;
}
-print "Matching password entry:\n\n$pw_ent\n\n";
+if (! $affirm) {
+ print "Matching password entry:\n\n$pw_ent\n\n";
-$ans = &get_yn("Is this the entry you wish to remove? ");
+ $ans = &get_yn("Is this the entry you wish to remove? ");
-if ($ans eq 'N') {
- print "User ${login_name} not removed.\n";
- &unlockpw;
- exit 0;
+ if ($ans eq 'N') {
+ print "${whoami}: Informational: User ${login_name} not removed.\n";
+ &unlockpw;
+ exit 0;
+ }
}
#
# If home_dir is a symlink and points to something that isn't a directory,
# or if home_dir is not a symlink and is not a directory, don't remove
# home_dir -- seems like a good thing to do, but probably isn't necessary...
+
if (((-l $home_dir) && ((-e $real_home_dir) && !(-d $real_home_dir))) ||
(!(-l $home_dir) && !(-d $home_dir))) {
- print STDERR "${whoami}: Home ${home_dir} is not a directory, so it won't be removed\n";
+ print STDERR "${whoami}: Informational: Home ${home_dir} is not a directory, so it won't be removed\n";
$remove_directory = 0;
}
if (length($real_home_dir) && -d $real_home_dir) {
$dir_owner = (stat($real_home_dir))[4]; # UID
if ($dir_owner != $uid) {
- print STDERR "${whoami}: Home dir ${real_home_dir} is not owned by ${login_name} (uid ${dir_owner})\n";
+ print STDERR "${whoami}: Informational: Home dir ${real_home_dir} is" .
+ " not owned by ${login_name} (uid ${dir_owner})\n," .
+ "\tso it won't be removed\n";
$remove_directory = 0;
}
}
-if ($remove_directory) {
+if ($remove_directory && ! $affirm) {
$ans = &get_yn("Remove user's home directory ($home_dir)? ");
if ($ans eq 'N') {
$remove_directory = 0;
&remove_at_jobs($login_name, $uid);
+#
+# Kill all the user's processes
+
+&kill_users_processes($login_name, $uid);
+
#
# Copy master password file to new file less removed user's entry
}
#
-# Remove the user's incoming mail file
+# Remove files related to the user from the mail directory
+
+#&remove_files_from_dir($mail_dir, $login_name, $uid);
+$file = "$mail_dir/$login_name";
+if (-e $file || -l $file) {
+ print STDERR "Removing user's incoming mail file ${file}:";
+ unlink $file ||
+ print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
+ print STDERR " done.\n";
+}
-if (-e "$mail_dir/$login_name" || -l "$mail_dir/$login_name") {
- print STDERR "Removing user's incoming mail file ($mail_dir/$login_name):";
- unlink "$mail_dir/$login_name" ||
- print STDERR "\n${whoami}: warning: unlink on $mail_dir/$login_name failed ($!) - continuing\n";
+#
+# Remove some pop daemon's leftover file
+
+$file = "$maildir/.${login_name}.pop";
+if (-e $file || -l $file) {
+ print STDERR "Removing pop daemon's temporary mail file ${file}:";
+ unlink $file ||
+ print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
print STDERR " done.\n";
}
+#
+# Remove files belonging to the user from the directories /tmp, /var/tmp,
+# and /var/tmp/vi.recover. Note that this doesn't take care of the
+# problem where a user may have directories or symbolic links in those
+# directories -- only regular files are removed.
+
+&remove_files_from_dir('/tmp', $login_name, $uid);
+&remove_files_from_dir('/var/tmp', $login_name, $uid);
+&remove_files_from_dir('/var/tmp/vi.recover', $login_name, $uid)
+ if (-e '/var/tmp/vi.recover');
+
#
# All done!
open(NEW_PW, ">$new_passwd_file") ||
die "\n${whoami}: Error: Couldn't open file ${new_passwd_file}:\n $!\n";
chmod(0600, $new_passwd_file) ||
- 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";
+ 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";
$skipped = 0;
while ($i = <MASTER_PW>) {
if ($i =~ /\n$/) {
if ($skipped == 0) {
print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n";
unlink($new_passwd_file) ||
- 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";
+ 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";
&unlockpw;
exit 1;
}
sub update_group_file {
local($login_name) = @_;
- local($i, $j, $grmember_list, $new_grent);
+ local($i, $j, $grmember_list, $new_grent, $changes);
local($grname, $grpass, $grgid, $grmember_list, @grmembers);
+ $changes = 0;
print STDERR "Updating group file:";
open(GROUP, $group_file) ||
die "\n${whoami}: Error: couldn't open ${group_file}: $!\n";
open(NEW_GROUP, ">$new_group_file") ||
die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n";
chmod($group_perms, $new_group_file) ||
- 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;
+ 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;
chown($group_uid, $group_gid, $new_group_file) ||
- 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";
+ 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";
while ($i = <GROUP>) {
if (!($i =~ /$login_name/)) {
# Line doesn't contain any references to the user, so just add it
local(@new_grmembers);
foreach $j (@grmembers) {
if ($j ne $login_name) {
- push(new_grmembers, $j);
- } elsif ($debug) {
- print STDERR "Removing $login_name from group $grname\n";
+ push(@new_grmembers, $j);
+ } else {
+ print STDERR " $grname";
+ $changes = 1;
}
}
if ($grname eq $login_name && $#new_grmembers == -1) {
# Remove a user's personal group if empty
- print STDERR "Removing group $grname -- personal group is empty\n";
+ print STDERR " (removing group $grname -- personal group is empty)";
+ $changes = 1;
} else {
$grmember_list = join(',', @new_grmembers);
$new_grent = join(':', $grname, $grpass, $grgid, $grmember_list);
}
close(NEW_GROUP);
rename($new_group_file, $group_file) || # Replace old group file with new
- die "\n${whoami}: error: couldn't rename $new_group_file to $group_file ($!)\n";
+ die "\n${whoami}: Error: couldn't rename $new_group_file to $group_file ($!)\n";
close(GROUP); # File handle is worthless now
+ print STDERR " (no changes)" if (! $changes);
print STDERR " done.\n";
}
system('/bin/rm', '-rf', $dir);
}
+sub remove_files_from_dir {
+ local($dir, $login_name, $uid) = @_;
+ local($path, $i, $owner);
+
+ print STDERR "Removing files belonging to ${login_name} from ${dir}:";
+
+ if (!opendir(DELDIR, $dir)) {
+ print STDERR "\n${whoami}: Warning: couldn't open directory ${dir} ($!)\n";
+ return;
+ }
+ while ($i = readdir(DELDIR)) {
+ next if $i eq '.';
+ next if $i eq '..';
+
+ $owner = (stat("$dir/$i"))[4]; # UID
+ if ($uid == $owner) {
+ if (-f "$dir/$i") {
+ print STDERR " $i";
+ unlink "$dir/$i" ||
+ print STDERR "\n${whoami}: Warning: unlink on ${dir}/${i} failed ($!) - continuing\n";
+ } else {
+ print STDERR " ($i not a regular file - skipped)";
+ }
+ }
+ }
+ closedir(DELDIR);
+
+ printf STDERR " done.\n";
+}
+
sub remove_at_jobs {
local($login_name, $uid) = @_;
local($i, $owner, $found);
}
return $path;
}
+
+sub kill_users_processes {
+ local($login_name, $uid) = @_;
+ local($pid, $result);
+
+ #
+ # Do something a little complex: fork a child that changes its
+ # real and effective UID to that of the removed user, then issues
+ # a "kill(9, -1)" to kill all processes of the same uid as the sender
+ # (see kill(2) for details).
+ # The parent waits for the exit of the child and then returns.
+
+ if ($pid = fork) {
+ # Parent process
+ waitpid($pid, 0);
+ } elsif (defined $pid) {
+ # Child process
+ $< = $uid;
+ $> = $uid;
+ if ($< != $uid || $> != $uid) {
+ print STDERR "${whoami}: Error (kill_users_processes):\n" .
+ "\tCouldn't reset uid/euid to ${uid}: current uid/euid's are $< and $>\n";
+ exit 1;
+ }
+ $result = kill(9, -1);
+ print STDERR "Killed process(es) belonging to $login_name.\n"
+ if $result;
+ exit 0;
+ } else {
+ # Couldn't fork!
+ print STDERR "${whoami}: Error: couldn't fork to kill ${login_name}'s processes - continuing\n";
+ }
+}