xref: /openbsd/usr.sbin/adduser/rmuser.perl (revision db3296cf)
1#!/usr/bin/perl
2# -*- perl -*-
3#
4# $OpenBSD: rmuser.perl,v 1.6 2002/05/31 19:47:00 millert Exp $
5#
6# Copyright 1995, 1996 Guy Helmer, Madison, South Dakota 57042.
7# All rights reserved.
8#
9# Redistribution and use in source and binary forms, with or without
10# modification, are permitted provided that the following conditions
11# are met:
12# 1. Redistributions of source code must retain the above copyright
13#    notice, this list of conditions and the following disclaimer as
14#    the first lines of this file unmodified.
15# 2. Redistributions in binary form must reproduce the above copyright
16#    notice, this list of conditions and the following disclaimer in the
17#    documentation and/or other materials provided with the distribution.
18# 3. The name of the author may not be used to endorse or promote products
19#    derived from this software without specific prior written permission.
20#
21# THIS SOFTWARE IS PROVIDED BY GUY HELMER ``AS IS'' AND ANY EXPRESS OR
22# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24# IN NO EVENT SHALL GUY HELMER BE LIABLE FOR ANY DIRECT, INDIRECT,
25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#
32# rmuser - Perl script to remove users
33#
34# Guy Helmer <ghelmer@alpha.dsu.edu>, 07/17/96
35#
36#	$From: rmuser.perl,v 1.2 1996/12/07 21:25:12 ache Exp $
37
38use Fcntl qw(:DEFAULT :flock);
39
40$ENV{"PATH"} = "/bin:/sbin:/usr/bin:/usr/sbin";
41umask(022);
42$whoami = $0;
43$passwd_file = "/etc/master.passwd";
44$passwd_tmp = "/etc/ptmp";
45$group_file = "/etc/group";
46$new_group_file = "${group_file}.new.$$";
47$mail_dir = "/var/mail";
48$crontab_dir = "/var/cron/tabs";
49$atjob_dir = "/var/at/jobs";
50
51#$debug = 1;
52
53END {
54    if (-e $passwd_tmp && defined(fileno(NEW_PW))) {
55	unlink($passwd_tmp) ||
56	    warn "\n${whoami}: warning: couldn't unlink $passwd_tmp ($!)\n\tPlease investigate, as this file should not be left in the filesystem\n";
57    }
58}
59
60sub cleanup {
61    local($sig) = @_;
62
63    print STDERR "Caught signal SIG$sig -- cleaning up.\n";
64    exit(0);
65}
66
67sub open_files {
68    open(GROUP, $group_file) ||
69	die "\n${whoami}: Error: couldn't open ${group_file}: $!\n";
70    if (!flock(GROUP, LOCK_EX|LOCK_NB)) {
71	print STDERR "\n${whoami}: Error: couldn't lock ${group_file}: $!\n";
72	exit 1;
73    }
74
75    sysopen(NEW_PW, $passwd_tmp, O_RDWR|O_CREAT|O_EXCL, 0600) ||
76	die "\n${whoami}: Error: Password file busy\n";
77
78    if (!open(MASTER_PW, $passwd_file)) {
79	print STDERR "${whoami}: Error: Couldn't open ${passwd_file}: $!\n";
80	exit(1);
81    }
82}
83
84$SIG{'INT'} = 'cleanup';
85$SIG{'QUIT'} = 'cleanup';
86$SIG{'HUP'} = 'cleanup';
87$SIG{'TERM'} = 'cleanup';
88
89if ($#ARGV > 0) {
90    print STDERR "usage: ${whoami} [username]\n";
91    exit(1);
92}
93
94if ($< != 0) {
95    print STDERR "${whoami}: Error: you must be root to use ${whoami}\n";
96    exit(1);
97}
98
99&open_files;
100
101if ($#ARGV == 0) {
102    # Username was given as a parameter
103    $login_name = pop(@ARGV);
104} else {
105    # Get the user name from the user
106    $login_name = &get_login_name;
107}
108
109if (($pw_ent = &check_login_name($login_name)) eq '0') {
110    print STDERR "${whoami}: Error: User ${login_name} not in password database\n";
111    exit 1;
112}
113
114($name, $password, $uid, $gid, $class, $change, $expire, $gecos, $home_dir,
115 $shell) = split(/:/, $pw_ent);
116
117if ($uid == 0) {
118    print "${whoami}: Sorry, I'd rather not remove a user with a uid of 0.\n";
119    exit 1;
120}
121
122print "Matching password entry:\n\n$pw_ent\n\n";
123
124$ans = &get_yn("Is this the entry you wish to remove? ");
125
126if ($ans eq 'N') {
127    print "User ${login_name} not removed.\n";
128    exit 0;
129}
130
131#
132# Get owner of user's home directory; don't remove home dir if not
133# owned by $login_name
134
135$remove_directory = 1;
136
137if (-l $home_dir) {
138    $real_home_dir = &resolvelink($home_dir);
139} else {
140    $real_home_dir = $home_dir;
141}
142
143#
144# If home_dir is a symlink and points to something that isn't a directory,
145# or if home_dir is not a symlink and is not a directory, don't remove
146# home_dir -- seems like a good thing to do, but probably isn't necessary...
147if (((-l $home_dir) && ((-e $real_home_dir) && !(-d $real_home_dir))) ||
148    (!(-l $home_dir) && !(-d $home_dir))) {
149    print STDERR "${whoami}: Home ${home_dir} is not a directory, so it won't be removed\n";
150    $remove_directory = 0;
151}
152
153if (length($real_home_dir) && -d $real_home_dir) {
154    $dir_owner = (stat($real_home_dir))[4]; # UID
155    if ($dir_owner != $uid) {
156	print STDERR "${whoami}: Home dir ${real_home_dir} is not owned by ${login_name} (uid ${dir_owner})\n";
157	$remove_directory = 0;
158    }
159}
160
161if ($remove_directory) {
162    $ans = &get_yn("Remove user's home directory ($home_dir)? ");
163    if ($ans eq 'N') {
164	$remove_directory = 0;
165    }
166}
167
168#exit 0 if $debug;
169
170#
171# Remove the user's crontab, if there is one
172# (probably needs to be done before password databases are updated)
173
174if (-e "$crontab_dir/$login_name") {
175    print STDERR "Removing user's crontab:";
176    system('/usr/bin/crontab', '-u', $login_name, '-r');
177    print STDERR " done.\n";
178}
179
180#
181# Remove the user's at jobs, if any
182# (probably also needs to be done before password databases are updated)
183
184&remove_at_jobs($login_name, $uid);
185
186#
187# Copy master password file to new file less removed user's entry
188
189&update_passwd_file;
190
191#
192# Remove the user from all groups in /etc/group
193
194&update_group_file($login_name);
195
196#
197# Remove the user's home directory
198
199if ($remove_directory) {
200    print STDERR "Removing user's home directory ($home_dir):";
201    &remove_dir($home_dir);
202    print STDERR " done.\n";
203}
204
205#
206# Remove the user's incoming mail file
207
208if (-e "$mail_dir/$login_name" || -l "$mail_dir/$login_name") {
209    print STDERR "Removing user's incoming mail file ($mail_dir/$login_name):";
210    unlink "$mail_dir/$login_name" ||
211	print STDERR "\n${whoami}: warning: unlink on $mail_dir/$login_name failed ($!) - continuing\n";
212    print STDERR " done.\n";
213}
214
215#
216# All done!
217
218exit 0;
219
220sub get_login_name {
221    #
222    # Get new user's name
223    local($done, $login_name);
224
225    for ($done = 0; ! $done; ) {
226	print "Enter login name for user to remove: ";
227	$login_name = <>;
228	chomp $login_name;
229	if (!($login_name =~ /^\w+$/)) {
230	    print STDERR "Sorry, login name must contain alphanumeric characters only.\n";
231	} elsif (length($login_name) > 31 || length($login_name) == 0) {
232	    print STDERR "Sorry, login name must be 31 characters or less.\n";
233	} else {
234	    $done = 1;
235	}
236    }
237
238    print "User name is ${login_name}\n" if $debug;
239    return($login_name);
240}
241
242sub check_login_name {
243    #
244    # Check to see whether login name is in password file
245    local($login_name) = @_;
246    local($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire,
247	  $Mgecos, $Mhome_dir, $Mshell);
248    local($i);
249
250    seek(MASTER_PW, 0, 0);
251    while ($i = <MASTER_PW>) {
252	chomp $i;
253	($Mname, $Mpassword, $Muid, $Mgid, $Mclass, $Mchange, $Mexpire,
254	 $Mgecos, $Mhome_dir, $Mshell) = split(/:/, $i);
255	if ($Mname eq $login_name) {
256	    seek(MASTER_PW, 0, 0);
257	    return($i);		# User is in password database
258	}
259    }
260    seek(MASTER_PW, 0, 0);
261
262    return '0';			# User wasn't found
263}
264
265sub get_yn {
266    #
267    # Get a yes or no answer; return 'Y' or 'N'
268    local($prompt) = @_;
269    local($done, $ans);
270
271    for ($done = 0; ! $done; ) {
272	print $prompt;
273	$ans = <>;
274	chomp $ans;
275	$ans =~ tr/a-z/A-Z/;
276	if (!($ans =~ /^[YN]/)) {
277	    print STDERR "Please answer (y)es or (n)o.\n";
278	} else {
279	    $done = 1;
280	}
281    }
282
283    return(substr($ans, 0, 1));
284}
285
286sub update_passwd_file {
287    local($skipped, $i);
288
289    print STDERR "Updating password file,";
290    seek(MASTER_PW, 0, 0);
291    $skipped = 0;
292    while ($i = <MASTER_PW>) {
293	chomp($i);
294	if ($i ne $pw_ent) {
295	    print NEW_PW "$i\n";
296	} else {
297	    print STDERR "Dropped entry for $login_name\n" if $debug;
298	    $skipped = 1;
299	}
300    }
301    close(NEW_PW);
302    seek(MASTER_PW, 0, 0);
303
304    if ($skipped == 0) {
305	print STDERR "\n${whoami}: Whoops! Didn't find ${login_name}'s entry second time around!\n";
306	exit 1;
307    }
308
309    #
310    # Run pwd_mkdb to install the updated password files and databases
311
312    print STDERR " updating databases,";
313    system('/usr/sbin/pwd_mkdb', '-p', ${passwd_tmp});
314    print STDERR " done.\n";
315
316    close(MASTER_PW);		# Not useful anymore
317}
318
319sub update_group_file {
320    local($login_name) = @_;
321
322    local($i, $j, $grmember_list, $new_grent);
323    local($grname, $grpass, $grgid, $grmember_list, @grmembers);
324
325    print STDERR "Updating group file:";
326    local($group_perms, $group_uid, $group_gid) =
327	(stat(GROUP))[2, 4, 5]; # File Mode, uid, gid
328    open(NEW_GROUP, ">$new_group_file") ||
329	die "\n${whoami}: Error: couldn't open ${new_group_file}: $!\n";
330    chmod($group_perms, $new_group_file) ||
331	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;
332    chown($group_uid, $group_gid, $new_group_file) ||
333	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";
334    while ($i = <GROUP>) {
335	if (!($i =~ /$login_name/)) {
336	    # Line doesn't contain any references to the user, so just add it
337	    # to the new file
338	    print NEW_GROUP $i;
339	} else {
340	    #
341	    # Remove the user from the group
342	    chomp $i;
343	    ($grname, $grpass, $grgid, $grmember_list) = split(/:/, $i);
344	    @grmembers = split(/,/, $grmember_list);
345	    undef @new_grmembers;
346	    local(@new_grmembers);
347	    foreach $j (@grmembers) {
348		if ($j ne $login_name) {
349		    push(@new_grmembers, $j);
350		} elsif ($debug) {
351		    print STDERR "Removing $login_name from group $grname\n";
352		}
353	    }
354	    if ($grname eq $login_name && $#new_grmembers == -1) {
355		# Remove a user's personal group if empty
356		print STDERR "Removing group $grname -- personal group is empty\n";
357	    } else {
358		$grmember_list = join(',', @new_grmembers);
359		$new_grent = join(':', $grname, $grpass, $grgid, $grmember_list);
360		print NEW_GROUP "$new_grent\n";
361	    }
362	}
363    }
364    close(NEW_GROUP);
365    rename($new_group_file, $group_file) || # Replace old group file with new
366	die "\n${whoami}: error: couldn't rename $new_group_file to $group_file ($!)\n";
367    close(GROUP);			# File handle is worthless now
368    print STDERR " done.\n";
369}
370
371sub remove_dir {
372    # Remove the user's home directory
373    local($dir) = @_;
374    local($linkdir);
375
376    if (-l $dir) {
377	$linkdir = &resolvelink($dir);
378	# Remove the symbolic link
379	unlink($dir) ||
380	    warn "${whoami}: Warning: could not unlink symlink $dir: $!\n";
381	if (!(-e $linkdir)) {
382	    #
383	    # Dangling symlink - just return now
384	    return;
385	}
386	# Set dir to be the resolved pathname
387	$dir = $linkdir;
388    }
389    if (!(-d $dir)) {
390	print STDERR "${whoami}: Warning: $dir is not a directory\n";
391	unlink($dir) || warn "${whoami}: Warning: could not unlink $dir: $!\n";
392	return;
393    }
394    system('/bin/rm', '-rf', $dir);
395}
396
397sub remove_at_jobs {
398    local($login_name, $uid) = @_;
399    local($i, $owner, $found);
400
401    $found = 0;
402    opendir(ATDIR, $atjob_dir) || return;
403    while ($i = readdir(ATDIR)) {
404	next if $i eq '.';
405	next if $i eq '..';
406	next if $i eq '.lockfile';
407
408	$owner = (stat("$atjob_dir/$i"))[4]; # UID
409	if ($uid == $owner) {
410	    if (!$found) {
411		print STDERR "Removing user's at jobs:";
412		$found = 1;
413	    }
414	    # Use atrm to remove the job
415	    print STDERR " $i";
416	    system('/usr/bin/atrm', $i);
417	}
418    }
419    closedir(ATDIR);
420    if ($found) {
421	print STDERR " done.\n";
422    }
423}
424
425sub resolvelink {
426    local($path) = @_;
427    local($l);
428
429    while (-l $path && -e $path) {
430	if (!defined($l = readlink($path))) {
431	    die "${whoami}: readlink on $path failed (but it should have worked!): $!\n";
432	}
433	if ($l =~ /^\//) {
434	    # Absolute link
435	    $path = $l;
436	} else {
437	    # Relative link
438	    $path =~ s/\/[^\/]+\/?$/\/$l/; # Replace last component of path
439	}
440    }
441    return $path;
442}
443