addpubkey.in 16.8 KB
Newer Older
1 2
#!/usr/bin/perl -wT
#
3
# Copyright (c) 2000-2017 University of Utah and the Flux Group.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
23 24 25
#
use English;
use Getopt::Std;
26
use XML::Simple;
27
use File::Temp qw(tempfile :POSIX );
28 29 30 31 32 33 34 35 36 37

#
# Parse ssh public keys and enter into the DB. The default format is
# openssh, but if the key is not in that format, then use ssh-keygen
# to see if it can be converted from either SSH2 or SECSH format into
# openssh format. This gets called from the webpage to parse keys
# uploaded by users.
#
sub usage()
{
Russ Fish's avatar
Russ Fish committed
38 39 40
    print "Usage: addpubkeys [-d] [-k | -f] [-n | -u <user>] [<keyfile> | <key>]\n";
    print "       addpubkeys [-d] -X <xmlfile>\n";
    print "       addpubkeys [-d] [-i [-r] | -w] <user>\n";
41
    print "Options:\n";
Russ Fish's avatar
Russ Fish committed
42
    print " -d      Turn on debugging\n";
43
    print " -k      Indicates that key was passed in on the command line\n";
44
    print " -f      Indicates that key was passed in as a filename\n";
45
    print " -n      Verify key format only; do not enter into into DB\n";
46
    print " -X      Get args from an XML file: verify, user, keyfile.\n";
47 48
    print " -w      Generate new authkeys (protocol 1 and 2) file for user\n";
    print " -i      Initialize mode; generate initial key for user\n";
49
    print " -r      Force a regenerate of initial key for user\n";
50 51
    exit(-1);
}
52
my $optlist   = "dkniwfu:rX:sRNC:S:Ia";
53 54
my $iskey     = 0;
my $verify    = 0;
55
my $initmode  = 0;
Leigh Stoller's avatar
Leigh Stoller committed
56
my $force     = 0;
57 58 59
my $genmode   = 0;
my $nobody    = 0;
my $noemail   = 0;
60 61
my $remove    = 0;
my $nodelete  = 0;
62
my $internal  = 0;
63
my $isaptkey  = 0;
64
my $Comment;
65
my $xmlfile;
66 67 68 69 70 71 72

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS       = "@TBOPSEMAIL@";
my $TBAUDIT     = "@TBAUDITEMAIL@";
Leigh Stoller's avatar
Leigh Stoller committed
73
my $OURDOMAIN   = "@OURDOMAIN@";
74
my $CONTROL	= "@USERNODE@";
75
my $KEYGEN	= "/usr/bin/ssh-keygen";
76 77 78
my $ACCOUNTPROXY= "$TB/sbin/accountsetup";
my $SSH		= "$TB/bin/sshtb";
my $SAVEUID	= $UID;
79 80 81 82
my $USERUID;

# Locals
my $user;
83 84
my $this_user;
my $target_user;
85 86 87 88 89 90
my $keyfile;
my $keyline;
my $key;
my $comment;
my $user_name;
my $user_email;
91 92
my $user_dbid;
my $user_uid;
93
my $user_gid;
Russ Fish's avatar
Russ Fish committed
94
my $debug = 0;
95 96 97 98 99

#
# Testbed Support libraries
#
use lib "@prefix@/lib";
100
use libaudit;
101 102
use libdb;
use libtestbed;
103
use User;
104 105 106
if (@PROTOGENI_SUPPORT@) {
    require APT_Utility;
}
107

108 109 110 111 112
#
# Function prototypes
#
sub ParseKey($);
sub InitUser();
Leigh Stoller's avatar
Leigh Stoller committed
113
sub GenerateKeyFile();
Russ Fish's avatar
Russ Fish committed
114
sub ParseXmlArgs($$$$$$);
115 116
sub fatal($);

117 118
my $HOMEDIR = USERROOT();

119 120 121 122 123 124 125 126 127 128 129 130
#
# These are the fields that we allow to come in from the XMLfile.
#
my $SLOT_OPTIONAL	= 0x1;	# The field is not required.
my $SLOT_REQUIRED	= 0x2;  # The field is required and must be non-null.
my $SLOT_ADMINONLY	= 0x4;  # Only admins can set this field.
my %xmlfields =
    # XML Field Name        DB slot name         Flags             Default
    ("verify"		=> ["verify",		$SLOT_OPTIONAL,	   0],
     "user"		=> ["user",		$SLOT_OPTIONAL],
     "keyfile"		=> ["keyfile",		$SLOT_REQUIRED]);

131 132 133 134 135 136 137 138 139 140 141
#
# Turn off line buffering on output
#
$| = 1;

#
# Untaint the path
# 
$ENV{'PATH'} = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

142 143 144 145 146 147 148 149
#
# 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");
}

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
#
# Please do not run it as root. Hard to track what has happened.
# 
if ($UID == 0) {
    die("*** $0:\n".
	"    Please do not run this as root!\n");
}

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
Russ Fish's avatar
Russ Fish committed
166 167 168
if (defined($options{"d"})) {
    $debug = 1;
}
169 170 171
if (defined($options{"a"})) {
    $isaptkey = 1;
}
172 173 174
if (defined($options{"k"})) {
    $iskey = 1;
}
175 176 177
if (defined($options{"f"})) {
    $iskey = 0;
}
178 179 180
if (defined($options{"n"})) {
    $verify = 1;
}
181 182 183
if (defined($options{"i"})) {
    $initmode = 1;
}
184 185 186
if (defined($options{"N"})) {
    $nodelete = 1;
}
187 188 189
if (defined($options{"I"})) {
    $internal = 1;
}
190
if (defined($options{"r"})) {
Leigh Stoller's avatar
Leigh Stoller committed
191 192
    $force = 1;
}
193 194 195
if (defined($options{"R"})) {
    $remove = 1;
}
196 197 198
if (defined($options{"s"})) {
    $noemail = 1;
}
199 200
if (defined($options{"w"})) {
    $genmode = 1;
201
}
202 203 204
if (defined($options{"u"})) {
    $user = $options{"u"};
}
205 206 207
if (defined($options{"C"})) {
    $Comment = $options{"C"};
}
208 209 210 211 212
if (defined($options{"X"})) {
    $xmlfile = $options{"X"};

    my %xmlargs = (); 
    my %errors = ();
Russ Fish's avatar
Russ Fish committed
213 214
    ParseXmlArgs($xmlfile, "user_pubkeys", \%xmlfields, $debug,
		 \%xmlargs, \%errors);
215 216 217 218 219 220 221 222 223 224 225 226 227 228
    if (keys(%errors)) {
	foreach my $key (keys(%errors)) {
	    my $val = $errors{$key};
	    print "${key}: $val\n";
	}
	fatal("XML arg error");
    }

    $verify = $xmlargs{"verify"};
    $user = $xmlargs{"user"}
	if (exists($xmlargs{"user"}));
    $ARGV[0] = $xmlargs{"keyfile"};
}

229
if ($verify && $genmode) {
230 231
    usage();
}
232
if ($initmode || $genmode) {
233 234 235 236
    usage()
	if (@ARGV != 1);

    $user = $ARGV[0];
237 238
}
else {
239 240 241 242 243 244 245
    usage()
	if (@ARGV != 1);
    usage()
	if (!$verify && !defined($user));
    
    $keyfile = $ARGV[0];
}
246 247 248 249

#
# Untaint the arguments.
#
250
if (defined($user)) {
251
    if ($user =~ /^([-\w]+)$/i) {
252 253 254 255 256
	$user = $1;
    }
    else {
	fatal("Tainted username: $user");
    }
257 258 259 260 261 262 263 264 265 266
    # Map user to object.
    $target_user = User->Lookup($user);
    if (! defined($target_user)) {
	fatal("$user does not exist!")
    }

    $user_name  = $target_user->name();
    $user_email = $target_user->email();
    $user_dbid  = $target_user->dbid();
    $user_uid   = $target_user->uid();
Leigh Stoller's avatar
Leigh Stoller committed
267
    $USERUID    = $target_user->unix_uid();
268 269 270 271 272 273 274 275 276 277 278 279

    my $firstproject;

    if ($target_user->FirstApprovedProject(\$firstproject) < 0) {
	fatal("Could not determine first approved project");
    }
    if (defined($firstproject)) {
	$user_gid = $firstproject->unix_gid();
    }
    else {
	$user_gid = "guest";
    }
280 281 282
}

#
283
# If invoked as "nobody" we came from the web interface. We have to have
284 285
# a credential in the environment, unless its just a key verification
# operation, which anyone can do. 
286 287
# 
if (getpwuid($UID) eq "nobody") {
288 289 290 291
    $this_user = User->ImpliedUser();
    
    if (($initmode || $genmode || !$verify) && !defined($this_user)) {
	fatal("Bad usage from web interface");
292 293 294 295
    }
    $nobody = 1;
}
else {
296 297
    # From the command line; map invoking user to object.
    $this_user = User->ThisUser();
298 299 300 301

    if (! defined($this_user)) {
	fatal("You ($UID) do not exist!");
    }
302 303 304
}

#
305
# Initmode or genmode, do it and exit.
306
#
307 308 309 310 311 312 313 314
if ($initmode || $genmode) {
    if ($initmode) {
	exit(InitUser());
    }
    if ($genmode) {
	exit(GenerateKeyFile());
    }
    exit(1);
315 316 317
}

# Else, key parse mode ...
318 319 320 321 322 323 324 325 326 327 328 329
if ($iskey) {
    if ($keyfile =~ /^([-\w\s\.\@\+\/\=]*)$/) {
	$keyfile = $1;
    }
    else {
	fatal("Tainted key: $keyfile");
    }
    $keyline = $keyfile;
}
else {
    if ($keyfile =~ /^([-\w\.\/]+)$/) {
	$keyfile = $1;
330
     }
331 332 333 334
    else {
	fatal("Tainted filename: $keyfile");
    }
    if (! -e $keyfile) {
335
	fatal("No such file: $keyfile\n");
336
    }
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
    
    open(KEY, "$keyfile")
	or fatal("Could not open $keyfile to read");
    #
    # Ick, look at first line to see if it looks like an openssh key,
    # and if so we want to remove the embedded newlines. 
    #
    $keyline = <KEY>;
    my $chompit = 0;
    if ($keyline =~ /^ssh/ || $keyline =~ /^\d+\s+\d+/) {
	$chompit = 1;
    }
    while (<KEY>) {
	chomp($_) if ($chompit);
	$keyline .= $_;
    }
    close(KEY);
354 355 356
}

#
357
# Check user
358
#
359
if (!$verify) {
360 361 362 363
    # If its the user himself, then we can generate a new authkeys file.
    # Assume nodelete option comes from internal script, so do not worry.
    if (!$target_user->SameUser($this_user) && !$this_user->IsAdmin() &&
	!$nodelete) {
364
	fatal("You are not allowed to set pubkeys for $target_user\n");
365
    }
366
    if ($target_user->SameUser($this_user) && $this_user->active()) {
367 368
	# Drop root privs, switch to target user.
	$EUID = $UID = $USERUID;
369 370
	$genmode = 1;
    }
371 372
}

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
#
# Grab the first line of the file. Parse it to see if its in the
# format we like (openssh), either protocol 1 or 2.
#
if (ParseKey($keyline)) {
    exit 0;
}
# If the key was entered on the command line, then nothing more to do.
if ($iskey) {
    exit 1;
}

#
# Run ssh-keygen over it and see if it can convert it. 
#
if (! open(KEYGEN, "ssh-keygen -i -f $keyfile 2>/dev/null |")) {
    fatal("*** $0:\n".
	  "    Could not start ssh-keygen\n");
}
$keyline = <KEYGEN>;
if (close(KEYGEN) && ParseKey($keyline)) {
    exit 0;
}
exit 1;

sub ParseKey($) {
    my ($keyline) = @_;
400

401
    # Remove trailing newlines which screws the patterns below.
402 403
    # First convert dos newlines since many people upload from windoze.
    $keyline =~ s/\r/\n/g;
404 405
    $keyline =~ s/\n//g;

406 407 408 409 410 411
    # Enforce a reasonable length on the key.
    if (length($keyline) > 4096) {
	print "Key is too long!\n";
	print "Key: $keyline\n";
	return 0;
    }
412
    
413
    if ($keyline =~ /^(\d*\s\d*\s[0-9a-zA-Z]*) ([-\w\@\.:\ ]*)\s*$/) {
414 415 416 417 418 419 420 421 422 423 424
        # Protocol 1
	$type    = "ssh-rsa1";
	$key     = $1;
	$comment = $2;
    }
    elsif ($keyline =~ /^(\d*\s\d*\s[0-9a-zA-Z]*)\s*$/) {
        # Protocol 1 but no comment field.
	$type    = "ssh-rsa1";
	$key     = $1;
    }
    elsif ($keyline =~
425
	   /^(ssh-rsa|ssh-dss|ssh-ed25519) ([-\w\.\@\+\/\=]*) ([-\w\@\.:\ ]*)$/) {
426 427 428 429 430
        # Protocol 2
	$type    = $1;
	$key     = "$1 $2";
	$comment = $3;
    }
431
    elsif ($keyline =~ /^(ssh-rsa|ssh-dss|ssh-ed25519) ([-\w\.\@\+\/\=:]*)$/) {
432 433 434 435 436 437
        # Protocol 2 but no comment field
	$type    = $1;
	$key     = "$1 $2";
    }

    if (!defined($key)) {
438
	print STDERR "Key cannot be parsed!\n";
439 440
	return 0;
    }
441

442 443
    # Do not enter into DB if in verify mode.
    if ($verify) {
Leigh Stoller's avatar
Leigh Stoller committed
444
	print "Key was good: $type\n";
445 446 447 448
	return 1;
    }

    #
449
    # Make up a comment field for the DB. 
450 451
    #
    if (!defined($comment)) {
452
	$comment = (defined($Comment) ? $Comment : "$type-${user_email}");
453 454
    }
    $key = "$key $comment";
455 456
    my $safe_key = DBQuoteSpecial($key);
    my $safe_comment = DBQuoteSpecial($comment);
457

458 459 460 461
    if ($remove) {
	DBQueryFatal("delete from user_pubkeys ".
		     "where uid_idx='$user_dbid' and comment=$safe_comment");
    }
462 463 464 465 466
    # Only one APT key allowed
    if ($isaptkey) {
	DBQueryFatal("delete from user_pubkeys ".
		     "where uid_idx='$user_dbid' and isaptkey=1");
    }
467 468
    DBQueryFatal("replace into user_pubkeys set ".
		 " uid='$user_uid', uid_idx='$user_dbid', ".
469
		 " internal='$internal', nodelete='$nodelete', ".
470
		 " isaptkey='$isaptkey',idx=NULL, stamp=now(), ".
471
		 " pubkey=$safe_key, comment=$safe_comment");
472

473 474 475
    #
    # Mark user record as modified so nodes are updated.
    #
476
    TBNodeUpdateAccountsByUID($user_uid);
477 478 479
    if (@PROTOGENI_SUPPORT@) {
	APT_Utility::UpdateInstancesByUser($target_user);
    }
480 481 482 483 484 485 486 487
    my $chunked = "";

    while (length($key)) {
	$chunked .= substr($key, 0, 65, "");
	if (length($key)) {
	    $chunked .= "\n";
	}
    }
488 489
    print "SSH Public Key for '$user' added:\n";
    print "$chunked\n";
490
    
491 492
    # Generate new auth keys file. 
    if ($genmode) {
Leigh Stoller's avatar
Leigh Stoller committed
493
	GenerateKeyFile();
494 495 496 497
    }

    if (! $noemail) {
	SENDMAIL("$user_name <$user_email>",
498 499
		 "SSH Public Key for '$user_uid' Added",
		 "SSH Public Key for '$user_uid' added:\n".
500 501
		 "\n".
		 "$chunked\n",
502
		 "$TBOPS", "Bcc: $TBAUDIT");
503 504
    }
    return 1;
505 506
}

507 508 509 510 511 512
#
# Init function for new users. Generate the first key for the user (which
# is loaded into the DB), and then generate the keyfiles. Note that the
# user might have preloaded personal keys.
#
sub InitUser()
513 514
{
    #
515
    # Want to delete existing keys from DB, but not the sslcert key.
516
    #
517 518 519 520 521 522 523 524 525 526 527 528 529
    DBQueryFatal("delete from user_pubkeys ".
		 "where uid_idx='$user_dbid' and ".
		 "      (internal=1 or comment like '%\@${OURDOMAIN}' or ".
		 "       comment like '%\@boss.${OURDOMAIN}') and ".
		 "      isaptkey=0 and comment not like 'sslcert:%'");

    # Redirect pub key to file, redirect STDERR to STDIN for display.
    my $outfile = tmpnam();
    my $command = "$ACCOUNTPROXY createsshkey $user_uid $user_gid ";
    	
    $UID = 0;
    open ERR, "$SSH -host $CONTROL '$command rsa' 2>&1 > $outfile |";
    $UID = $SAVEUID;
530

531 532 533 534 535 536 537 538 539
    $errs = "";
    while (<ERR>) {
	$errs .= $_;
    }
    close(ERR);
    print STDERR $errs;
    if ($?) {
	unlink($outfile);
	fatal("Could not create rsa key");
540
    }
Leigh Stoller's avatar
Leigh Stoller committed
541

542 543 544
    $pubkey      = `cat $outfile`;
    chomp($pubkey);
    $safe_pubkey = DBQuoteSpecial($pubkey);
545
    $comment     = "rsa\@${OURDOMAIN}";
546
    
547 548 549 550 551 552 553 554
    if (! DBQueryWarn("replace into user_pubkeys set ".
		      " uid='$user_uid', uid_idx='$user_dbid', ".
		      " internal='1', nodelete='1', idx=NULL, stamp=now(), ".
		      " pubkey=$safe_pubkey, comment='$comment'")) {
	unlink($outfile);
	fatal("Could not add rsa key to database");
    }
    unlink($outfile);
Leigh Stoller's avatar
Leigh Stoller committed
555
    return GenerateKeyFile();
556 557 558 559 560 561
}

#
# Generate ssh authorized_keys files. Either protocol 1 or 2.
# Returns 0 on success, -1 on failure.
#
Leigh Stoller's avatar
Leigh Stoller committed
562
sub GenerateKeyFile()
563
{
Leigh Stoller's avatar
Leigh Stoller committed
564
    my @pkeys   = ();
565
    my $outfile = tmpnam();
566
    my $sshdir  = "$HOMEDIR/$user_uid/.ssh";
567

Leigh Stoller's avatar
Leigh Stoller committed
568
    my $query_result =
569 570
	DBQueryFatal("select pubkey from user_pubkeys ".
		     "where uid_idx='$user_dbid'");
Leigh Stoller's avatar
Leigh Stoller committed
571 572 573

    while (my ($key) = $query_result->fetchrow_array()) {
	push(@pkeys, $key);
574
    }
Leigh Stoller's avatar
Leigh Stoller committed
575
    
576 577 578
    print "Generating authorized_keys ...\n";
    if (!open(AUTHKEYS, "> $outfile")) {
	warn("*** WARNING: Could not open $outfile: $!\n");
579 580 581 582
	return -1;
    }
    print AUTHKEYS "#\n";
    print AUTHKEYS "# DO NOT EDIT! This file auto generated by ".
583
	"Emulab account software.\n";
584 585 586 587 588 589 590 591 592 593
    print AUTHKEYS "#\n";
    print AUTHKEYS "# Please use the web interface to edit your ".
	"public key list.\n";
    print AUTHKEYS "#\n";
    
    foreach my $key (@pkeys) {
	print AUTHKEYS "$key\n";
    }
    close(AUTHKEYS);

594 595 596 597 598 599 600 601 602
    $UID = 0;
    system("$SSH -host $CONTROL ".
	   "'$ACCOUNTPROXY dropfile $user_uid $user_gid 0600 $sshdir ".
	   "authorized_keys' < $outfile");
    $UID = $SAVEUID;

    if ($?) {
	unlink($outfile);
	fatal("Could not copy authorized_keys file to $CONTROL");
Leigh Stoller's avatar
Leigh Stoller committed
603
    }
Leigh Stoller's avatar
Leigh Stoller committed
604
    unlink($outfile);
605 606 607
    return 0;
}

Russ Fish's avatar
Russ Fish committed
608 609 610
sub ParseXmlArgs($$$$$$) {
    my ($xmlfile, $table_name, $fields_ref, $debug,
	$args_ref, $errs_ref) = @_;
611 612 613 614 615
    #
    # Input args:
    #  $xmlfile	   - XML file path.
    #  $table_name - table_regex table_name for low-level checking patterns.
    #  $fields_ref - xmlfields specification (hash reference.)
Russ Fish's avatar
Russ Fish committed
616
    #  $debug
617 618 619 620 621
    #
    # Output args:
    #  $args_ref   - Parsed argument values (hash reference.)
    #  $errs_ref   - Error messages on failure (hash reference.)

Leigh Stoller's avatar
Leigh Stoller committed
622
    $debug = 0;
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651

    #
    # Must wrap the parser in eval since it exits on error.
    #
    my $xmlparse = eval { XMLin($xmlfile,
				VarAttr => 'name',
				ContentKey => '-content',
				SuppressEmpty => undef); };
    if ($@) {
	$errs_ref->{"XML Parse Error"} = "Return code $@";
	return;
    }

    #
    # Make sure all the required arguments were provided.
    #
    my $key;
    foreach $key (keys(%{ $fields_ref })) {
	my (undef, $required, undef) = @{$fields_ref->{$key}};

	$errs_ref->{$key} = "Required value not provided"
	    if ($required & $SLOT_REQUIRED  &&
		! exists($xmlparse->{'attribute'}->{"$key"}));
    }
    return
	if (keys(%{ $errs_ref }));

    foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
	my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
652 653 654
	if (!defined($value)) {	# Empty string comes from XML as an undef value.
	    $xmlparse->{'attribute'}->{"$key"}->{'value'} = $value = "";
	}
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693

	if ($debug) {
	    print STDERR "User attribute: '$key' -> '$value'\n";
	}

	$errs_ref->{$key} = "Unknown attribute"
	    if (!exists($fields_ref->{$key}));

	my ($dbslot, $required, $default) = @{$fields_ref->{$key}};

	if ($required & $SLOT_REQUIRED) {
	    # A slot that must be provided, so do not allow a null value.
	    if (!defined($value)) {
		$errs_ref->{$key} = "Must provide a non-null value";
		next;
	    }
	}
	if ($required & $SLOT_OPTIONAL) {
	    # Optional slot. If value is null skip it. Might not be the correct
	    # thing to do all the time?
	    if (!defined($value)) {
		next
		    if (!defined($default));
		$value = $default;
	    }
	}
	if ($required & $SLOT_ADMINONLY) {
	    # Admin implies optional, but thats probably not correct approach.
	    $errs_ref->{$key} = "Administrators only"
		if (! $this_user->IsAdmin());
	}

	# Now check that the value is legal.
	if (! TBcheck_dbslot($value, $table_name, $dbslot, 
			     TBDB_CHECKDBSLOT_ERROR)) {
	    $errs_ref->{$key} = TBFieldErrorString();
	    next;
	}

Russ Fish's avatar
Russ Fish committed
694
	$args_ref->{$key} = $value;
695 696 697
    }
}

698 699 700 701 702 703
sub fatal($) {
    my($mesg) = $_[0];

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