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

#
# EMULAB-COPYRIGHT
5
# Copyright (c) 2000-2004 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
28
29
	  "<add|del|mod|passwd|freeze|thaw> <user>\n");
    exit(-1);
}
30
31
32
my $optlist = "fb";
my $force   = 0;
my $batch   = 0;
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

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

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";
52
53
my $SETGROUPS	= "$TB/sbin/setgroups";
my $GENELISTS	= "$TB/sbin/genelists";
54
my $MKUSERCERT	= "$TB/sbin/mkusercert";
55
my $SFSUPDATE	= "$TB/sbin/sfskey_update";
56
my $PBAG	= "$TB/sbin/paperbag";
57
my $EXPORTSSETUP= "$TB/sbin/exports_setup";
58
my $NOLOGIN	= "/sbin/nologin";
59
my $SSH		= "$TB/bin/sshtb";
60
61
62
my $SAVEUID	= $UID;
my $NOSUCHUSER  = 67;
my $USEREXISTS  = 65;
63
64
65
# 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");
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

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.
83
#
84
85
86
87
88
89
90
if ($UID == 0) {
    die("*** $0:\n".
	"    Please do not run this as root! Its already setuid!\n");
}

#
# Untaint the path
91
#
92
93
94
95
96
97
98
99
100
$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;

#
101
# Load the Testbed support stuff.
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#
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;
}
119
120
121
if (defined($options{"b"})) {
    $batch = 1;
}
122
123
124
if (@ARGV != 2) {
    usage();
}
125
126
my $cmd  = $ARGV[0];
my $user = $ARGV[1];
127
128
129
130

#
# Untaint the arguments.
#
131
if ($user =~ /^([-\w]+)$/i) {
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
    $user = $1;
}
else {
    die("Tainted argument: $user\n");
}
if ($cmd =~ /^(add|del|mod|freeze|passwd|thaw)$/) {
    $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, ".
164
		 " u.usr_email,u.status,u.webonly,u.usr_shell,admin ".
165
		 "from users as u ".
166
167
168
169
170
171
172
		 "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];
173
my $user_number = $row[1];
174
175
176
177
my $fullname    = $row[2];
my $user_email  = $row[3];
my $status      = $row[4];
my $webonly     = $row[5];
178
my $usr_shell   = $row[6];
179
my $usr_admin   = $row[7];
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

#
# 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";
202

203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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
    ($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;
    };
    /^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.
    #
    if ($webonly || $status ne USERSTATUS_ACTIVE) {
	if ($webonly) {
	    return 0;
	}
	fatal("$user is not active! Cannot build an account!");
    }
271

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

276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
	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 ".
293
		   "'$USERADD $user -u $user_number -c \"$fullname\" ".
294
295
		   "-k /usr/share/skel -h - -m -d $HOMEDIR/$user ".
		   "-g $default_groupname -s /bin/tcsh'")) {
296
	    if (($? >> 8) != $USEREXISTS) {
297
298
299
300
301
302
303
304
305
306
307
308
309
		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");
    }
310
    # Add to elists.
311
312
    system("$GENELISTS -u $user")
	if (! $batch);
313

314
315
316
    # Generate the SSL cert for the user.
    system("$MKUSERCERT $user");

317
318
319
320
321
322
323
324
325
    #
    # 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.
    #
326
327
328
329
    if (! $batch) {
	print "Updating exports file.\n";
	system("$EXPORTSSETUP");
    }
330
331
332
333
334
335
    $EUID = 0;

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

337
338
339
340
341
342
343
344
345
    return UpdatePassword();
}

#
# Delete a user.
#
sub DelUser()
{
    #
346
    # Only admin people can do this.
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
    #
    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";
370

371
372
373
374
375
376
377
	if (system("$SSH -host $CONTROL '$USERDEL $user'")) {
	    if (($? >> 8) != $NOSUCHUSER) {
		fatal("Could not remove user $user from $CONTROL.");
	    }
	}
    }
    $UID = $SAVEUID;
378
379

    $EUID = $UID;
380
381
382
383
384
385
386
    #
    # 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");

387
    # Remove from elists.
388
    system("$GENELISTS -u $user");
389
    $EUID = 0;
390

391
392
393
394
395
396
    $sfsupdate = 1;
    return 0;
}

#
# Change a password for the user on the control node. The local password
397
# is not touched!
398
399
400
401
#
sub UpdatePassword()
{
    # shell escape.
402
    $pswd     =~ s/\$/\\\$/g;
403

404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
    #
    # 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;
    return 0;
}

#
# Update user info. Allow for optional shell change for freeze/thaw.
#
sub UpdateUser(;$)
{
    my ($freezeopt) = @_;
    my $locshellarg = "";
    my $remshellarg = "";

    #
434
    # Sanity check.
435
436
437
438
439
    #
    if ($webonly) {
	return 0;
    }
    if (!defined($freezeopt) && ($status ne USERSTATUS_ACTIVE)) {
440
	fatal("$user is not active! Cannot update the account!");
441
442
443
    }

    # Shell is different on local vs control node.
444
445
446
447
448
    if (defined($freezeopt) && $freezeopt) {
	$locshellarg = "-s $NOLOGIN";
	$remshellarg = "-s $NOLOGIN";
    }
    else {
449
450
451
	# Leave local shell alone if an admin.
	$locshellarg = "-s $PBAG"
	    if (!$usr_admin);
452
453
454
455

	if (!defined($usr_shell) ||
	    !exists($shellpaths{$usr_shell})) {
	    $remshellarg = "-s " . $shellpaths{"tcsh"};
456
	}
457
458
	else  {
	    $remshellarg = "-s " . $shellpaths{$usr_shell};
459
460
461
	}
    }
    print "Updating user $user ($user_number) on local node.\n";
462

463
464
465
466
    $UID = 0;
    if (system("$USERMOD $user $locshellarg -c \"$fullname\" ")) {
	fatal("Could not modify user $user on local node.");
    }
467

468
469
470
471
472
473
474
475
    #
    # 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";
476

477
	if (system("$SSH -host $CONTROL ".
478
		   "'$USERMOD $user $remshellarg -c \"$fullname\"'")) {
479
480
481
482
	    fatal("Could not modify user $user record on $CONTROL.");
	}
    }
    $UID = $SAVEUID;
483
484

    $EUID = $UID;
485
    # Update elists in case email changed.
486
    system("$GENELISTS -m -u $user");
487
    $EUID = 0;
488

489
490
491
492
493
494
495
496
497
    return 0;
}

#
# Freeze a user.
#
sub FreezeUser()
{
    #
498
    # Only admin people can do this.
499
500
501
502
503
504
505
506
507
508
509
    #
    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;
510

511
512
513
514
515
516
517
518
519
    return UpdateUser(1);
}

#
# Thaw a user.
#
sub ThawUser()
{
    #
520
    # Only admin people can do this.
521
522
523
524
525
526
527
528
529
530
531
    #
    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;
532

533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
    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: $!");
563
564
565
566
567
568
569
570
571
	$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;
	      };
	}
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
    }

    #
    # 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;

588
589
590
591
592
    if (defined($dochown) && $dochown!=0) {
	chown($user_number,$default_groupgid,"$HOMEDIR/$user/.forward") or
	  warn("Could not chown $forward: $!");
    }

593
594
595
596
597
598
599
600
601
    return 0;
}

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

603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
    #
    # 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;
621
622
 	if (system("$SSH -host $CONTROL '$SFSKEYGEN -KPn ".
 		   "$user\@ops.emulab.net $sfsdir/identity'")) {
623
	    fatal("Failure in sfskey gen!");
624
625
626
627
628
629
	}
	# Version 7 stuff for later.
	#if (system("$SSH -host $CONTROL '$SFSKEYGEN -KP ".
	#	    "-l $user\@ops.emulab.net $sfsdir/identity'")) {
	#    fatal("Failure in sfskey gen!");
	#}
630
	$UID = $SAVEUID;
631

632
633
634
635
	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: $!");
636

637
638
639
640
641
642
643
	#
	# 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 =~ /.*,.*,.*,(.*),(.*)/) {
644
645
646
647
648
649
650
	    # Version 6
	    DBQueryFatal("replace into user_sfskeys ".
			 "values ('$user', '$2', '${user}:${1}:${user}::', ".
			 "now())");
	}
	elsif ($ident =~ /.*:.*:.*:(.*):(.*)/) {
	    # Version 7
651
652
653
654
655
656
657
658
659
660
661
	    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;
662
}
663
664
665
666
667
668
669

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

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