]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - usr.sbin/adduser/rmuser.perl
This commit was generated by cvs2svn to compensate for changes in r80357,
[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 $ptmp = "/etc/ptmp";
46 $group_file = "/etc/group";
47 $new_group_file = "${group_file}.new.$$";
48 $mail_dir = "/var/mail";
49 $crontab_dir = "/var/cron/tabs";
50 $affirm = 0;
51
52 #$debug = 1;
53
54 sub cleanup {
55     local($sig) = @_;
56
57     print STDERR "Caught signal SIG$sig -- cleaning up.\n";
58     &unlockpw;
59     if (-e $new_passwd_file) {
60         unlink $new_passwd_file;
61     }
62     exit(0);
63 }
64
65 sub lockpw {
66     # Open the password file for reading
67     if (!open(MASTER_PW, "$passwd_file")) {
68         print STDERR "${whoami}: Error: Couldn't open ${passwd_file}: $!\n";
69         exit(1);
70     }
71     # Set the close-on-exec flag just in case
72     fcntl(MASTER_PW, &F_SETFD, 1);
73     # Apply an advisory lock the password file
74     if (!flock(MASTER_PW, &LOCK_EX|&LOCK_NB)) {
75         print STDERR "${whoami}: Error: Couldn't lock ${passwd_file}: $!\n";
76         exit(1);
77     }
78 }
79
80 sub unlockpw {
81     flock(MASTER_PW, &LOCK_UN);
82 }
83
84 $SIG{'INT'} = 'cleanup';
85 $SIG{'QUIT'} = 'cleanup';
86 $SIG{'HUP'} = 'cleanup';
87 $SIG{'TERM'} = 'cleanup';
88
89 if ($#ARGV == 1 && $ARGV[0] eq '-y') {
90     shift @ARGV;
91     $affirm = 1;
92 }
93
94 if ($#ARGV > 0) {
95     print STDERR "usage: ${whoami} [-y] [username]\n";
96     exit(1);
97 }
98
99 if ($< != 0) {
100     print STDERR "${whoami}: Error: you must be root to use ${whoami}\n";
101     exit(1);
102 }
103
104 &lockpw;
105
106 if ($#ARGV == 0) {
107     # Username was given as a parameter
108     $login_name = pop(@ARGV);
109 } else {
110     if ($affirm) {
111         print STDERR "${whoami}: Error: -y option given without username!\n";
112         &unlockpw;
113         exit 1;
114     }
115     # Get the user name from the user
116     $login_name = &get_login_name;
117 }
118
119 ($name, $password, $uid, $gid, $change, $class, $gecos, $home_dir, $shell) =
120     (getpwnam("$login_name"));
121
122 if (!defined $uid) {
123     print STDERR "${whoami}: Error: User ${login_name} not in password database\n";
124     &unlockpw;
125     exit 1;
126 }
127
128 if ($uid == 0) {
129     print "${whoami}: Error: I'd rather not remove a user with a uid of 0.\n";
130     &unlockpw;
131     exit 1;
132 }
133
134 if (! $affirm) {
135     print "Matching password entry:\n\n$name\:$password\:$uid\:$gid\:$class\:$change\:0\:$gecos\:$home_dir\:$shell\n\n";
136
137     $ans = &get_yn("Is this the entry you wish to remove? ");
138
139     if ($ans eq 'N') {
140         print "${whoami}: Informational: User ${login_name} not removed.\n";
141         &unlockpw;
142         exit 0;
143     }
144 }
145
146 #
147 # Get owner of user's home directory; don't remove home dir if not
148 # owned by $login_name
149
150 $remove_directory = 1;
151
152 if (-l $home_dir) {
153     $real_home_dir = &resolvelink($home_dir);
154 } else {
155     $real_home_dir = $home_dir;
156 }
157
158 #
159 # If home_dir is a symlink and points to something that isn't a directory,
160 # or if home_dir is not a symlink and is not a directory, don't remove
161 # home_dir -- seems like a good thing to do, but probably isn't necessary...
162
163 if (((-l $home_dir) && ((-e $real_home_dir) && !(-d $real_home_dir))) ||
164     (!(-l $home_dir) && !(-d $home_dir))) {
165     print STDERR "${whoami}: Informational: Home ${home_dir} is not a directory, so it won't be removed\n";
166     $remove_directory = 0;
167 }
168
169 if (length($real_home_dir) && -d $real_home_dir) {
170     $dir_owner = (stat($real_home_dir))[4]; # UID
171     if ($dir_owner != $uid) {
172         print STDERR "${whoami}: Informational: Home dir ${real_home_dir} is" .
173             " not owned by ${login_name} (uid ${dir_owner})\n," .
174                 "\tso it won't be removed\n";
175         $remove_directory = 0;
176     }
177 }
178
179 if ($remove_directory && ! $affirm) {
180     $ans = &get_yn("Remove user's home directory ($home_dir)? ");
181     if ($ans eq 'N') {
182         $remove_directory = 0;
183     }
184 }
185
186 #exit 0 if $debug;
187
188 #
189 # Remove the user's crontab, if there is one
190 # (probably needs to be done before password databases are updated)
191
192 if (-e "$crontab_dir/$login_name") {
193     print STDERR "Removing user's crontab:";
194     system('/usr/bin/crontab', '-u', $login_name, '-r');
195     print STDERR " done.\n";
196 }
197
198 #
199 # Remove the user's at jobs, if any
200 # (probably also needs to be done before password databases are updated)
201
202 &remove_at_jobs($login_name);
203
204 #
205 # Kill all the user's processes
206
207 &kill_users_processes($login_name, $uid);
208
209 #
210 # Copy master password file to new file less removed user's entry
211
212 &update_passwd_file;
213
214 #
215 # Remove the user from all groups in /etc/group
216
217 &update_group_file($login_name);
218
219 #
220 # Remove the user's home directory
221
222 if ($remove_directory) {
223     print STDERR "Removing user's home directory ($home_dir):";
224     &remove_dir($home_dir);
225     print STDERR " done.\n";
226 }
227
228 #
229 # Remove files related to the user from the mail directory
230
231 #&remove_files_from_dir($mail_dir, $login_name, $uid);
232 $file = "$mail_dir/$login_name";
233 if (-e $file || -l $file) {
234     print STDERR "Removing user's incoming mail file ${file}:";
235     unlink $file ||
236         print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
237     print STDERR " done.\n";
238 }
239
240 #
241 # Remove some pop daemon's leftover file
242
243 $file = "$mail_dir/.${login_name}.pop";
244 if (-e $file || -l $file) {
245     print STDERR "Removing pop daemon's temporary mail file ${file}:";
246     unlink $file ||
247         print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
248     print STDERR " done.\n";
249 }
250
251 #
252 # Remove files belonging to the user from the directories /tmp, /var/tmp,
253 # and /var/tmp/vi.recover.  Note that this doesn't take care of the
254 # problem where a user may have directories or symbolic links in those
255 # directories -- only regular files are removed.
256
257 &remove_files_from_dir('/tmp', $login_name, $uid);
258 &remove_files_from_dir('/var/tmp', $login_name, $uid);
259 &remove_files_from_dir('/var/tmp/vi.recover', $login_name, $uid)
260     if (-e '/var/tmp/vi.recover');
261
262 #
263 # All done!
264
265 exit 0;
266
267 sub get_login_name {
268     #
269     # Get new user's name
270     local($done, $login_name);
271
272     for ($done = 0; ! $done; ) {
273         print "Enter login name for user to remove: ";
274         $login_name = <>;
275         chomp $login_name;
276         if (not getpwnam("$login_name")) {
277             print STDERR "Sorry, login name not in password database.\n";
278         } else {
279             $done = 1;
280         }
281     }
282
283     print "User name is ${login_name}\n" if $debug;
284     return($login_name);
285 }
286
287 sub get_yn {
288     #
289     # Get a yes or no answer; return 'Y' or 'N'
290     local($prompt) = @_;
291     local($done, $ans);
292
293     for ($done = 0; ! $done; ) {
294         print $prompt;
295         $ans = <>;
296         chop $ans;
297         $ans =~ tr/a-z/A-Z/;
298         if (!($ans =~ /^[YN]/)) {
299             print STDERR "Please answer (y)es or (n)o.\n";
300         } else {
301             $done = 1;
302         }
303     }
304
305     return(substr($ans, 0, 1));
306 }
307
308 sub update_passwd_file {
309     local($skipped);
310
311     print STDERR "Updating password file,";
312     seek(MASTER_PW, 0, 0);
313
314     sysopen(NEW_PW, $etc_ptmp, O_RDWR|O_CREAT|O_EXCL, 0600) ||
315         die "\n${whoami}: Error: Couldn't open file ${etc_ptmp}:\n $!\n";
316
317     $skipped = 0;
318     while (<MASTER_PW>) {
319         if (/^\Q$login_name:/o) {
320             print STDERR "Dropped entry for $login_name\n" if $debug;
321             $skipped = 1;
322         } else {
323             print NEW_PW;
324             # The other perl password tools assume all lowercase entries.
325             # Add a warning to help unsuspecting admins who might be
326             # using the wrong tool for the job, or might otherwise
327             # be unwittingly holding a loaded foot-shooting device.
328             if (/^\Q$login_name:/io) {
329                 my $name = $_;
330                 $name =~ s#\:.*\n##;
331                 print STDERR "\n\n\tThere is also an entry for $name in your",
332                     "password file.\n\tThis can cause problems in some ",
333                     "situations.\n\n";
334             }
335         }
336     }
337     close(NEW_PW);
338     seek(MASTER_PW, 0, 0);
339
340     if ($skipped == 0) {
341         print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n";
342         unlink($etc_ptmp) ||
343             print STDERR "\n${whoami}: Warning: couldn't unlink $etc_ptmp ($!)\n\tPlease investigate, as this file should not be left in the filesystem\n";
344         &unlockpw;
345         exit 1;
346     }
347
348     #
349     # Run pwd_mkdb to install the updated password files and databases
350
351     print STDERR " updating databases,";
352     system('/usr/sbin/pwd_mkdb', '-p', ${passwd_file});
353     print STDERR " done.\n";
354
355     close(MASTER_PW);           # Not useful anymore
356 }
357
358 sub update_group_file {
359     local($login_name) = @_;
360
361     local($i, $j, $grmember_list, $new_grent, $changes);
362     local($grname, $grpass, $grgid, $grmember_list, @grmembers);
363
364     $changes = 0;
365     print STDERR "Updating group file:";
366     open(GROUP, $group_file) ||
367         die "\n${whoami}: Error: couldn't open ${group_file}: $!\n";
368     if (!flock(GROUP, &LOCK_EX|&LOCK_NB)) {
369         print STDERR "\n${whoami}: Error: couldn't lock ${group_file}: $!\n";
370         exit 1;
371     }
372     local($group_perms, $group_uid, $group_gid) =
373         (stat(GROUP))[2, 4, 5]; # File Mode, uid, gid
374     open(NEW_GROUP, ">$new_group_file") ||
375         die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n";
376     chmod($group_perms, $new_group_file) ||
377         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;
378     chown($group_uid, $group_gid, $new_group_file) ||
379         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";
380     while ($i = <GROUP>) {
381         if (!($i =~ /$login_name/)) {
382             # Line doesn't contain any references to the user, so just add it
383             # to the new file
384             print NEW_GROUP $i;
385         } else {
386             #
387             # Remove the user from the group
388             if ($i =~ /\n$/) {
389                 chop $i;
390             }
391             ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i);
392             @grmembers = split(/,/, $grmember_list);
393             undef @new_grmembers;
394             local(@new_grmembers);
395             foreach $j (@grmembers) {
396                 if ($j ne $login_name) {
397                     push(@new_grmembers, $j);
398                 } else {
399                     print STDERR " $grname";
400                     $changes = 1;
401                 }
402             }
403             if ($grname eq $login_name && $#new_grmembers == -1) {
404                 # Remove a user's personal group if empty
405                 print STDERR " (removing group $grname -- personal group is empty)";
406                 $changes = 1;
407             } else {
408                 $grmember_list = join(',', @new_grmembers);
409                 $new_grent = join(':', $grname, $grpass, $grgid, $grmember_list);
410                 print NEW_GROUP "$new_grent\n";
411             }
412         }
413     }
414     close(NEW_GROUP);
415     rename($new_group_file, $group_file) || # Replace old group file with new
416         die "\n${whoami}: Error: couldn't rename $new_group_file to $group_file ($!)\n";
417     close(GROUP);                       # File handle is worthless now
418     print STDERR " (no changes)" if (! $changes);
419     print STDERR " done.\n";
420 }
421
422 sub remove_dir {
423     # Remove the user's home directory
424     local($dir) = @_;
425     local($linkdir);
426
427     if (-l $dir) {
428         $linkdir = &resolvelink($dir);
429         # Remove the symbolic link
430         unlink($dir) ||
431             warn "${whoami}: Warning: could not unlink symlink $dir: $!\n";
432         if (!(-e $linkdir)) {
433             #
434             # Dangling symlink - just return now
435             return;
436         }
437         # Set dir to be the resolved pathname
438         $dir = $linkdir;
439     }
440     if (!(-d $dir)) {
441         print STDERR "${whoami}: Warning: $dir is not a directory\n";
442         unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n";
443         return;
444     }
445     system('/bin/rm', '-rf', $dir);
446 }
447
448 sub remove_files_from_dir {
449     local($dir, $login_name, $uid) = @_;
450     local($path, $i, $owner);
451
452     print STDERR "Removing files belonging to ${login_name} from ${dir}:";
453
454     if (!opendir(DELDIR, $dir)) {
455         print STDERR "\n${whoami}: Warning: couldn't open directory ${dir} ($!)\n";
456         return;
457     }
458     while ($i = readdir(DELDIR)) {
459         next if $i eq '.';
460         next if $i eq '..';
461
462         $owner = (stat("$dir/$i"))[4]; # UID
463         if ($uid == $owner) {
464             if (-f "$dir/$i") {
465                 print STDERR " $i";
466                 unlink "$dir/$i" ||
467                     print STDERR "\n${whoami}: Warning: unlink on ${dir}/${i} failed ($!) - continuing\n";
468             } else {
469                 print STDERR " ($i not a regular file - skipped)";
470             }
471         }
472     }
473     closedir(DELDIR);
474
475     printf STDERR " done.\n";
476 }
477
478
479 sub invoke_atq {
480     local *ATQ;
481     my($user) = (shift || "");
482     my($path_atq) = "/usr/bin/atq";
483     my(@at) = ();
484     my($pid, $line);
485     
486     return @at if ($user eq "");
487     
488     if (!defined($pid = open(ATQ, "-|"))) {
489         die("creating pipe to atq: $!\n");
490     } elsif ($pid == 0) {
491         exec($path_atq, $user);
492         die("executing $path_atq: $!\n");
493     }
494     
495     while(defined($_ = <ATQ>)) {
496         chomp;
497         if (/^\d\d:\d\d:\d\d\s+\d\d\/\d\d\/\d\d\s+(\S+)\s+\S+\s+(\d+)$/) {
498             push(@at, $2) if ($1 eq $user);
499         }
500     }
501     close ATQ;
502     return @at;
503 }
504
505 sub invoke_atrm {
506     local *ATRM;
507     my($user) = (shift || "");
508     my($path_atrm) = "/usr/bin/atrm";
509     my(@jobs) = @_;
510     my($pid);
511     my($txt) = "";
512     
513     return "Invalid arguments" if (($user eq "") || ($#jobs == -1));
514     
515     if (!defined($pid = open(ATRM, "-|"))) {
516         die("creating pipe to atrm: $!\n");
517     } elsif ($pid == 0) {
518         exec($path_atrm, $user, @jobs);
519     }
520     
521     while(defined($_ = <ATRM>)) {
522         $txt .= $_;
523     }
524     close ATRM;
525     return $txt;
526 }
527
528 sub remove_at_jobs {
529     my($user) = (shift || "");
530     my(@at, $atrm);
531     
532     return 1 if ($user eq "");
533     
534     print STDERR "Removing user's at jobs:";
535     @at = invoke_atq($user);
536     return 0 if ($#at == -1);
537     
538     print STDERR " @at:";
539     $atrm = invoke_atrm($user, @at);
540     if ($atrm ne "") {
541         print STDERR " -- $atrm\n";
542         return 1;
543     }
544     
545     print STDERR "done.\n";
546     return 0;
547 }
548
549 sub resolvelink {
550     local($path) = @_;
551     local($l);
552
553     while (-l $path && -e $path) {
554         if (!defined($l = readlink($path))) {
555             die "${whoami}: readlink on $path failed (but it should have worked!): $!\n";
556         }
557         if ($l =~ /^\//) {
558             # Absolute link
559             $path = $l;
560         } else {
561             # Relative link
562             $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path
563         }
564     }
565     return $path;
566 }
567
568 sub kill_users_processes {
569     local($login_name, $uid) = @_;
570     local($pid, $result);
571
572     #
573     # Do something a little complex: fork a child that changes its
574     # real and effective UID to that of the removed user, then issues
575     # a "kill(9, -1)" to kill all processes of the same uid as the sender
576     # (see kill(2) for details).
577     # The parent waits for the exit of the child and then returns.
578
579     if ($pid = fork) {
580         # Parent process
581         waitpid($pid, 0);
582     } elsif (defined $pid) {
583         # Child process
584         $< = $uid;
585         $> = $uid;
586         if ($< != $uid || $> != $uid) {
587             print STDERR "${whoami}: Error (kill_users_processes):\n" .
588                 "\tCouldn't reset uid/euid to ${uid}: current uid/euid's are $< and $>\n";
589             exit 1;
590         }
591         $result = kill(9, -1);
592         print STDERR "Killed process(es) belonging to $login_name.\n"
593             if $result;
594         exit 0;
595     } else {
596         # Couldn't fork!
597         print STDERR "${whoami}: Error: couldn't fork to kill ${login_name}'s processes - continuing\n";
598     }
599 }