tbacct.in 16.8 KB
Newer Older
1
2
3
4
#!/usr/bin/perl -wT

#
# EMULAB-COPYRIGHT
5
# Copyright (c) 2000-2005 University of Utah and the Flux Group.
6
7
8
9
# All rights reserved.
#
use English;
use Getopt::Std;
10

11
12
13
14
15
16
17
18
19
#
# Deal with user accounts. This script does not deal with group stuff.
# Just add/del/mod/passwd/freeze/thaw/ stuff. We do give users an
# initial group of course, which will be guest if not in any groups.
#
# This script is setuid. We farm stuff out to subscripts though, and need
# to be wary of what the UID/EUID is when those scripts are invoked. The
# subscripts are not generally setuid, but of course the web interface
# allows users to do things on behalf of other users, and we want to track
20
# that in the audit log.
21
22
#
# This script always does the right thing ...
23
#
24
25
sub usage()
{
26
    print("Usage: tbacct [-f] [-b] ".
27
	  "<add|del|mod|passwd|wpasswd|freeze|thaw> <user>\n");
28
29
    exit(-1);
}
30
31
32
my $optlist = "fb";
my $force   = 0;
my $batch   = 0;
33
34
35
36
37
38
39
40
41
42

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS	= "@TBOPSEMAIL@";
my $TBLOGS	= "@TBLOGSEMAIL@";
my $CONTROL	= "@USERNODE@";
my $BOSSNODE	= "@BOSSNODE@";
my $WITHSFS	= @SFSSUPPORT@;
43
my $WIKISUPPORT = @WIKISUPPORT@;
44

45
46
47
my $SAMBANODE	= "fs";  # DNS makes this do the right thing in E-in-E.
my $SMBPASSWD	= "/usr/local/bin/smbpasswd";

48
49
50
51
52
53
54
55
my $HOMEDIR	= "/users";
my $USERPATH	= "$TB/bin";
my $ADDKEY	= "$TB/sbin/addpubkey";
my $USERADD	= "/usr/sbin/pw useradd";
my $USERDEL	= "/usr/sbin/pw userdel";
my $USERMOD	= "/usr/sbin/pw usermod";
my $CHPASS	= "/usr/bin/chpass";
my $SFSKEYGEN	= "/usr/local/bin/sfskey gen";
56
57
my $SETGROUPS	= "$TB/sbin/setgroups";
my $GENELISTS	= "$TB/sbin/genelists";
58
my $MKUSERCERT	= "$TB/sbin/mkusercert";
59
my $SFSUPDATE	= "$TB/sbin/sfskey_update";
60
my $PBAG	= "$TB/sbin/paperbag";
61
my $EXPORTSSETUP= "$TB/sbin/exports_setup";
62
63
my $ADDWIKIUSER = "$TB/sbin/addwikiuser";
my $DELWIKIUSER = "$TB/sbin/delwikiuser";
64
my $NOLOGIN	= "/sbin/nologin";
65
my $SSH		= "$TB/bin/sshtb";
66
67
68
my $SAVEUID	= $UID;
my $NOSUCHUSER  = 67;
my $USEREXISTS  = 65;
69
70
71
# Nasty. Should do this with /etc/pw.conf shellpath.
my %shellpaths  = ("csh"  => "/bin/csh", "sh" => "/bin/sh",
		   "tcsh" => "/bin/tcsh", "bash" => "/usr/local/bin/bash");
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

my $errors      = 0;
my $sfsupdate   = 0;
my @row;
my $query_result;

#
# We don't want to run this script unless its the real version.
#
if ($EUID != 0) {
    die("*** $0:\n".
	"    Must be setuid! Maybe its a development version?\n");
}

#
# This script is setuid, so please do not run it as root. Hard to track
# what has happened.
89
#
90
91
92
93
94
95
96
if ($UID == 0) {
    die("*** $0:\n".
	"    Please do not run this as root! Its already setuid!\n");
}

#
# Untaint the path
97
#
98
99
100
101
102
103
104
105
106
$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

#
# Turn off line buffering on output
#
$| = 1;

#
107
# Load the Testbed support stuff.
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#
use lib "@prefix@/lib";
use libaudit;
use libdb;
use libtestbed;

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"f"})) {
    $force = 1;
}
125
126
127
if (defined($options{"b"})) {
    $batch = 1;
}
128
129
130
if (@ARGV != 2) {
    usage();
}
131
132
my $cmd  = $ARGV[0];
my $user = $ARGV[1];
133
134
135
136

#
# Untaint the arguments.
#
137
if ($user =~ /^([-\w]+)$/i) {
138
139
140
141
142
    $user = $1;
}
else {
    die("Tainted argument: $user\n");
}
143
if ($cmd =~ /^(add|del|mod|freeze|passwd|wpasswd|thaw)$/) {
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
    $cmd = $1;
}
else {
    usage();
}

# Only admins can use force mode.
if ($force && ! TBAdmin($UID)) {
    fatal("Only admins can use force mode!");
}

#
# This script is always audited. Mail is sent automatically upon exit.
#
if (AuditStart(0)) {
    #
    # Parent exits normally
    #
    exit(0);
}

#
# Get the user info (the user being operated on).
#
$query_result =
    DBQueryFatal("select u.usr_pswd,u.unix_uid,u.usr_name, ".
170
171
		 " u.usr_email,u.status,u.webonly,u.usr_shell,admin, ".
		 " u.usr_w_pswd,u.wikionly ".
172
		 "from users as u ".
173
174
175
176
177
178
179
		 "where u.uid='$user'");

if ($query_result->numrows == 0) {
    fatal("$user is not in the DB. This is bad.\n");
}
@row            = $query_result->fetchrow_array();
my $pswd        = $row[0];
180
my $user_number = $row[1];
181
182
183
184
my $fullname    = $row[2];
my $user_email  = $row[3];
my $status      = $row[4];
my $webonly     = $row[5];
185
my $usr_shell   = $row[6];
186
my $usr_admin   = $row[7];
187
my $wpswd       = $row[8];
188
my $wikionly    = $row[9];
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

#
# Get the users earliest project membership to use as the default group
# for the case that the account is being (re)created. We convert that to
# the unix info.
#
my $default_groupname;
my $default_groupgid;

$query_result =
    DBQueryFatal("select m.pid from group_membership as m ".
		 "where m.uid='$user' and m.pid=m.gid and m.trust!='none' ".
		 "order by date_approved asc limit 1");

if (my ($defpid) = $query_result->fetchrow_array) {
    if (! TBGroupUnixInfo($defpid, $defpid,
			  \$default_groupgid, \$default_groupname)) {
	fatal("No info for default project $defpid!");
    }
}
else {
    print "No group membership for $user; using the guest group!\n";
211

212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
    ($default_groupname,undef,$default_groupgid,undef) = getgrnam("guest");
}

#
# Now dispatch operation.
#
SWITCH: for ($cmd) {
    /^add$/ && do {
	AddUser();
	last SWITCH;
    };
    /^del$/ && do {
	DelUser();
	last SWITCH;
    };
    /^passwd$/ && do {
	UpdatePassword();
	last SWITCH;
    };
231
232
233
234
    /^wpasswd$/ && do {
	UpdateWindowsPassword();
	last SWITCH;
    };
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
    /^mod$/ && do {
	UpdateUser();
	last SWITCH;
    };
    /^freeze$/ && do {
	FreezeUser();
	last SWITCH;
    };
    /^thaw$/ && do {
	ThawUser();
	last SWITCH;
    };
}

# Always do this!
CheckDotFiles();

#
# Invoke as real user for auditing (and cause of perl).
#
if ($sfsupdate) {
    $EUID = $UID;
    system($SFSUPDATE) == 0
	or fatal("$SFSUPDATE failed!");
    $EUID = 0;
}

#
# Now schedule account updates on all the nodes that this person has
# an account on.
#
TBNodeUpdateAccountsByUID($user);

exit(0);

#
# Add new user.
#
sub AddUser()
{
    #
    # Check status. Only active users get accounts built.
    #
278
    if ($webonly || $wikionly || $status ne USERSTATUS_ACTIVE) {
279
280
281
	if ($webonly) {
	    return 0;
	}
282
283
284
285
286
287
288
289
290
291
	if ($wikionly) {
	    $EUID = $UID;

	    # And to the wiki if enabled.
	    system("$ADDWIKIUSER $user")
		if ($WIKISUPPORT && !$batch);
	    
	    $EUID = 0;
	    return 0;
	}
292
293
	fatal("$user is not active! Cannot build an account!");
    }
294

295
296
297
    $UID = 0;
    if (system("egrep -q -s '^${user}:' /etc/passwd")) {
	print "Adding user $user ($user_number) to local node.\n";
298

299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
	if (system("$USERADD $user -u $user_number -c \"$fullname\" ".
		   "-k /usr/share/skel -h - -m -d $HOMEDIR/$user ".
		   "-g $default_groupname -s $PBAG")) {
	    fatal("Could not add user $user to local node.");
	}
    }

    #
    # Quote special chars for ssh and the shell on the other side
    #
    $fullname =~ s/\"/\'/g;
    $fullname =~ s/([^\\])([\'\"\(\)])/$1\\$2/g;

    if ($CONTROL ne $BOSSNODE) {
	print "Adding user $user ($user_number) to $CONTROL.\n";

	if (system("$SSH -host $CONTROL ".
316
		   "'$USERADD $user -u $user_number -c \"$fullname\" ".
317
318
		   "-k /usr/share/skel -h - -m -d $HOMEDIR/$user ".
		   "-g $default_groupname -s /bin/tcsh'")) {
319
	    if (($? >> 8) != $USEREXISTS) {
320
321
322
323
324
325
326
327
328
329
330
331
332
		fatal("Could not add user $user ($user_number) to $CONTROL.");
	    }
	}
    }
    $UID = $SAVEUID;

    #
    # Do the ssh thing. Invoke as real user for auditing.
    #
    $EUID = $UID;
    if (system("$ADDKEY -i $user")) {
	fatal("Could not generate initial ssh key for $user");
    }
333
    # Add to elists.
334
335
    system("$GENELISTS -u $user")
	if (! $batch);
336

337
338
339
340
    # And to the wiki if enabled.
    system("$ADDWIKIUSER $user")
	if ($WIKISUPPORT && !$batch);

341
342
343
    # Generate the SSL cert for the user.
    system("$MKUSERCERT $user");

344
345
346
347
348
349
350
351
352
    #
    # Must update the exports file or else nodes will complain.  There
    # is a bit of race in here since this update happens after the
    # user is marked "active", and in that time a node could suck over
    # the account info, but not be able to mount the directory. Short
    # window though. Do not worry about the exit value. Note that this
    # could hang for a while if another update is in progress. Hmm, I
    # do not like this.
    #
353
354
355
356
    if (! $batch) {
	print "Updating exports file.\n";
	system("$EXPORTSSETUP");
    }
357
358
359
360
361
362
    $EUID = 0;

    # SFS key.
    if ($CONTROL ne $BOSSNODE) {
	GenerateSFSKey();
    }
363

364
365
366
367
368
369
370
371
372
    return UpdatePassword();
}

#
# Delete a user.
#
sub DelUser()
{
    #
373
    # Only admin people can do this.
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
    #
    if (! TBAdmin($UID)) {
	fatal("You do not have permission to delete user $user.");
    }
    #
    # Check status. Active indicates something is wrong.
    #
    if (!$force && $status eq USERSTATUS_ACTIVE) {
	fatal("$user is still active! Cannot delete the account!");
    }

    print "Deleting user $user ($user_number) from local node.\n";

    $UID = 0;

    if (system("$USERDEL $user")) {
	if (($? >> 8) != $NOSUCHUSER) {
	    fatal("Could not remove user $user from local node.");
	}
    }

    if ($CONTROL ne $BOSSNODE) {
	print "Removing user $user from $CONTROL\n";
397

398
399
400
401
402
403
404
	if (system("$SSH -host $CONTROL '$USERDEL $user'")) {
	    if (($? >> 8) != $NOSUCHUSER) {
		fatal("Could not remove user $user from $CONTROL.");
	    }
	}
    }
    $UID = $SAVEUID;
405
406

    $EUID = $UID;
407
408
409
410
411
412
413
    #
    # Must update the exports file or else nodes will complain.  Note
    # that this could hang for a while if another update is in progress. 
    #
    print "Updating exports file.\n";
    system("$EXPORTSSETUP");

414
    # Remove from elists.
415
    system("$GENELISTS -u $user");
416
417
418
419
420

    # And to the wiki if enabled.
    system("$DELWIKIUSER $user")
	if ($WIKISUPPORT);
    
421
    $EUID = 0;
422

423
424
425
426
427
428
    $sfsupdate = 1;
    return 0;
}

#
# Change a password for the user on the control node. The local password
429
# is not touched!
430
431
432
433
#
sub UpdatePassword()
{
    # shell escape.
434
    $pswd     =~ s/\$/\\\$/g;
435

436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
    #
    # Check status. Ignore if user is not active.
    #
    if ($status ne USERSTATUS_ACTIVE) {
	print("$user is not active! Not updating the password!\n");
	return 0;
    }

    $UID = 0;
    if ($CONTROL ne $BOSSNODE) {
	print "Updating user $user password on $CONTROL.\n";

	if (system("$SSH -host $CONTROL $CHPASS -p '$pswd' $user")) {
	    fatal("Could not change password for user $user on $CONTROL!");
	}
    }
    $UID = $SAVEUID;
453
454
455
456
457
458
459

    $EUID = $UID;
    # And to the wiki if enabled.
    system("$ADDWIKIUSER -u $user")
	if ($WIKISUPPORT);
    $EUID = 0;
    
460
461
462
    return 0;
}

463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
#
# Change a Windows password for the user on the Samba server node.
# The local password is not touched!
#
sub UpdateWindowsPassword()
{
    # shell escape.
    $wpswd     =~ s/\$/\\\$/g;

    #
    # Check status. Ignore if user is not active.
    #
    if ($status ne USERSTATUS_ACTIVE) {
	print("$user is not active! Not updating the password!\n");
	return 0;
    }

    $UID = 0;
    print "Updating user $user Samba password on $SAMBANODE.\n";
    # -s = silent, -a = add user if necessary.
    open( SPCMD, "| $SSH -host $SAMBANODE $SMBPASSWD -s -a $user")
        || fatal("Opening $SMBPASSWD pipe, user $user on $SAMBANODE: $! $?");
    local $SIG{PIPE} = sub { die "smbpasswd spooler pipe broke" };
    print SPCMD "$wpswd\n$wpswd\n";
    close SPCMD 
	|| fatal("Closing $SMBPASSWD pipe, user $user on $SAMBANODE: $! $?");

    $UID = $SAVEUID;
    return 0;
}

494
495
496
497
498
499
500
501
502
503
#
# Update user info. Allow for optional shell change for freeze/thaw.
#
sub UpdateUser(;$)
{
    my ($freezeopt) = @_;
    my $locshellarg = "";
    my $remshellarg = "";

    #
504
    # Sanity check.
505
506
507
508
509
    #
    if ($webonly) {
	return 0;
    }
    if (!defined($freezeopt) && ($status ne USERSTATUS_ACTIVE)) {
510
	fatal("$user is not active! Cannot update the account!");
511
512
513
    }

    # Shell is different on local vs control node.
514
515
516
517
518
    if (defined($freezeopt) && $freezeopt) {
	$locshellarg = "-s $NOLOGIN";
	$remshellarg = "-s $NOLOGIN";
    }
    else {
519
520
521
	# Leave local shell alone if an admin.
	$locshellarg = "-s $PBAG"
	    if (!$usr_admin);
522
523
524
525

	if (!defined($usr_shell) ||
	    !exists($shellpaths{$usr_shell})) {
	    $remshellarg = "-s " . $shellpaths{"tcsh"};
526
	}
527
528
	else  {
	    $remshellarg = "-s " . $shellpaths{$usr_shell};
529
530
531
	}
    }
    print "Updating user $user ($user_number) on local node.\n";
532

533
534
535
536
    $UID = 0;
    if (system("$USERMOD $user $locshellarg -c \"$fullname\" ")) {
	fatal("Could not modify user $user on local node.");
    }
537

538
539
540
541
542
543
544
545
    #
    # Quote special chars for ssh and the shell on the other side
    #
    $fullname =~ s/\"/\'/g;
    $fullname =~ s/([^\\])([\'\"\(\)])/$1\\$2/g;

    if ($CONTROL ne $BOSSNODE) {
	print "Updating user $user ($user_number) on $CONTROL\n";
546

547
	if (system("$SSH -host $CONTROL ".
548
		   "'$USERMOD $user $remshellarg -c \"$fullname\"'")) {
549
550
551
552
	    fatal("Could not modify user $user record on $CONTROL.");
	}
    }
    $UID = $SAVEUID;
553
554

    $EUID = $UID;
555
    # Update elists in case email changed.
556
    system("$GENELISTS -m -u $user");
557
    $EUID = 0;
558

559
560
561
562
563
564
565
566
567
    return 0;
}

#
# Freeze a user.
#
sub FreezeUser()
{
    #
568
    # Only admin people can do this.
569
570
571
572
573
574
575
576
577
578
579
    #
    if (! TBAdmin($UID)) {
	fatal("You do not have permission to freeze user $user.");
    }
    #
    # Check status.
    #
    if ($status ne USERSTATUS_FROZEN) {
	fatal("$user is still active! Cannot freeze the account!");
    }
    $sfsupdate = 1;
580

581
582
583
584
585
586
587
588
589
    return UpdateUser(1);
}

#
# Thaw a user.
#
sub ThawUser()
{
    #
590
    # Only admin people can do this.
591
592
593
594
595
596
597
598
599
600
601
    #
    if (! TBAdmin($UID)) {
	fatal("You do not have permission to thaw user $user.");
    }
    #
    # Check status.
    #
    if ($status ne USERSTATUS_ACTIVE) {
	fatal("$user is not active! Cannot thaw the account!");
    }
    $sfsupdate = 1;
602

603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
    return UpdateUser(0);
}

#
# Check dot files. We do this over and over ...
#
sub CheckDotFiles()
{
    my $forward = "$HOMEDIR/$user/.forward";
    my $cshrc   = "$HOMEDIR/$user/.cshrc";
    my $profile = "$HOMEDIR/$user/.profile";

    if (! -d "$HOMEDIR/$user") {
	return 0;
    }

    # As the user.
    $UID = $user_number;

    #
    # Set up a .forward file so that any email to them gets forwarded off.
    #
    if (! -e $forward) {
	print "Setting up .forward file for $user.\n";

	if (system("echo \"$user_email\" > $forward")) {
		fatal("Could not create $forward!");
	}
	chmod(0644, "$HOMEDIR/$user/.forward") or
		fatal("Could not chmod $forward: $!");
633
634
635
636
637
638
639
640
641
	$fileowner= (stat($forward))[4];
	$dochown=0;
	if ($fileowner==0) {
	    chown($user_number,$default_groupgid,"$HOMEDIR/$user/.forward") or
	      do {
		  warn("Could not chown $forward: $!");
		  $dochown=1;
	      };
	}
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
    }

    #
    # Add testbed path to .cshrc and .profile.
    #
    my $cpathstr = "set path = ($USERPATH \$path)";
    if (-e $cshrc && system("egrep -q -s '$USERPATH' $cshrc")) {
	system("echo '$cpathstr' >> $cshrc");
    }

    my $spathstr = "PATH=$USERPATH:\$PATH";
    if (-e $profile && system("egrep -q -s '$USERPATH' $profile")) {
	system("echo '$spathstr' >> $profile");
    }
    $UID = $SAVEUID;

658
659
660
661
662
    if (defined($dochown) && $dochown!=0) {
	chown($user_number,$default_groupgid,"$HOMEDIR/$user/.forward") or
	  warn("Could not chown $forward: $!");
    }

663
664
665
666
667
668
669
670
671
    return 0;
}

#
# Do SFS stuff. Might move this out to its own script at some point.
#
sub GenerateSFSKey()
{
    my $sfsdir  = "$HOMEDIR/$user/.sfs";
672

673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
    #
    # Set up the sfs key, but only if not done so already.
    # This has to be done from root because the sfs_users file needs
    # to be updated (and "sfskey register" won't work because it
    # prompts for the user's UNIX password if not run from root.)
    #
    if ($WITHSFS && ! -e "$sfsdir/identity") {
	if (! -e "$sfsdir" ) {
	    print "Setting up sfs configuration for $user.\n";

	    mkdir("$sfsdir", 0700) or
		fatal("Could not mkdir $sfsdir: $!");
	    chown($user_number, $default_groupgid, "$sfsdir") or
		fatal("Could not chown $sfsdir: $!");
	}

	print "Generating sfs key\n";
	$UID = 0;
691
692
 	if (system("$SSH -host $CONTROL '$SFSKEYGEN -KPn ".
 		   "$user\@ops.emulab.net $sfsdir/identity'")) {
693
	    fatal("Failure in sfskey gen!");
694
695
696
697
698
699
	}
	# Version 7 stuff for later.
	#if (system("$SSH -host $CONTROL '$SFSKEYGEN -KP ".
	#	    "-l $user\@ops.emulab.net $sfsdir/identity'")) {
	#    fatal("Failure in sfskey gen!");
	#}
700
	$UID = $SAVEUID;
701

702
703
704
705
	chown($user_number, $default_groupgid, "$sfsdir/identity") or
	    fatal("Could not chown $sfsdir/identity: $!");
	chmod(0600, "$sfsdir/identity") or
	    fatal("Could not chmod $sfsdir/identity: $!");
706

707
708
709
710
711
712
713
	#
	# Grab a copy for the DB. Causes an SFS update key to run so
	# that key is inserted into the files.
	#
	my $ident = `cat $sfsdir/identity`;

	if ($ident =~ /.*,.*,.*,(.*),(.*)/) {
714
715
716
717
718
719
720
	    # Version 6
	    DBQueryFatal("replace into user_sfskeys ".
			 "values ('$user', '$2', '${user}:${1}:${user}::', ".
			 "now())");
	}
	elsif ($ident =~ /.*:.*:.*:(.*):(.*)/) {
	    # Version 7
721
722
723
724
725
726
727
728
729
730
731
	    DBQueryFatal("replace into user_sfskeys ".
			 "values ('$user', '$2', '${user}:${1}:${user}::', ".
			 "now())");
	}
	else {
	    warn("*** $0:\n".
		 "    Bad emulab SFS public key\n");
	}
	$sfsupdate = 1;
    }
    return 0;
732
}
733
734
735
736
737
738
739

sub fatal($) {
    my($mesg) = $_[0];

    die("*** $0:\n".
	"    $mesg\n");
}