]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - usr.sbin/adduser/rmuser.perl
This commit was generated by cvs2svn to compensate for changes in r78460,
[FreeBSD/FreeBSD.git] / usr.sbin / adduser / rmuser.perl
1 #!/usr/bin/perl
2 # -*- perl -*-
3 # Copyright 1995, 1996, 1997 Guy Helmer, Ames, Iowa 50014.
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
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.
17 #
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.
28 #
29 # rmuser - Perl script to remove users
30 #
31 # Guy Helmer <ghelmer@cs.iastate.edu>, 02/23/97
32 #
33 # $FreeBSD$
34
35 sub LOCK_SH {0x01;}
36 sub LOCK_EX {0x02;}
37 sub LOCK_NB {0x04;}
38 sub LOCK_UN {0x08;}
39 sub F_SETFD {2;}
40
41 $ENV{"PATH"} = "/bin:/sbin:/usr/bin:/usr/sbin";
42 umask(022);
43 $whoami = $0;
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";
51 $affirm = 0;
52
53 #$debug = 1;
54
55 sub cleanup {
56     local($sig) = @_;
57
58     print STDERR "Caught signal SIG$sig -- cleaning up.\n";
59     &unlockpw;
60     if (-e $new_passwd_file) {
61         unlink $new_passwd_file;
62     }
63     exit(0);
64 }
65
66 sub lockpw {
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";
70         exit(1);
71     }
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";
77         exit(1);
78     }
79 }
80
81 sub unlockpw {
82     flock(MASTER_PW, &LOCK_UN);
83 }
84
85 $SIG{'INT'} = 'cleanup';
86 $SIG{'QUIT'} = 'cleanup';
87 $SIG{'HUP'} = 'cleanup';
88 $SIG{'TERM'} = 'cleanup';
89
90 if ($#ARGV == 1 && $ARGV[0] eq '-y') {
91     shift @ARGV;
92     $affirm = 1;
93 }
94
95 if ($#ARGV > 0) {
96     print STDERR "usage: ${whoami} [-y] [username]\n";
97     exit(1);
98 }
99
100 if ($< != 0) {
101     print STDERR "${whoami}: Error: you must be root to use ${whoami}\n";
102     exit(1);
103 }
104
105 &lockpw;
106
107 if ($#ARGV == 0) {
108     # Username was given as a parameter
109     $login_name = pop(@ARGV);
110 } else {
111     if ($affirm) {
112         print STDERR "${whoami}: Error: -y option given without username!\n";
113         &unlockpw;
114         exit 1;
115     }
116     # Get the user name from the user
117     $login_name = &get_login_name;
118 }
119
120 ($name, $password, $uid, $gid, $change, $class, $gecos, $home_dir, $shell) =
121     (getpwnam("$login_name"));
122
123 if (!defined $uid) {
124     print STDERR "${whoami}: Error: User ${login_name} not in password database\n";
125     &unlockpw;
126     exit 1;
127 }
128
129 if ($uid == 0) {
130     print "${whoami}: Error: I'd rather not remove a user with a uid of 0.\n";
131     &unlockpw;
132     exit 1;
133 }
134
135 if (! $affirm) {
136     print "Matching password entry:\n\n$name\:$password\:$uid\:$gid\:$class\:$change\:0\:$gecos\:$home_dir\:$shell\n\n";
137
138     $ans = &get_yn("Is this the entry you wish to remove? ");
139
140     if ($ans eq 'N') {
141         print "${whoami}: Informational: User ${login_name} not removed.\n";
142         &unlockpw;
143         exit 0;
144     }
145 }
146
147 #
148 # Get owner of user's home directory; don't remove home dir if not
149 # owned by $login_name
150
151 $remove_directory = 1;
152
153 if (-l $home_dir) {
154     $real_home_dir = &resolvelink($home_dir);
155 } else {
156     $real_home_dir = $home_dir;
157 }
158
159 #
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...
163
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;
168 }
169
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;
177     }
178 }
179
180 if ($remove_directory && ! $affirm) {
181     $ans = &get_yn("Remove user's home directory ($home_dir)? ");
182     if ($ans eq 'N') {
183         $remove_directory = 0;
184     }
185 }
186
187 #exit 0 if $debug;
188
189 #
190 # Remove the user's crontab, if there is one
191 # (probably needs to be done before password databases are updated)
192
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";
197 }
198
199 #
200 # Remove the user's at jobs, if any
201 # (probably also needs to be done before password databases are updated)
202
203 &remove_at_jobs($login_name, $uid);
204
205 #
206 # Kill all the user's processes
207
208 &kill_users_processes($login_name, $uid);
209
210 #
211 # Copy master password file to new file less removed user's entry
212
213 &update_passwd_file;
214
215 #
216 # Remove the user from all groups in /etc/group
217
218 &update_group_file($login_name);
219
220 #
221 # Remove the user's home directory
222
223 if ($remove_directory) {
224     print STDERR "Removing user's home directory ($home_dir):";
225     &remove_dir($home_dir);
226     print STDERR " done.\n";
227 }
228
229 #
230 # Remove files related to the user from the mail directory
231
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}:";
236     unlink $file ||
237         print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
238     print STDERR " done.\n";
239 }
240
241 #
242 # Remove some pop daemon's leftover file
243
244 $file = "$mail_dir/.${login_name}.pop";
245 if (-e $file || -l $file) {
246     print STDERR "Removing pop daemon's temporary mail file ${file}:";
247     unlink $file ||
248         print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
249     print STDERR " done.\n";
250 }
251
252 #
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.
257
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');
262
263 #
264 # All done!
265
266 exit 0;
267
268 sub get_login_name {
269     #
270     # Get new user's name
271     local($done, $login_name);
272
273     for ($done = 0; ! $done; ) {
274         print "Enter login name for user to remove: ";
275         $login_name = <>;
276         chomp $login_name;
277         if (not getpwnam("$login_name")) {
278             print STDERR "Sorry, login name not in password database.\n";
279         } else {
280             $done = 1;
281         }
282     }
283
284     print "User name is ${login_name}\n" if $debug;
285     return($login_name);
286 }
287
288 sub get_yn {
289     #
290     # Get a yes or no answer; return 'Y' or 'N'
291     local($prompt) = @_;
292     local($done, $ans);
293
294     for ($done = 0; ! $done; ) {
295         print $prompt;
296         $ans = <>;
297         chop $ans;
298         $ans =~ tr/a-z/A-Z/;
299         if (!($ans =~ /^[YN]/)) {
300             print STDERR "Please answer (y)es or (n)o.\n";
301         } else {
302             $done = 1;
303         }
304     }
305
306     return(substr($ans, 0, 1));
307 }
308
309 sub update_passwd_file {
310     local($skipped);
311
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";
318     $skipped = 0;
319     while (<MASTER_PW>) {
320         if (/^\Q$login_name:/o) {
321             print STDERR "Dropped entry for $login_name\n" if $debug;
322             $skipped = 1;
323         } else {
324             print NEW_PW;
325             # The other perl password tools assume all lowercase entries.
326             # Add a warning to help unsuspecting admins who might be
327             # using the wrong tool for the job, or might otherwise
328             # be unwittingly holding a loaded foot-shooting device.
329             if (/^\Q$login_name:/io) {
330                 my $name = $_;
331                 $name =~ s#\:.*\n##;
332                 print STDERR "\n\n\tThere is also an entry for $name in your",
333                     "password file.\n\tThis can cause problems in some ",
334                     "situations.\n\n";
335             }
336         }
337     }
338     close(NEW_PW);
339     seek(MASTER_PW, 0, 0);
340
341     if ($skipped == 0) {
342         print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n";
343         unlink($new_passwd_file) ||
344             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";
345         &unlockpw;
346         exit 1;
347     }
348
349     #
350     # Run pwd_mkdb to install the updated password files and databases
351
352     print STDERR " updating databases,";
353     system('/usr/sbin/pwd_mkdb', '-p', ${new_passwd_file});
354     print STDERR " done.\n";
355
356     close(MASTER_PW);           # Not useful anymore
357 }
358
359 sub update_group_file {
360     local($login_name) = @_;
361
362     local($i, $j, $grmember_list, $new_grent, $changes);
363     local($grname, $grpass, $grgid, $grmember_list, @grmembers);
364
365     $changes = 0;
366     print STDERR "Updating group file:";
367     open(GROUP, $group_file) ||
368         die "\n${whoami}: Error: couldn't open ${group_file}: $!\n";
369     if (!flock(GROUP, &LOCK_EX|&LOCK_NB)) {
370         print STDERR "\n${whoami}: Error: couldn't lock ${group_file}: $!\n";
371         exit 1;
372     }
373     local($group_perms, $group_uid, $group_gid) =
374         (stat(GROUP))[2, 4, 5]; # File Mode, uid, gid
375     open(NEW_GROUP, ">$new_group_file") ||
376         die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n";
377     chmod($group_perms, $new_group_file) ||
378         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;
379     chown($group_uid, $group_gid, $new_group_file) ||
380         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";
381     while ($i = <GROUP>) {
382         if (!($i =~ /$login_name/)) {
383             # Line doesn't contain any references to the user, so just add it
384             # to the new file
385             print NEW_GROUP $i;
386         } else {
387             #
388             # Remove the user from the group
389             if ($i =~ /\n$/) {
390                 chop $i;
391             }
392             ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i);
393             @grmembers = split(/,/, $grmember_list);
394             undef @new_grmembers;
395             local(@new_grmembers);
396             foreach $j (@grmembers) {
397                 if ($j ne $login_name) {
398                     push(@new_grmembers, $j);
399                 } else {
400                     print STDERR " $grname";
401                     $changes = 1;
402                 }
403             }
404             if ($grname eq $login_name && $#new_grmembers == -1) {
405                 # Remove a user's personal group if empty
406                 print STDERR " (removing group $grname -- personal group is empty)";
407                 $changes = 1;
408             } else {
409                 $grmember_list = join(',', @new_grmembers);
410                 $new_grent = join(':', $grname, $grpass, $grgid, $grmember_list);
411                 print NEW_GROUP "$new_grent\n";
412             }
413         }
414     }
415     close(NEW_GROUP);
416     rename($new_group_file, $group_file) || # Replace old group file with new
417         die "\n${whoami}: Error: couldn't rename $new_group_file to $group_file ($!)\n";
418     close(GROUP);                       # File handle is worthless now
419     print STDERR " (no changes)" if (! $changes);
420     print STDERR " done.\n";
421 }
422
423 sub remove_dir {
424     # Remove the user's home directory
425     local($dir) = @_;
426     local($linkdir);
427
428     if (-l $dir) {
429         $linkdir = &resolvelink($dir);
430         # Remove the symbolic link
431         unlink($dir) ||
432             warn "${whoami}: Warning: could not unlink symlink $dir: $!\n";
433         if (!(-e $linkdir)) {
434             #
435             # Dangling symlink - just return now
436             return;
437         }
438         # Set dir to be the resolved pathname
439         $dir = $linkdir;
440     }
441     if (!(-d $dir)) {
442         print STDERR "${whoami}: Warning: $dir is not a directory\n";
443         unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n";
444         return;
445     }
446     system('/bin/rm', '-rf', $dir);
447 }
448
449 sub remove_files_from_dir {
450     local($dir, $login_name, $uid) = @_;
451     local($path, $i, $owner);
452
453     print STDERR "Removing files belonging to ${login_name} from ${dir}:";
454
455     if (!opendir(DELDIR, $dir)) {
456         print STDERR "\n${whoami}: Warning: couldn't open directory ${dir} ($!)\n";
457         return;
458     }
459     while ($i = readdir(DELDIR)) {
460         next if $i eq '.';
461         next if $i eq '..';
462
463         $owner = (stat("$dir/$i"))[4]; # UID
464         if ($uid == $owner) {
465             if (-f "$dir/$i") {
466                 print STDERR " $i";
467                 unlink "$dir/$i" ||
468                     print STDERR "\n${whoami}: Warning: unlink on ${dir}/${i} failed ($!) - continuing\n";
469             } else {
470                 print STDERR " ($i not a regular file - skipped)";
471             }
472         }
473     }
474     closedir(DELDIR);
475
476     printf STDERR " done.\n";
477 }
478
479 sub remove_at_jobs {
480     local($login_name, $uid) = @_;
481     local($i, $owner, $found);
482
483     $found = 0;
484     opendir(ATDIR, $atjob_dir) || return;
485     while ($i = readdir(ATDIR)) {
486         next if $i eq '.';
487         next if $i eq '..';
488         next if $i eq '.lockfile';
489
490         $owner = (stat("$atjob_dir/$i"))[4]; # UID
491         if ($uid == $owner) {
492             if (!$found) {
493                 print STDERR "Removing user's at jobs:";
494                 $found = 1;
495             }
496             # Use atrm to remove the job
497             print STDERR " $i";
498             system('/usr/bin/atrm', $i);
499         }
500     }
501     closedir(ATDIR);
502     if ($found) {
503         print STDERR " done.\n";
504     }
505 }
506
507 sub resolvelink {
508     local($path) = @_;
509     local($l);
510
511     while (-l $path && -e $path) {
512         if (!defined($l = readlink($path))) {
513             die "${whoami}: readlink on $path failed (but it should have worked!): $!\n";
514         }
515         if ($l =~ /^\//) {
516             # Absolute link
517             $path = $l;
518         } else {
519             # Relative link
520             $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path
521         }
522     }
523     return $path;
524 }
525
526 sub kill_users_processes {
527     local($login_name, $uid) = @_;
528     local($pid, $result);
529
530     #
531     # Do something a little complex: fork a child that changes its
532     # real and effective UID to that of the removed user, then issues
533     # a "kill(9, -1)" to kill all processes of the same uid as the sender
534     # (see kill(2) for details).
535     # The parent waits for the exit of the child and then returns.
536
537     if ($pid = fork) {
538         # Parent process
539         waitpid($pid, 0);
540     } elsif (defined $pid) {
541         # Child process
542         $< = $uid;
543         $> = $uid;
544         if ($< != $uid || $> != $uid) {
545             print STDERR "${whoami}: Error (kill_users_processes):\n" .
546                 "\tCouldn't reset uid/euid to ${uid}: current uid/euid's are $< and $>\n";
547             exit 1;
548         }
549         $result = kill(9, -1);
550         print STDERR "Killed process(es) belonging to $login_name.\n"
551             if $result;
552         exit 0;
553     } else {
554         # Couldn't fork!
555         print STDERR "${whoami}: Error: couldn't fork to kill ${login_name}'s processes - continuing\n";
556     }
557 }