]> git.cameronkatri.com Git - pw-darwin.git/blob - adduser/rmuser.perl
Make uu_* const correct.
[pw-darwin.git] / 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 # $Id: rmuser.perl,v 1.6 1997/03/08 18:04:45 wosch Exp $
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 die "Sorry, login name must contain alphanumeric characters only.\n"
111 if ($login_name !~ /^[a-z0-9_][a-z0-9_\-]*$/);
112 } else {
113 if ($affirm) {
114 print STDERR "${whoami}: Error: -y option given without username!\n";
115 &unlockpw;
116 exit 1;
117 }
118 # Get the user name from the user
119 $login_name = &get_login_name;
120 }
121
122 if (($pw_ent = &check_login_name($login_name)) eq '0') {
123 print STDERR "${whoami}: Error: User ${login_name} not in password database\n";
124 &unlockpw;
125 exit 1;
126 }
127
128 ($name, $password, $uid, $gid, $class, $change, $expire, $gecos, $home_dir,
129 $shell) = split(/:/, $pw_ent);
130
131 if ($uid == 0) {
132 print "${whoami}: Error: I'd rather not remove a user with a uid of 0.\n";
133 &unlockpw;
134 exit 1;
135 }
136
137 if (! $affirm) {
138 print "Matching password entry:\n\n$pw_ent\n\n";
139
140 $ans = &get_yn("Is this the entry you wish to remove? ");
141
142 if ($ans eq 'N') {
143 print "${whoami}: Informational: User ${login_name} not removed.\n";
144 &unlockpw;
145 exit 0;
146 }
147 }
148
149 #
150 # Get owner of user's home directory; don't remove home dir if not
151 # owned by $login_name
152
153 $remove_directory = 1;
154
155 if (-l $home_dir) {
156 $real_home_dir = &resolvelink($home_dir);
157 } else {
158 $real_home_dir = $home_dir;
159 }
160
161 #
162 # If home_dir is a symlink and points to something that isn't a directory,
163 # or if home_dir is not a symlink and is not a directory, don't remove
164 # home_dir -- seems like a good thing to do, but probably isn't necessary...
165
166 if (((-l $home_dir) && ((-e $real_home_dir) && !(-d $real_home_dir))) ||
167 (!(-l $home_dir) && !(-d $home_dir))) {
168 print STDERR "${whoami}: Informational: Home ${home_dir} is not a directory, so it won't be removed\n";
169 $remove_directory = 0;
170 }
171
172 if (length($real_home_dir) && -d $real_home_dir) {
173 $dir_owner = (stat($real_home_dir))[4]; # UID
174 if ($dir_owner != $uid) {
175 print STDERR "${whoami}: Informational: Home dir ${real_home_dir} is" .
176 " not owned by ${login_name} (uid ${dir_owner})\n," .
177 "\tso it won't be removed\n";
178 $remove_directory = 0;
179 }
180 }
181
182 if ($remove_directory && ! $affirm) {
183 $ans = &get_yn("Remove user's home directory ($home_dir)? ");
184 if ($ans eq 'N') {
185 $remove_directory = 0;
186 }
187 }
188
189 #exit 0 if $debug;
190
191 #
192 # Remove the user's crontab, if there is one
193 # (probably needs to be done before password databases are updated)
194
195 if (-e "$crontab_dir/$login_name") {
196 print STDERR "Removing user's crontab:";
197 system('/usr/bin/crontab', '-u', $login_name, '-r');
198 print STDERR " done.\n";
199 }
200
201 #
202 # Remove the user's at jobs, if any
203 # (probably also needs to be done before password databases are updated)
204
205 &remove_at_jobs($login_name, $uid);
206
207 #
208 # Kill all the user's processes
209
210 &kill_users_processes($login_name, $uid);
211
212 #
213 # Copy master password file to new file less removed user's entry
214
215 &update_passwd_file;
216
217 #
218 # Remove the user from all groups in /etc/group
219
220 &update_group_file($login_name);
221
222 #
223 # Remove the user's home directory
224
225 if ($remove_directory) {
226 print STDERR "Removing user's home directory ($home_dir):";
227 &remove_dir($home_dir);
228 print STDERR " done.\n";
229 }
230
231 #
232 # Remove files related to the user from the mail directory
233
234 #&remove_files_from_dir($mail_dir, $login_name, $uid);
235 $file = "$mail_dir/$login_name";
236 if (-e $file || -l $file) {
237 print STDERR "Removing user's incoming mail file ${file}:";
238 unlink $file ||
239 print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
240 print STDERR " done.\n";
241 }
242
243 #
244 # Remove some pop daemon's leftover file
245
246 $file = "$mail_dir/.${login_name}.pop";
247 if (-e $file || -l $file) {
248 print STDERR "Removing pop daemon's temporary mail file ${file}:";
249 unlink $file ||
250 print STDERR "\n${whoami}: Warning: unlink on $file failed ($!) - continuing\n";
251 print STDERR " done.\n";
252 }
253
254 #
255 # Remove files belonging to the user from the directories /tmp, /var/tmp,
256 # and /var/tmp/vi.recover. Note that this doesn't take care of the
257 # problem where a user may have directories or symbolic links in those
258 # directories -- only regular files are removed.
259
260 &remove_files_from_dir('/tmp', $login_name, $uid);
261 &remove_files_from_dir('/var/tmp', $login_name, $uid);
262 &remove_files_from_dir('/var/tmp/vi.recover', $login_name, $uid)
263 if (-e '/var/tmp/vi.recover');
264
265 #
266 # All done!
267
268 exit 0;
269
270 sub get_login_name {
271 #
272 # Get new user's name
273 local($done, $login_name);
274
275 for ($done = 0; ! $done; ) {
276 print "Enter login name for user to remove: ";
277 $login_name = <>;
278 chop $login_name;
279 if (!($login_name =~ /^[a-z0-9_][a-z0-9_\-]*$/)) {
280 print STDERR "Sorry, login name must contain alphanumeric characters only.\n";
281 } elsif (length($login_name) > 16 || length($login_name) == 0) {
282 print STDERR "Sorry, login name must be 16 characters or less.\n";
283 } else {
284 $done = 1;
285 }
286 }
287
288 print "User name is ${login_name}\n" if $debug;
289 return($login_name);
290 }
291
292 sub check_login_name {
293 #
294 # Check to see whether login name is in password file
295 local($login_name) = @_;
296 local($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire,
297 $Mgecos, $Mhome_dir, $Mshell);
298 local($i);
299
300 seek(MASTER_PW, 0, 0);
301 while ($i = <MASTER_PW>) {
302 chop $i;
303 ($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire,
304 $Mgecos, $Mhome_dir, $Mshell) = split(/:/, $i);
305 if ($Mname eq $login_name) {
306 seek(MASTER_PW, 0, 0);
307 return($i); # User is in password database
308 }
309 }
310 seek(MASTER_PW, 0, 0);
311
312 return '0'; # User wasn't found
313 }
314
315 sub get_yn {
316 #
317 # Get a yes or no answer; return 'Y' or 'N'
318 local($prompt) = @_;
319 local($done, $ans);
320
321 for ($done = 0; ! $done; ) {
322 print $prompt;
323 $ans = <>;
324 chop $ans;
325 $ans =~ tr/a-z/A-Z/;
326 if (!($ans =~ /^[YN]/)) {
327 print STDERR "Please answer (y)es or (n)o.\n";
328 } else {
329 $done = 1;
330 }
331 }
332
333 return(substr($ans, 0, 1));
334 }
335
336 sub update_passwd_file {
337 local($skipped, $i);
338
339 print STDERR "Updating password file,";
340 seek(MASTER_PW, 0, 0);
341 open(NEW_PW, ">$new_passwd_file") ||
342 die "\n${whoami}: Error: Couldn't open file ${new_passwd_file}:\n $!\n";
343 chmod(0600, $new_passwd_file) ||
344 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";
345 $skipped = 0;
346 while ($i = <MASTER_PW>) {
347 if ($i =~ /\n$/) {
348 chop $i;
349 }
350 if ($i ne $pw_ent) {
351 print NEW_PW "$i\n";
352 } else {
353 print STDERR "Dropped entry for $login_name\n" if $debug;
354 $skipped = 1;
355 }
356 }
357 close(NEW_PW);
358 seek(MASTER_PW, 0, 0);
359
360 if ($skipped == 0) {
361 print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n";
362 unlink($new_passwd_file) ||
363 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";
364 &unlockpw;
365 exit 1;
366 }
367
368 #
369 # Run pwd_mkdb to install the updated password files and databases
370
371 print STDERR " updating databases,";
372 system('/usr/sbin/pwd_mkdb', '-p', ${new_passwd_file});
373 print STDERR " done.\n";
374
375 close(MASTER_PW); # Not useful anymore
376 }
377
378 sub update_group_file {
379 local($login_name) = @_;
380
381 local($i, $j, $grmember_list, $new_grent, $changes);
382 local($grname, $grpass, $grgid, $grmember_list, @grmembers);
383
384 $changes = 0;
385 print STDERR "Updating group file:";
386 open(GROUP, $group_file) ||
387 die "\n${whoami}: Error: couldn't open ${group_file}: $!\n";
388 if (!flock(GROUP, &LOCK_EX|&LOCK_NB)) {
389 print STDERR "\n${whoami}: Error: couldn't lock ${group_file}: $!\n";
390 exit 1;
391 }
392 local($group_perms, $group_uid, $group_gid) =
393 (stat(GROUP))[2, 4, 5]; # File Mode, uid, gid
394 open(NEW_GROUP, ">$new_group_file") ||
395 die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n";
396 chmod($group_perms, $new_group_file) ||
397 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;
398 chown($group_uid, $group_gid, $new_group_file) ||
399 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";
400 while ($i = <GROUP>) {
401 if (!($i =~ /$login_name/)) {
402 # Line doesn't contain any references to the user, so just add it
403 # to the new file
404 print NEW_GROUP $i;
405 } else {
406 #
407 # Remove the user from the group
408 if ($i =~ /\n$/) {
409 chop $i;
410 }
411 ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i);
412 @grmembers = split(/,/, $grmember_list);
413 undef @new_grmembers;
414 local(@new_grmembers);
415 foreach $j (@grmembers) {
416 if ($j ne $login_name) {
417 push(@new_grmembers, $j);
418 } else {
419 print STDERR " $grname";
420 $changes = 1;
421 }
422 }
423 if ($grname eq $login_name && $#new_grmembers == -1) {
424 # Remove a user's personal group if empty
425 print STDERR " (removing group $grname -- personal group is empty)";
426 $changes = 1;
427 } else {
428 $grmember_list = join(',', @new_grmembers);
429 $new_grent = join(':', $grname, $grpass, $grgid, $grmember_list);
430 print NEW_GROUP "$new_grent\n";
431 }
432 }
433 }
434 close(NEW_GROUP);
435 rename($new_group_file, $group_file) || # Replace old group file with new
436 die "\n${whoami}: Error: couldn't rename $new_group_file to $group_file ($!)\n";
437 close(GROUP); # File handle is worthless now
438 print STDERR " (no changes)" if (! $changes);
439 print STDERR " done.\n";
440 }
441
442 sub remove_dir {
443 # Remove the user's home directory
444 local($dir) = @_;
445 local($linkdir);
446
447 if (-l $dir) {
448 $linkdir = &resolvelink($dir);
449 # Remove the symbolic link
450 unlink($dir) ||
451 warn "${whoami}: Warning: could not unlink symlink $dir: $!\n";
452 if (!(-e $linkdir)) {
453 #
454 # Dangling symlink - just return now
455 return;
456 }
457 # Set dir to be the resolved pathname
458 $dir = $linkdir;
459 }
460 if (!(-d $dir)) {
461 print STDERR "${whoami}: Warning: $dir is not a directory\n";
462 unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n";
463 return;
464 }
465 system('/bin/rm', '-rf', $dir);
466 }
467
468 sub remove_files_from_dir {
469 local($dir, $login_name, $uid) = @_;
470 local($path, $i, $owner);
471
472 print STDERR "Removing files belonging to ${login_name} from ${dir}:";
473
474 if (!opendir(DELDIR, $dir)) {
475 print STDERR "\n${whoami}: Warning: couldn't open directory ${dir} ($!)\n";
476 return;
477 }
478 while ($i = readdir(DELDIR)) {
479 next if $i eq '.';
480 next if $i eq '..';
481
482 $owner = (stat("$dir/$i"))[4]; # UID
483 if ($uid == $owner) {
484 if (-f "$dir/$i") {
485 print STDERR " $i";
486 unlink "$dir/$i" ||
487 print STDERR "\n${whoami}: Warning: unlink on ${dir}/${i} failed ($!) - continuing\n";
488 } else {
489 print STDERR " ($i not a regular file - skipped)";
490 }
491 }
492 }
493 closedir(DELDIR);
494
495 printf STDERR " done.\n";
496 }
497
498 sub remove_at_jobs {
499 local($login_name, $uid) = @_;
500 local($i, $owner, $found);
501
502 $found = 0;
503 opendir(ATDIR, $atjob_dir) || return;
504 while ($i = readdir(ATDIR)) {
505 next if $i eq '.';
506 next if $i eq '..';
507 next if $i eq '.lockfile';
508
509 $owner = (stat("$atjob_dir/$i"))[4]; # UID
510 if ($uid == $owner) {
511 if (!$found) {
512 print STDERR "Removing user's at jobs:";
513 $found = 1;
514 }
515 # Use atrm to remove the job
516 print STDERR " $i";
517 system('/usr/bin/atrm', $i);
518 }
519 }
520 closedir(ATDIR);
521 if ($found) {
522 print STDERR " done.\n";
523 }
524 }
525
526 sub resolvelink {
527 local($path) = @_;
528 local($l);
529
530 while (-l $path && -e $path) {
531 if (!defined($l = readlink($path))) {
532 die "${whoami}: readlink on $path failed (but it should have worked!): $!\n";
533 }
534 if ($l =~ /^\//) {
535 # Absolute link
536 $path = $l;
537 } else {
538 # Relative link
539 $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path
540 }
541 }
542 return $path;
543 }
544
545 sub kill_users_processes {
546 local($login_name, $uid) = @_;
547 local($pid, $result);
548
549 #
550 # Do something a little complex: fork a child that changes its
551 # real and effective UID to that of the removed user, then issues
552 # a "kill(9, -1)" to kill all processes of the same uid as the sender
553 # (see kill(2) for details).
554 # The parent waits for the exit of the child and then returns.
555
556 if ($pid = fork) {
557 # Parent process
558 waitpid($pid, 0);
559 } elsif (defined $pid) {
560 # Child process
561 $< = $uid;
562 $> = $uid;
563 if ($< != $uid || $> != $uid) {
564 print STDERR "${whoami}: Error (kill_users_processes):\n" .
565 "\tCouldn't reset uid/euid to ${uid}: current uid/euid's are $< and $>\n";
566 exit 1;
567 }
568 $result = kill(9, -1);
569 print STDERR "Killed process(es) belonging to $login_name.\n"
570 if $result;
571 exit 0;
572 } else {
573 # Couldn't fork!
574 print STDERR "${whoami}: Error: couldn't fork to kill ${login_name}'s processes - continuing\n";
575 }
576 }