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

#
# EMULAB-COPYRIGHT
5
# Copyright (c) 2000-2012 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
23
24
# Use -u for update mode, which skips the checks on current status,
# and forces the target user into that state. Eventually, this should
# be the default mode of operation (independent of web interface).
25
#
26
27
sub usage()
{
28
    print("Usage: tbacct [-f] [-b] [-u] ".
29
30
	  "<add|del|mod|passwd|wpasswd|email|freeze|thaw|verify> ".
	  "<user> [args]\n");
31
32
    exit(-1);
}
33
my $optlist = "fbu";
34
35
my $force   = 0;
my $batch   = 0;
36
my $update  = 0;
37
38
39
40
41
42
43

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS	= "@TBOPSEMAIL@";
my $TBLOGS	= "@TBLOGSEMAIL@";
44
my $TBAUDIT	= "@TBAUDITEMAIL@";
45
46
47
my $CONTROL	= "@USERNODE@";
my $BOSSNODE	= "@BOSSNODE@";
my $WITHSFS	= @SFSSUPPORT@;
48
my $WIKISUPPORT = @WIKISUPPORT@;
49
my $TRACSUPPORT = @TRACSUPPORT@;
50
my $BUGDBSUPPORT= @BUGDBSUPPORT@;
51
my $OPSDBSUPPORT= @OPSDBSUPPORT@;
52
my $CHATSUPPORT = @CHATSUPPORT@;
53
my $MAILMANSUPPORT= @MAILMANSUPPORT@;
54
my $THISHOMEBASE= "@THISHOMEBASE@";
55
my $PROTOUSER   = 'elabman';
56
my $ELABINELAB  = @ELABINELAB@;
57

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

61
62
63
64
65
66
67
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";
68
my $GENELISTS	= "$TB/sbin/genelists";
69
my $MKUSERCERT	= "$TB/sbin/mkusercert";
70
my $SFSUPDATE	= "$TB/sbin/sfskey_update";
71
my $PBAG	= "$TB/sbin/paperbag";
72
my $EXPORTSSETUP= "$TB/sbin/exports_setup";
73
74
my $ADDWIKIUSER = "$TB/sbin/addwikiuser";
my $DELWIKIUSER = "$TB/sbin/delwikiuser";
75
76
my $ADDTRACUSER = "$TB/sbin/tracuser";
my $DELTRACUSER = "$TB/sbin/tracuser -r";
77
78
my $ADDBUGDBUSER= "$TB/sbin/addbugdbuser";
my $DELBUGDBUSER= "$TB/sbin/delbugdbuser";
79
80
my $ADDCHATUSER = "$TB/sbin/addjabberuser";
my $DELCHATUSER = "$TB/sbin/deljabberuser";
81
82
83
my $MMMODIFYUSER= "$TB/sbin/mmmodifymember";
my $ADDMMUSER   = "$TB/sbin/addmmuser";
my $DELMMUSER   = "$TB/sbin/delmmuser";
84
my $OPSDBCONTROL= "$TB/sbin/opsdb_control";
85
my $ADDHOOK     = "$TB/sbin/adduserhook";
86
my $SETGROUPS   = "$TB/sbin/setgroups";
87
my $NOLOGIN	= "/sbin/nologin";
88
my $SSH		= "$TB/bin/sshtb";
89
90
91
my $SAVEUID	= $UID;
my $NOSUCHUSER  = 67;
my $USEREXISTS  = 65;
92
93
# Nasty. Should do this with /etc/pw.conf shellpath.
my %shellpaths  = ("csh"  => "/bin/csh", "sh" => "/bin/sh",
94
		   "tcsh" => "/bin/tcsh", "bash" => "/usr/local/bin/bash",
Leigh B. Stoller's avatar
Leigh B. Stoller committed
95
		   "zsh" => "/usr/local/bin/zsh");
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

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.
113
#
114
115
116
117
118
119
120
if ($UID == 0) {
    die("*** $0:\n".
	"    Please do not run this as root! Its already setuid!\n");
}

#
# Untaint the path
121
#
122
123
124
125
126
127
128
129
130
$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;

#
131
# Load the Testbed support stuff.
132
133
134
135
136
#
use lib "@prefix@/lib";
use libaudit;
use libdb;
use libtestbed;
137
use User;
138
use Project;
139

140
141
142
143
144
145
146
147
148
149
#
# Function prototypes
#
sub AddUser();
sub DelUser();
sub UpdatePassword();
sub UpdateWindowsPassword();
sub UpdateUser(;$);
sub FreezeUser();
sub ThawUser();
150
sub VerifyUser();
151
sub UpdateEmail();
152
153
154
155
sub CheckDotFiles();
sub GenerateSFSKey();
sub fatal($);

156
157
my $HOMEDIR	= USERROOT();

158
159
160
#
# Rewrite audit version of ARGV to prevent password in mail logs.
#
161
if (scalar(@ARGV) == 3 && $ARGV[0] eq "passwd") {
162
163
164
165
166
167
    my @NEWARGV = @ARGV;

    $NEWARGV[scalar(@NEWARGV) - 1] = "**********";
    AuditSetARGV(@NEWARGV);
}

168
169
170
171
172
173
174
175
176
177
178
#
# 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;
}
179
180
181
if (defined($options{"b"})) {
    $batch = 1;
}
182
183
184
if (defined($options{"u"})) {
    $update = 1;
}
185
if (@ARGV < 2) {
186
187
    usage();
}
188
189
my $cmd  = shift(@ARGV);
my $user = shift(@ARGV);
190
191
192
193

#
# Untaint the arguments.
#
194
if ($user =~ /^([-\w]+)$/i) {
195
196
197
198
199
    $user = $1;
}
else {
    die("Tainted argument: $user\n");
}
200
if ($cmd =~ /^(add|del|mod|freeze|passwd|wpasswd|thaw|email|verify)$/) {
201
202
203
204
205
206
207
208
209
210
211
    $cmd = $1;
}
else {
    usage();
}

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

212
213
214
215
216
217
# Map target user to object.
my $target_user = User->Lookup($user);
if (! defined($target_user)) {
    fatal("$user does not exist!");
}

218
219
220
221
222
223
224
225
226
227
228
229
#
# Map invoking user to object. 
# If invoked as "nobody" its for a user with no actual account, and so
# just set the current user to that user. If we make any callouts it will
# fail (verbosely of course).
#
my $this_user;

if (getpwuid($UID) eq "nobody") {
    $this_user = $target_user;
}
else {
230
    $this_user = User->ThisUser();
231
232
233
234

    if (! defined($this_user)) {
	fatal("You ($UID) do not exist!");
    }
235
236
}

237
238
239
240
241
242
243
244
245
246
247
248
249
#
# 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).
#
250
my $dbid        = $target_user->dbid();
251
252
253
254
255
256
257
258
259
260
my $pswd        = $target_user->pswd();
my $user_number = $target_user->unix_uid();
my $fullname    = $target_user->name();
my $user_email  = $target_user->email();
my $status      = $target_user->status();
my $webonly     = $target_user->webonly();
my $usr_shell   = $target_user->shell();
my $usr_admin   = $target_user->admin();
my $wpswd       = $target_user->w_pswd();
my $wikionly    = $target_user->wikionly();
261
262
my $isnonlocal  = $target_user->IsNonLocal();
my $nocollabtools = $target_user->nocollabtools();
263
264
265
266
267
268

#
# 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.
#
269
my $firstproject;
270
271
272
my $default_groupname;
my $default_groupgid;

273
274
275
if ($target_user->FirstApprovedProject(\$firstproject) < 0) {
    fatal("Could not determine first approved project for $target_user");
}
276

277
278
279
if (defined($firstproject)) {
    $default_groupname = $firstproject->unix_name();
    $default_groupgid  = $firstproject->unix_gid();
280
281
282
}
else {
    print "No group membership for $user; using the guest group!\n";
283

284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
    ($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;
    };
303
304
305
306
    /^wpasswd$/ && do {
	UpdateWindowsPassword();
	last SWITCH;
    };
307
308
309
310
    /^email$/ && do {
	UpdateEmail();
	last SWITCH;
    };
311
312
313
314
315
316
317
318
319
320
321
322
    /^mod$/ && do {
	UpdateUser();
	last SWITCH;
    };
    /^freeze$/ && do {
	FreezeUser();
	last SWITCH;
    };
    /^thaw$/ && do {
	ThawUser();
	last SWITCH;
    };
323
324
325
326
    /^verify$/ && do {
	VerifyUser();
	last SWITCH;
    };
327
328
329
330
331
332
333
334
}

# Always do this!
CheckDotFiles();

#
# Invoke as real user for auditing (and cause of perl).
#
335
if ($WITHSFS && $sfsupdate) {
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
    $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.
    #
358
    if ($webonly || $wikionly || $status ne USERSTATUS_ACTIVE) {
359
360
361
	if ($webonly) {
	    return 0;
	}
362
363
364
365
366
367
368
	#
	# Allow for users to be initialized to frozen in an inner Emulab.
	#
	if ($ELABINELAB && $status eq USERSTATUS_FROZEN) {
	    print STDERR "Ignoring frozen user in elabinelab\n";
	    return 0;
	}
369
370
371
372
373
374
375
	if ($wikionly) {
	    $EUID = $UID;

	    # And to the wiki if enabled.
	    system("$ADDWIKIUSER $user")
		if ($WIKISUPPORT && !$batch);
	    
376
377
378
379
	    # And to the bugdb if enabled.
	    system("$ADDBUGDBUSER $user")
		if ($BUGDBSUPPORT && !$batch);
	    
380
381
382
	    $EUID = 0;
	    return 0;
	}
383
384
	fatal("$user is not active! Cannot build an account!");
    }
385

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

390
391
392
393
394
395
396
397
398
399
400
401
402
	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;

403
    if (!$isnonlocal) {
404
405
406
	print "Adding user $user ($user_number) to $CONTROL.\n";

	if (system("$SSH -host $CONTROL ".
407
		   "'$USERADD $user -u $user_number -c \"$fullname\" ".
408
		   "-k /usr/share/skel -h - -m -d $HOMEDIR/$user ".
409
		   "-g $default_groupname -s $shellpaths{$usr_shell}'")) {
410
	    if (($? >> 8) != $USEREXISTS) {
411
412
413
		fatal("Could not add user $user ($user_number) to $CONTROL.");
	    }
	}
414

415
416
417
418
419
420
421
422
423
424
425
426
	#
	# Leave the password "starred" on elabinelab; safer.
	#
	if (!$ELABINELAB) {
	    # shell escape.
	    $pswd =~ s/\$/\\\$/g;
	    $pswd =~ s/\*/\\\*/g;

	    print "Initializing user $user password on $CONTROL.\n";
	    if (system("$SSH -host $CONTROL $CHPASS -p '$pswd' $user")) {
		fatal("Could not initialize password for $user on $CONTROL!");
	    }
427
	}
428
429
430
431
432
433
434
435
436
437

	#
	# Extra hook added for CMU. Generalize later.
	#
	if ($THISHOMEBASE =~ /^cmuemulab$/i) {
	    print "Running post create hook for user $user on $CONTROL.\n";

	    # Do not worry about failure. 
	    system("$SSH -host $CONTROL $ADDHOOK $user");
	}
438
439
440
    }
    $UID = $SAVEUID;

441
    goto skipstuff
442
	if ($isnonlocal);
443
    
444
445
446
447
    #
    # Do the ssh thing. Invoke as real user for auditing.
    #
    $EUID = $UID;
448
    if ($user ne $PROTOUSER && system("$ADDKEY -i $user")) {
449
450
	fatal("Could not generate initial ssh key for $user");
    }
451
452
    # Generate the SSL cert for the user.
    system("$MKUSERCERT $user");
453
454
455
456
457
458
459
460
461

    #
    # If the user requested an initial encrypted SSL certificate, create
    # that too. Need to delete the initial_passphrase slot though, so that
    # we do not try to recreate it at some future time.
    #
    if (defined($target_user->initial_passphrase())) {
	my $pphrase = User::escapeshellarg($target_user->initial_passphrase());
	
462
	system("$MKUSERCERT -p $pphrase $user");
463
464
465
	if ($?) {
	    fatal("Could not create initial encrypted SSL certificate");
	}
466
	$target_user->Update({'initial_passphrase' => "NULL"});
467
    }
468
469
470
471
472
473
    
    if ($nocollabtools) {
	$EUID = 0;
	goto skipstuff;
    }
    
474
    # Add to elists.
475
476
    system("$GENELISTS -u $user")
	if (! $batch);
477

478
479
    # And to the wiki if enabled.
    system("$ADDWIKIUSER $user")
480
	if ($WIKISUPPORT && $user ne $PROTOUSER);
481

482
    # And to the bugdb if enabled.
483
    system("$ADDBUGDBUSER $user")
484
	if ($BUGDBSUPPORT && $user ne $PROTOUSER);
485

486
487
488
489
    # And to the OPS db if enabled.
    system("$OPSDBCONTROL adduser $user")
	if ($OPSDBSUPPORT && $user ne $PROTOUSER);

490
491
    # And to the chat server if enabled.
    system("$ADDCHATUSER $user")
492
	if ($CHATSUPPORT && $user ne $PROTOUSER);
493

494
495
496
497
    # And the mailman lists if enabled.
    system("$ADDMMUSER $user")
	if ($MAILMANSUPPORT);
    
498
499
500
501
    # And to the trac system if enabled.
    system("$ADDTRACUSER $user")
	if ($TRACSUPPORT && $user ne $PROTOUSER);

502
503
504
505
506
507
508
509
510
    #
    # 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.
    #
511
512
513
514
    if (! $batch) {
	print "Updating exports file.\n";
	system("$EXPORTSSETUP");
    }
515
516
517
518
519
520
    $EUID = 0;

    # SFS key.
    if ($CONTROL ne $BOSSNODE) {
	GenerateSFSKey();
    }
521
  skipstuff:
522
    return 0;
523
524
525
526
527
528
529
530
}

#
# Delete a user.
#
sub DelUser()
{
    #
531
    # Only admin people can do this.
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
    #
    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.");
	}
    }

553
    if (! $isnonlocal) {
554
	print "Removing user $user from $CONTROL\n";
555

556
557
558
559
560
561
562
	if (system("$SSH -host $CONTROL '$USERDEL $user'")) {
	    if (($? >> 8) != $NOSUCHUSER) {
		fatal("Could not remove user $user from $CONTROL.");
	    }
	}
    }
    $UID = $SAVEUID;
563

564
565
566
    goto skipstuff
	if ($isnonlocal || $nocollabtools);
    
567
    $EUID = $UID;
568
569
570
571
572
573
574
    #
    # 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");

575
    # Remove from elists.
576
    system("$GENELISTS -u $user");
577
578
579
580
581

    # And to the wiki if enabled.
    system("$DELWIKIUSER $user")
	if ($WIKISUPPORT);
    
582
583
584
585
    # And the chat server if enabled.
    system("$DELCHATUSER $user")
	if ($CHATSUPPORT);
    
586
587
588
589
    # And the mailman lists if enabled.
    system("$DELMMUSER $user")
	if ($MAILMANSUPPORT);
    
590
591
592
593
    # And to the trac system if enabled.
    system("$DELTRACUSER $user")
	if ($TRACSUPPORT);

594
    $EUID = 0;
595

596
    $sfsupdate = 1;
597
  skipstuff:
598
599
600
601
602
    return 0;
}

#
# Change a password for the user on the control node. The local password
603
# is not touched!
604
605
606
607
#
sub UpdatePassword()
{
    #
608
    # New password (encrypted) comes in on the command line. 
609
    #
610
611
612
613
614
615
616
617
    usage()
	if (! @ARGV);

    my $new_pswd  = shift(@ARGV);

    # Lets not do this if no changes.
    if ($new_pswd eq $target_user->pswd()) {
	print "Password has not changed ...\n";
618
619
620
	return 0;
    }

621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
    # Lets prevent any odd characters.
    if ($new_pswd =~ /[\'\\\"\&]+/) {
	fatal("Invalid characters in new password encryption string!");
    }

    #
    # Insert into database. When changing password for someone else,
    # always set the expiration to right now so that the target user
    # is "forced" to change it. 
    #
    my $expires;
    
    if (! $target_user->SameUser($this_user)) {
	$expires = "now()";
    }
    else {
	$expires = "date_add(now(), interval 1 year)";
    }

    if ($target_user->SetPassword($new_pswd, $expires)) {
	fatal("Could not update password encryption string for $target_user");
    }

644
645
646
647
    # Go no further if a nonlocal user.
    return 0
	if ($isnonlocal);

648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
    # Send auditing email before next step in case of failure.
    SENDMAIL("$fullname <$user_email>",
	     "Password for '$user' has been changed",
	     "\n".
	     "Emulab password for '$user' has been changed by " .
	            $this_user->uid() ."\n".
	     "\n".
	     "Name:              " . $target_user->name()  . "\n".
	     "IDX:               " . $target_user->uid_idx()  . "\n".
	     "\n".
	     "If this is unexpected, please contact Testbed Operations\n".
	     "($TBOPS) immediately!\n".
	     "\n",
	     "$TBOPS",
	     "Bcc: $TBAUDIT");

    # Go no further if a webonly user.
    return 0
	if ($webonly);

    #
    # Go no further if user is not active or frozen.
    #
    return 0
	if (! ($status eq USERSTATUS_ACTIVE || $status eq USERSTATUS_FROZEN));

    #
    # Change on ops only if there is a real account there.
676
    # For ELABINELAB, safer to leave the password "starred".
677
    #
678
    if (!$wikionly && !$ELABINELAB) {
679
680
681
682
683
684
685
686
	#
	# Grab from the DB to avoid taint checking sillyness.
	#
	my $safe_pswd = $target_user->pswd();
	# shell escape.
	$safe_pswd    =~ s/\$/\\\$/g;
	$safe_pswd    =~ s/\*/\\\*/g;
	
687
688
689
690
	$UID = 0;
	if ($CONTROL ne $BOSSNODE) {
	    print "Updating user $user password on $CONTROL.\n";
	    
691
	    if (system("$SSH -host $CONTROL $CHPASS -p '$safe_pswd' $user")) {
692
693
		fatal("Could not change password for user $user on $CONTROL!");
	    }
694
	}
695
	$UID = $SAVEUID;
696
    }
697
698
699
700
701
702
703
704
705

    #
    # Ick. If invoked as "nobody" then the user was either frozen or
    # inactive. Lets skip the rest of this for now. Needs more thought
    # and cleanup in the web interface to this, since we cannot call
    # out to these scripts as "nobody" (yet).
    #
    return 0
	if (getpwuid($UID) eq "nobody");
706
    
707
    $EUID = $UID;
708
    # And the wiki if enabled.
709
    system("$ADDWIKIUSER -u $user")
710
	if ($WIKISUPPORT && $user ne $PROTOUSER && !$webonly);
711
712
713

    # And to the bugdb if enabled.
    system("$ADDBUGDBUSER -m $user")
714
	if ($BUGDBSUPPORT && $user ne $PROTOUSER && ! ($wikionly || $webonly));
715

716
717
718
    system("$ADDTRACUSER -u $user")
	if ($TRACSUPPORT && $user ne $PROTOUSER && !$webonly);

719
720
    $EUID = 0;
    
721
722
723
    return 0;
}

724
725
726
727
728
729
730
#
# Change a Windows password for the user on the Samba server node.
# The local password is not touched!
#
sub UpdateWindowsPassword()
{
    #
731
    # New password (encrypted) comes in on the command line. 
732
    #
733
734
735
736
737
738
739
    usage()
	if (! @ARGV);
    my $new_wpswd  = shift(@ARGV);

    # Lets not do this if no changes.
    if ($new_wpswd eq $target_user->w_pswd()) {
	print "Password has not changed ...\n";
740
741
742
	return 0;
    }

743
744
745
746
747
748
    #
    # Insert into database.
    #
    if ($target_user->SetWindowsPassword($new_wpswd)) {
	fatal("Could not update Windows password string for $target_user");
    }
749

750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
    # Go no further if a webonly user.
    return 0
	if ($webonly);

    #
    # Go no further if user is not active or frozen.
    #
    return 0
	if (! ($status eq USERSTATUS_ACTIVE || $status eq USERSTATUS_FROZEN));

    #
    # Change on ops for Samba only if there is a real account there.
    #
    if (! $wikionly) {
	# shell escape.
	$new_wpswd     =~ s/\$/\\\$/g;

	$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 "$new_wpswd\n$new_wpswd\n";
	close SPCMD 
	    || fatal("Closing $SMBPASSWD pipe, user $user on $SAMBANODE: $! $?");

	$UID = $SAVEUID;
    }
779
780
781
    return 0;
}

782
783
784
785
786
787
788
789
790
791
#
# Update user info. Allow for optional shell change for freeze/thaw.
#
sub UpdateUser(;$)
{
    my ($freezeopt) = @_;
    my $locshellarg = "";
    my $remshellarg = "";

    #
792
    # Sanity check.
793
    #
794
    if ($webonly || $isnonlocal) {
795
796
797
	return 0;
    }
    if (!defined($freezeopt) && ($status ne USERSTATUS_ACTIVE)) {
798
799
800
801
802
803
804
805
	#
	# If doing a modification to a frozen user, then just ignore
	# it; the modification will happen later when the user is thawed.
	#
	if ($status eq USERSTATUS_FROZEN) {
	    print "Ignoring update of frozen user $user\n";
	    return 0;
	}
806
	fatal("$user is not active! Cannot update the account!");
807
808
809
    }

    # Shell is different on local vs control node.
810
    if (defined($freezeopt) && $freezeopt) {
811
812
813
814
	$locshellarg = "-s $NOLOGIN";
	$remshellarg = "-s $NOLOGIN";
    }
    else {
815
816
817
	# Leave local shell alone if an admin.
	$locshellarg = "-s $PBAG"
	    if (!$usr_admin);
818
819
820
	# Special treatment for PROTUSER
	$locshellarg = "-s " . $shellpaths{"tcsh"} . " "
	    if ($usr_admin && $user eq $PROTOUSER);
821
822
823
824

	if (!defined($usr_shell) ||
	    !exists($shellpaths{$usr_shell})) {
	    $remshellarg = "-s " . $shellpaths{"tcsh"};
825
	}
826
827
	else  {
	    $remshellarg = "-s " . $shellpaths{$usr_shell};
828
829
830
	}
    }
    print "Updating user $user ($user_number) on local node.\n";
831

832
833
834
835
    $UID = 0;
    if (system("$USERMOD $user $locshellarg -c \"$fullname\" ")) {
	fatal("Could not modify user $user on local node.");
    }
836

837
838
839
840
841
842
843
844
    #
    # 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";
845

846
	if (system("$SSH -host $CONTROL ".
847
		   "'$USERMOD $user $remshellarg -c \"$fullname\"'")) {
848
849
850
851
	    fatal("Could not modify user $user record on $CONTROL.");
	}
    }
    $UID = $SAVEUID;
852
853

    $EUID = $UID;
854
855
856
857
    # Update elists in case email changed.
    system("$MMMODIFYUSER $user")
	if ($MAILMANSUPPORT && !$batch);
    
858
    # Update elists in case email changed.
859
    system("$GENELISTS -m -u $user");
860
    $EUID = 0;
861

862
863
864
    return 0;
}

865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
#
# Change email address for user.
#
sub UpdateEmail()
{
    my $forward = "$HOMEDIR/$user/.forward";
    
    #
    # Only admin people can do this.
    #
    if (! TBAdmin($UID)) {
	fatal("You do not have permission to update email for user $user.");
    }

    #
    # New email comes in on the command line. 
    #
    usage()
	if (! @ARGV);

    my $new_email = shift(@ARGV);

    # Lets not do this if no changes.
    return 0
	if ($new_email eq $user_email);

    # Must be valid.
    if (! TBcheck_dbslot($new_email, "users", "usr_email",
			 TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
	fatal("Invalid characters in email address!");
    }

    my %args = ();
    $args{"usr_email"} = $new_email;

    if ($target_user->Update(\%args)) {
	fatal("Could not update email address for $target_user");
    }

904
905
906
    return 0
	if ($isnonlocal);

907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
    # Send auditing email before next step in case of failure.
    SENDMAIL("$fullname <$user_email>",
	     "Email Address for '$user' Modified",
	     "\n".
	     "Email Address for '$user' changed by " . $this_user->uid() ."\n".
	     "\n".
	     "Name:              " . $target_user->name()  . "\n".
	     "IDX:               " . $target_user->uid_idx()  . "\n".
	     "Old Email:         " . $user_email . "\n".
	     "New Email:         " . $new_email . "\n".
	     "\n".
	     "If this is unexpected, please contact Testbed Operations\n".
	     "($TBOPS) immediately!\n".
	     "\n",
	     "$TBOPS",
	     "CC: $new_email\n".
	     "Bcc: $TBAUDIT");

    # Change global in this script. 
    $user_email = $target_user->email();

    $EUID = $UID;

    # Update mailman elists.
    system("$MMMODIFYUSER $user")
	if ($MAILMANSUPPORT);
    
    # Update system elists.
    system("$GENELISTS -m -u $user");

    $EUID = 0;

    # Remove the users current .forward file to force regen.
    unlink($forward)
	if (-e $forward);
    
    return 0;
}

946
947
948
949
950
951
#
# Freeze a user.
#
sub FreezeUser()
{
    #
952
    # Only admin people can do this.
953
954
955
956
957
958
959
960
    #
    if (! TBAdmin($UID)) {
	fatal("You do not have permission to freeze user $user.");
    }
    #
    # Check status.
    #
    if ($status ne USERSTATUS_FROZEN) {
961
962
963
	fatal("$user is still active! Cannot freeze the account!")
	    if (!$update);

964
965
	$target_user->SetStatus(USERSTATUS_FROZEN());
	$status = USERSTATUS_FROZEN();
966
967
    }
    $sfsupdate = 1;
968

969
970
971
972
973
974
975
976
977
    return UpdateUser(1);
}

#
# Thaw a user.
#
sub ThawUser()
{
    #
978
    # Only admin people can do this.
979
980
981
982
983
984
985
986
    #
    if (! TBAdmin($UID)) {
	fatal("You do not have permission to thaw user $user.");
    }
    #
    # Check status.
    #
    if ($status ne USERSTATUS_ACTIVE) {
987
988
989
	fatal("$user is not active! Cannot thaw the account!")
	    if (!$update);
	$target_user->SetStatus(USERSTATUS_ACTIVE());
990
	$status = USERSTATUS_ACTIVE();
991
992
    }
    $sfsupdate = 1;
993

994
995
996
997
998
999
    #
    # This lets users start off as frozen in an ELABINELAB, and then
    # get created later. Saves a lot of time.
    #
    if ($ELABINELAB &&
	system("egrep -q -s '^${user}:' /etc/passwd")) {
1000
1001
1002
1003
1004
	
	AddUser() == 0
	    or fatal("Cannot thaw $user");

	system("$USERMOD -n $user -s /bin/tcsh");
1005
    }
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
    else {
	UpdateUser(0) == 0
	    or fatal("Cannot thaw $user");
    }

    #
    # Invoke as real user for auditing.
    #
    $EUID = $UID;
    system("$SETGROUPS $user");
    $EUID = 0;
    
    return 0;
1019
1020
}

1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
#
# Verify a user. Converts status and sends email
#
sub VerifyUser()
{
    #
    # Only admin people can do this unless its the user himself.
    #
    if (! $target_user->SameUser($this_user) && ! TBAdmin()) {
	fatal("You do not have permission to verify user $user.");
    }

    if ($target_user->status() ne USERSTATUS_NEWUSER) {
	fatal("$user is not a newuser! Cannot verify the account!");
    }

    my $newstatus = ($target_user->wikionly() ?
		     USERSTATUS_ACTIVE() : USERSTATUS_UNAPPROVED());

    $target_user->SetStatus($newstatus) == 0 or
	fatal("Could not set user status to '$newstatus' for $target_user");

    $target_user->SendVerifiedEmail() == 0 or
	fatal("Could not send verified email for $target_user");

    return 0;
}

1049
1050
1051
1052
1053
1054
1055
1056
1057
#
# 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";

1058
1059
1060
1061
    # No home dirs for these.
    return 0
	if ($webonly || $isnonlocal);
    
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
    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: $!");
1080
1081
1082
1083
1084
1085
1086
1087
1088
	$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;
	      };
	}
1089
1090
1091
1092
    }

    #
    # Add testbed path to .cshrc and .profile.
1093
    # Plus a conditional Cygwin section for the Windows system path.
1094
    #
1095
1096
1097
1098
    my $cpathstr = "set path = ($USERPATH \$path)\n" .
    'if ( `uname -s` =~ CYGWIN* ) then' . "\n" .
    '    setenv PATH "${PATH}:/cygdrive/c/WINDOWS/system32:/cygdrive/c/WINDOWS"' . "\n" .
    'endif';
1099
1100
1101
1102
    if (-e $cshrc && system("egrep -q -s '$USERPATH' $cshrc")) {
	system("echo '$cpathstr' >> $cshrc");
    }

1103
1104
1105
1106
    my $spathstr = "PATH=$USERPATH:\$PATH\n" .
    'if [[ `uname -s` == CYGWIN* ]]; then' . "\n" .
    '    PATH="$PATH":/cygdrive/c/WINDOWS/system32:/cygdrive/c/WINDOWS' . "\n" .
    'fi';
1107
1108
1109
1110
1111
    if (-e $profile && system("egrep -q -s '$USERPATH' $profile")) {
	system("echo '$spathstr' >> $profile");
    }
    $UID = $SAVEUID;

1112
1113
1114
1115
1116
    if (defined($dochown) && $dochown!=0) {
	chown($user_number,$default_groupgid,"$HOMEDIR/$user/.forward") or
	  warn("Could not chown $forward: $!");
    }

1117
1118
1119
1120
1121
1122
1123
1124
1125
    return 0;
}

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

1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
    #
    # 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;
1145
1146
 	if (system("$SSH -host $CONTROL '$SFSKEYGEN -KPn ".
 		   "$user\@ops.emulab.net $sfsdir/identity'")) {
1147
	    fatal("Failure in sfskey gen!");
1148
1149
1150
1151
1152
1153
	}
	# Version 7 stuff for later.
	#if (system("$SSH -host $CONTROL '$SFSKEYGEN -KP ".
	#	    "-l $user\@ops.emulab.net $sfsdir/identity'")) {
	#    fatal("Failure in sfskey gen!");
	#}
1154
	$UID = $SAVEUID;
1155

1156
1157
1158
1159
	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: $!");
1160

1161
1162
1163
1164
1165
1166
1167
	#
	# 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 =~ /.*,.*,.*,(.*),(.*)/) {
1168
1169
1170
1171
1172
1173
1174
	    # Version 6
	    DBQueryFatal("replace into user_sfskeys ".
			 "values ('$user', '$2', '${user}:${1}:${user}::', ".
			 "now())");
	}
	elsif ($ident =~ /.*:.*:.*:(.*):(.*)/) {
	    # Version 7
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
	    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;
1186
}
1187
1188
1189
1190
1191
1192
1193

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

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