tbauth.php3 31.7 KB
Newer Older
1
2
<?php
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
3
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2008 University of Utah and the Flux Group.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
5
6
7
# All rights reserved.
#
#
8
# Login support: Beware empty spaces (cookies)!
9
10
#

11
12
13
14
# These global are to prevent repeated calls to the DB. 
#
$CHECKLOGIN_STATUS		= -1;
$CHECKLOGIN_UID			= 0;
15
$CHECKLOGIN_IDX			= null;
16
$CHECKLOGIN_NOLOGINS		= -1;
17
$CHECKLOGIN_WIKINAME            = "";
18
$CHECKLOGIN_IDLETIME            = 0;
19
20
$CHECKLOGIN_HASHKEY             = null;
$CHECKLOGIN_HASHHASH            = null;
21
$CHECKLOGIN_USER                = null;
22
23
24
25
26
27
28
29
30

#
# New Mapping. 
#
define("CHECKLOGIN_NOSTATUS",		-1);
define("CHECKLOGIN_NOTLOGGEDIN",	0);
define("CHECKLOGIN_LOGGEDIN",		1);
define("CHECKLOGIN_TIMEDOUT",		2);
define("CHECKLOGIN_MAYBEVALID",		4);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
31
32
define("CHECKLOGIN_STATUSMASK",		0x0000ff);
define("CHECKLOGIN_MODMASK",		0xffff00);
33
34
35
36
37
#
# These are modifiers of the above status fields. They are stored
# as a bit field in the top part. This is intended to localize as
# many queries related to login as possible. 
#
38
39
40
41
42
43
44
45
46
47
define("CHECKLOGIN_NEWUSER",		0x000100);
define("CHECKLOGIN_UNVERIFIED",		0x000200);
define("CHECKLOGIN_UNAPPROVED",		0x000400);
define("CHECKLOGIN_ACTIVE",		0x000800);
define("CHECKLOGIN_USERSTATUS",		0x000f00);
define("CHECKLOGIN_PSWDEXPIRED",	0x001000);
define("CHECKLOGIN_FROZEN",		0x002000);
define("CHECKLOGIN_ISADMIN",		0x004000);
define("CHECKLOGIN_TRUSTED",		0x008000);
define("CHECKLOGIN_CVSWEB",		0x010000);
48
define("CHECKLOGIN_ADMINON",		0x020000);
49
50
51
define("CHECKLOGIN_WEBONLY",		0x040000);
define("CHECKLOGIN_PLABUSER",		0x080000);
define("CHECKLOGIN_STUDLY",		0x100000);
52
define("CHECKLOGIN_WIKIONLY",		0x200000);
53
54
define("CHECKLOGIN_OPSGUY",		0x400000);	# Member of emulab-ops.
define("CHECKLOGIN_ISFOREIGN_ADMIN",	0x800000);	# Admin of another Emulab. 
55

56
57
58
#
# Constants for tracking possible login attacks.
#
59
60
define("DOLOGIN_MAXUSERATTEMPTS",	15);
define("DOLOGIN_MAXIPATTEMPTS",		25);
61

62
63
64
65
66
67
68
69
70
#
# Generate a hash value suitable for authorization. We use the results of
# microtime, combined with a random number.
# 
function GENHASH() {
    $fp = fopen("/dev/urandom", "r");
    if (! $fp) {
        TBERROR("Error opening /dev/urandom", 1);
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
71
    $random_bytes = fread($fp, 128);
72
73
    fclose($fp);

74
    $hash  = mhash (MHASH_MD5, bin2hex($random_bytes) . " " . microtime());
75
76
77
    return bin2hex($hash);
}

78
79
80
81
82
83
84
85
86
87
88
89
90
#
# Return the value of what we told the browser to remember for the login.
# Currently, this is an email address, stored in a long term cookie.
#
function REMEMBERED_ID() {
    global $TBEMAILCOOKIE;

    if (isset($_COOKIE[$TBEMAILCOOKIE])) {
	return $_COOKIE[$TBEMAILCOOKIE];
    }
    return null;
}

91
92
#
# Return the value of the currently logged in uid, or null if not
93
# logged in. This interface is deprecated and being replaced.
94
95
# 
function GETLOGIN() {
96
97
98
99
    global $CHECKLOGIN_USER;
    
    if (CheckLogin($status))
	return $CHECKLOGIN_USER->uid();
100
101
102
103
104
105
106
107
108
109

    return FALSE;
}

#
# Return the value of the UID cookie. This does not check to see if
# this person is currently logged in. We just want to know what the
# browser thinks, if anything.
# 
function GETUID() {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
110
    global $TBNAMECOOKIE;
111
    $status_archived = TBDB_USERSTATUS_ARCHIVED;
112

Leigh B. Stoller's avatar
Leigh B. Stoller committed
113
    if (isset($_GET['nocookieuid'])) {
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
	$uid = $_GET['nocookieuid'];
	
	#
	# XXX - nocookieuid is sent by netbuild applet in URL. A few other java
        # apps as well, and we retain this for backwards compatability.
	#
        # Pedantic check
	if (! preg_match("/^[-\w]+$/", $uid)) {
	    return FALSE;
	}
	$safe_uid = addslashes($uid);

	#
	# Map this to an index (from a uid).
	#
	$query_result =
130
131
132
	    DBQueryFatal("select uid_idx from users ".
			 "where uid='$safe_uid' and ".
			 "      status!='$status_archived'");
133
134
135
136
137
138
    
	if (! mysql_num_rows($query_result))
	    return FALSE;
	
	$row = mysql_fetch_array($query_result);
	return $row[0];
Chad Barb's avatar
 
Chad Barb committed
139
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
140
    elseif (isset($_COOKIE[$TBNAMECOOKIE])) {
141
142
143
144
145
146
147
148
	$idx = $_COOKIE[$TBNAMECOOKIE];

        # Pedantic check
	if (! preg_match("/^[-\w]+$/", $idx)) {
	    return FALSE;
	}

	return $idx;
Chad Barb's avatar
 
Chad Barb committed
149
    }
150
    return FALSE;
151
152
}

153
#
154
# Verify a login by sucking UIDs current hash value out of the database.
155
156
157
# If the login has expired, or of the hashkey in the database does not
# match what came back in the cookie, then the UID is no longer logged in.
#
158
# Returns a combination of the CHECKLOGIN values above.
159
#
160
function LoginStatus() {
161
    global $TBAUTHCOOKIE, $TBLOGINCOOKIE, $HTTP_COOKIE_VARS, $TBAUTHTIMEOUT;
162
    global $CHECKLOGIN_STATUS, $CHECKLOGIN_UID, $CHECKLOGIN_NODETYPES;
163
    global $CHECKLOGIN_WIKINAME, $TBOPSPID;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
164
    global $EXPOSEARCHIVE, $EXPOSETEMPLATES;
165
    global $CHECKLOGIN_HASHKEY, $CHECKLOGIN_HASHHASH;
166
    global $CHECKLOGIN_IDX, $CHECKLOGIN_USER;
167
    
168
169
170
171
172
173
    #
    # If we already figured this out, do not duplicate work!
    #
    if ($CHECKLOGIN_STATUS != CHECKLOGIN_NOSTATUS) {
	return $CHECKLOGIN_STATUS;
    }
174
175
176
177
178
179
180

    # No UID in the browser? Obviously not logged in!
    if (($uid_idx = GETUID()) == FALSE) {
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }
    $CHECKLOGIN_IDX = $uid_idx;
181

Chad Barb's avatar
 
Chad Barb committed
182
183
    # for java applet, we can send the key in the $auth variable,
    # rather than passing it is a cookie.
184
185
    if (isset($_GET['nocookieauth'])) {
	$curhash = $_GET['nocookieauth'];
186
187
    }
    elseif (array_key_exists($TBAUTHCOOKIE, $HTTP_COOKIE_VARS)) {
Chad Barb's avatar
 
Chad Barb committed
188
189
	$curhash = $HTTP_COOKIE_VARS[$TBAUTHCOOKIE];
    }
190
191
192
    if (array_key_exists($TBLOGINCOOKIE, $HTTP_COOKIE_VARS)) {
	$hashhash = $HTTP_COOKIE_VARS[$TBLOGINCOOKIE];
    }
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211

    #
    # We have to get at least one of the hashes. The Java applets do not
    # send it, but web browsers will.
    #
    if (!isset($curhash) && !isset($hashhash)) {
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }
    if (isset($curhash) &&
	! preg_match("/^[\w]+$/", $curhash)) {
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }
    if (isset($hashhash) &&
	! preg_match("/^[\w]+$/", $hashhash)) {
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }
212
213
214
215
216
217
218

    if (isset($curhash)) {
	$CHECKLOGIN_HASHKEY = $safe_curhash = addslashes($curhash);
    }
    if (isset($hashhash)) {
	$CHECKLOGIN_HASHHASH = $safe_hashhash = addslashes($hashhash);
    }
219
    $safe_idx = addslashes($uid_idx);
Chad Barb's avatar
 
Chad Barb committed
220
    
221
222
223
224
    #
    # Note that we get multiple rows back because of the group_membership
    # join. No big deal.
    # 
225
    $query_result =
226
	DBQueryFatal("select NOW()>=u.pswd_expires,l.hashkey,l.timeout, ".
227
		     "       status,admin,cvsweb,g.trust,l.adminon,webonly, " .
228
		     "       user_interface,n.type,u.stud,u.wikiname, ".
229
230
		     "       u.wikionly,g.pid,u.foreign_admin,u.uid_idx, " .
		     "       p.allow_workbench ".
231
		     " from users as u ".
232
233
		     "left join login as l on l.uid_idx=u.uid_idx ".
		     "left join group_membership as g on g.uid_idx=u.uid_idx ".
234
		     "left join projects as p on p.pid_idx=g.pid_idx ".
235
		     "left join nodetypeXpid_permissions as n on g.pid=n.pid ".
236
		     "where u.uid_idx='$safe_idx' and ".
237
238
239
		     (isset($curhash) ?
		      "l.hashkey='$safe_curhash'" :
		      "l.hashhash='$safe_hashhash'"));
240

241
    # No such user.
242
243
244
245
246
247
248
    if (! mysql_num_rows($query_result)) { 
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }
    
    #
    # Scan the rows. All the info is duplicate, except for the trust
249
    # values and the pid. pid is a hack.
250
    #
251
252
253
    $trusted   = 0;
    $opsguy    = 0;
    $workbench = 0;
254
    
255
256
257
258
259
260
261
262
263
264
265
266
    while ($row = mysql_fetch_array($query_result)) {
	$expired = $row[0];
	$hashkey = $row[1];
	$timeout = $row[2];
	$status  = $row[3];
	$admin   = $row[4];
	$cvsweb  = $row[5];

	if (! strcmp($row[6], "project_root") ||
	    ! strcmp($row[6], "group_root")) {
	    $trusted = 1;
	}
267
	$adminon  = $row[7];
268
	$webonly  = $row[8];
269
	$interface= $row[9];
270
271

	$type     = $row[10];
272
	$stud     = $row[11];
273
	$wikiname = $row[12];
274
	$wikionly = $row[13];
275

276
277
278
279
280
	# Check for an ops guy.
	$pid = $row[14];
	if ($pid == $TBOPSPID) {
	    $opsguy = 1;
	}
281
282
283

	# Set foreign_admin=1 for admins of another Emulab.
	$foreign_admin   = $row[15];
284
	$uid_idx         = $row[16];
285
	$workbench      += $row[17];
286

287
	$CHECKLOGIN_NODETYPES[$type] = 1;
288
    }
289
290
291
292

    #
    # If user exists, but login has no entry, quit now.
    #
293
294
295
296
    if (!$hashkey) {
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }
297

298
    #
299
    # Check for frozen account. Might do something interesting later.
300
    #
301
    if (! strcmp($status, TBDB_USERSTATUS_FROZEN)) {
302
	DBQueryFatal("DELETE FROM login WHERE uid_idx='$uid_idx'");
303
304
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
305
306
    }

307
    #
308
309
    # Check for expired login. Remove this entry from the logins table to
    # keep it from getting cluttered.
310
    #
311
    if (time() > $timeout) {
312
313
	DBQueryFatal("delete from login where ".
		     "uid_idx='$uid_idx' and hashkey='$hashkey'");
314
315
316
	$CHECKLOGIN_STATUS = CHECKLOGIN_TIMEDOUT;
	return $CHECKLOGIN_STATUS;
    }
317
    $CHECKLOGIN_IDLETIME = time() - ($timeout - $TBAUTHTIMEOUT);
318
319

    #
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
    # We know the login has not expired. The problem is that we might not
    # have received a cookie since that is set to transfer only when using
    # https. However, we do not want the menu to be flipping back and forth
    # each time the user uses http (say, for documentation), and so the lack
    # of a cookie does not provide enough info to determine if the user is
    # logged in or not from the current browser. Also, we want to allow for
    # a user to switch browsers, and not get confused by getting a uid but
    # no valid cookie from the new browser. In that case the user should just
    # be able to login from the new browser; gets a standard not-logged-in
    # front page. In order to accomplish this, we need another cookie that is
    # set on login, cleared on logout.
    #
    if (isset($curhash)) {
	#
	# Got a cookie (https).
335
	#
336
337
338
339
340
341
342
343
344
	if ($curhash != $hashkey) {
	    #
	    # User is not logged in from this browser. Must be stale.
	    # 
	    $CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	    return $CHECKLOGIN_STATUS;
	}
	else {
            #
345
346
	    # User is logged in.
	    #
347
348
	    $CHECKLOGIN_STATUS = CHECKLOGIN_LOGGEDIN;
	}
349
350
    }
    else {
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
	#
	# No cookie. Might be because its http, so there is no way to tell
	# if user is not logged in from the current browser without more
	# information. We use another cookie for this, which is a crc of
	# of the real hash, and simply tells us what menu to draw, but does
	# not impart any privs!
	#
	if (isset($hashhash) &&
	    $hashhash == bin2hex(mhash(MHASH_CRC32, $hashkey))) {
            #
            # The login is probably valid, but we have no proof yet. 
            #
	    $CHECKLOGIN_STATUS = CHECKLOGIN_MAYBEVALID;
	}
	else {
	    #
	    # No hash of the hash, so assume no real cookie either. 
	    # 
	    $CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	    return $CHECKLOGIN_STATUS;
	}
372
373
    }

374
375
376
377
378
379
380
    # Cache this now; someone will eventually want it.
    $CHECKLOGIN_USER = User::Lookup($uid_idx);
    if (! $CHECKLOGIN_USER) {
	$CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;
	return $CHECKLOGIN_STATUS;
    }

381
    #
382
    # Now add in the modifiers.
383
    #
384
385
    # Do not expire passwords for admin users.
    if ($expired && !$admin)
386
387
388
	$CHECKLOGIN_STATUS |= CHECKLOGIN_PSWDEXPIRED;
    if ($admin)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ISADMIN;
389
390
    if ($adminon)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ADMINON;
391
392
    if ($webonly)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_WEBONLY;
393
394
    if ($wikionly)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_WIKIONLY;
395
396
    if ($trusted)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_TRUSTED;
397
398
    if ($stud)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_STUDLY;
399
400
    if ($cvsweb)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_CVSWEB;
401
    if ($interface == TBDB_USER_INTERFACE_PLAB)
402
	$CHECKLOGIN_STATUS |= CHECKLOGIN_PLABUSER;
403
404
405
406
407
408
409
410
    if (strcmp($status, TBDB_USERSTATUS_NEWUSER) == 0)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_NEWUSER;
    if (strcmp($status, TBDB_USERSTATUS_UNAPPROVED) == 0)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_UNAPPROVED;
    if (strcmp($status, TBDB_USERSTATUS_UNVERIFIED) == 0)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_UNVERIFIED;
    if (strcmp($status, TBDB_USERSTATUS_ACTIVE) == 0)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ACTIVE;
411
412
    if (isset($wikiname) && $wikiname != "")
	$CHECKLOGIN_WIKINAME = $wikiname;
413
414
    if ($opsguy)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_OPSGUY;
415
416
    if ($foreign_admin)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ISFOREIGN_ADMIN;
417

418
419
420
    #
    # Set the magic enviroment variable, if appropriate, for the sake of
    # any processes we might spawn. We prepend an HTTP_ on the front of
421
    # the variable name, so that it will get through suexec.
422
    #
423
    if ($admin && $adminon) {
424
425
    	putenv("HTTP_WITH_TB_ADMIN_PRIVS=1");
    }
426
427
428
429
430
431
432
433
434
    #
    # This environment variable is likely to become the new method for
    # specifying the credentials of the invoking user. Still thinking
    # about this, but the short story is that the web interface should
    # not invoke so much stuff as the user, but rather as a neutral user
    # with implied credentials. 
    #
    putenv("HTTP_INVOKING_USER=" . $CHECKLOGIN_USER->webid());
    
435
436
437
    # XXX Temporary.
    if ($stud) {
	$EXPOSEARCHIVE = 1;
438
439
440
    }
    if ($workbench) {
	$EXPOSETEMPLATES = $EXPOSEARCHIVE = 1;
441
    }
442
    return $CHECKLOGIN_STATUS;
443
444
445
446
}

#
# This one checks for login, but then dies with an appropriate error
447
448
# message. The modifier allows you to turn off checks for specified
# conditions. 
449
#
450
function LOGGEDINORDIE($uid, $modifier = 0, $login_url = NULL) {
451
    global $TBBASE, $BASEPATH;
452
    global $TBAUTHTIMEOUT, $CHECKLOGIN_HASHKEY, $CHECKLOGIN_IDX;
453

454
    #
455
456
457
458
459
    # We now ignore the $uid argument and let LoginStatus figure it out.
    #
    
    #
    # Allow the caller to specify a different URL to direct the user to
460
461
462
463
464
465
    #
    if (!$login_url) {
	$login_url = "$TBBASE/login.php3?refer=1";
    }

    $link = "\n<a href=\"$login_url\">Please ".
466
	"log in again.</a>\n";
467

468
    $status = LoginStatus();
469
470
471

    switch ($status & CHECKLOGIN_STATUSMASK) {
    case CHECKLOGIN_NOTLOGGEDIN:
472
        USERERROR("You do not appear to be logged in! $link", 1);
473
        break;
474
    case CHECKLOGIN_TIMEDOUT:
475
        USERERROR("Your login has timed out! $link", 1);
476
        break;
477
478
    case CHECKLOGIN_MAYBEVALID:
        USERERROR("Your login cannot be verified. Are cookies turned on? ".
479
		  "Are you using https? Are you logged in using another ".
480
		  "browser or another machine? $link", 1);
481
        break;
482
    case CHECKLOGIN_LOGGEDIN:
483
484
485
486
487
488
        #
	# Update the time in the database.
        # Basically, each time the user does something, we bump the
	# logout further into the future. This avoids timing them
	# out just when they are doing useful work.
        #
489
490
	if (! is_null($CHECKLOGIN_HASHKEY)) {
	    $timeout = time() + $TBAUTHTIMEOUT;
491

492
	    DBQueryFatal("UPDATE login set timeout='$timeout' ".
493
494
			 "where uid_idx='$CHECKLOGIN_IDX' and ".
			 "      hashkey='$CHECKLOGIN_HASHKEY'");
495
	}
496
497
498
	break;
    default:
	TBERROR("LOGGEDINORDIE failed mysteriously", 1);
499
    }
500
501
502
503
504
505
506

    $status = $status & ~$modifier;

    #
    # Check other conditions.
    #
    if ($status & CHECKLOGIN_PSWDEXPIRED)
507
508
        USERERROR("Your password has expired. ".
		  "<a href=moduserinfo.php3>Please change it now!</a>", 1);
509
510
511
512
513
514
    if ($status & CHECKLOGIN_FROZEN)
        USERERROR("Your account has been frozen!", 1);
    if ($status & (CHECKLOGIN_UNVERIFIED|CHECKLOGIN_NEWUSER))
        USERERROR("You have not verified your account yet!", 1);
    if ($status & CHECKLOGIN_UNAPPROVED)
        USERERROR("Your account has not been approved yet!", 1);
515
    if (($status & CHECKLOGIN_WEBONLY) && ! ISADMIN())
516
        USERERROR("Your account does not permit you to access this page!", 1);
517
    if (($status & CHECKLOGIN_WIKIONLY) && ! ISADMIN())
518
        USERERROR("Your account does not permit you to access this page!", 1);
519
520
521
522
523

    #
    # Lastly, check for nologins here. This heads off a bunch of other
    # problems and checks we would need.
    #
524
    if (NOLOGINS() && !ISADMIN())
525
526
527
        USERERROR("Sorry. The Web Interface is ".
		  "<a href=nologins.php3>Temporarily Unavailable!</a>", 1);

528
529
530
531
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
    # No one should ever look at the return value of this function.
    return null;
}

#
# This is the new interface to the above function. 
#
function CheckLoginOrDie($modifier = 0, $login_url = NULL)
{
    global $CHECKLOGIN_USER;
    
    LOGGEDINORDIE(GETUID(), $modifier, $login_url);

    #
    # If this returns, login is valid. Return the user object to caller.
    #
    return $CHECKLOGIN_USER;
}

#
# This interface allows the return of the actual status. I know, its a
# global variable, but this interface is cleaner. 
#
function CheckLogin(&$status)
{
    global $CHECKLOGIN_USER;

    $status = LoginStatus();

    # If login looks valid, return the user. 
    if ($status & (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_MAYBEVALID))
	    return $CHECKLOGIN_USER;

    return null;
562
563
564
}

#
565
566
567
568
# Is this user an admin type, and is his admin bit turned on.
# Its actually incorrect to look at the $uid. Its the currently logged
# in user that has to be admin. So ignore the uid and make sure
# there is a login status.
569
#
570
function ISADMIN() {
571
572
    global $CHECKLOGIN_STATUS;
    
573
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
574
	return 0;
575
    }
576

577
    return (($CHECKLOGIN_STATUS &
578
579
	     (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_ISADMIN|CHECKLOGIN_ADMINON)) ==
	    (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_ISADMIN|CHECKLOGIN_ADMINON));
580
581
}

582
583
584
585
586
587
588
589
590
591
592
593
594
595
596

# Is this user an admin of another Emulab.
function ISFOREIGN_ADMIN($uid = 1) {
    global $CHECKLOGIN_STATUS;

    # Definitely not, if not logged in.
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
	return 0;
    }

    return (($CHECKLOGIN_STATUS &
	     (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_ISFOREIGN_ADMIN)) ==
	    (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_ISFOREIGN_ADMIN));
}

597
598
599
600
function STUDLY() {
    global $CHECKLOGIN_STATUS;
    
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
601
	TBERROR("STUDLY: user is not logged in!", 1);
602
603
604
605
606
607
608
    }

    return (($CHECKLOGIN_STATUS &
	     (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_STUDLY)) ==
	    (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_STUDLY));
}

609
610
611
612
function OPSGUY() {
    global $CHECKLOGIN_STATUS;
    
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
613
	TBERROR("OPSGUY: user is not logged in!", 1);
614
615
616
617
618
619
620
    }

    return (($CHECKLOGIN_STATUS &
	     (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_OPSGUY)) ==
	    (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_OPSGUY));
}

621
622
623
624
function WIKIONLY() {
    global $CHECKLOGIN_STATUS;
    
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
625
	TBERROR("WIKIONLY: user is not logged in!", 1);
626
627
628
629
630
631
632
    }

    return (($CHECKLOGIN_STATUS &
	     (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_WIKIONLY)) ==
	    (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_WIKIONLY));
}

633
# Is this user a real administrator (ignore onoff bit).
634
function ISADMINISTRATOR() {
635
636
637
638
639
640
641
642
643
644
    global $CHECKLOGIN_STATUS;
    
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS)
	TBERROR("ISADMIN: $uid is not logged in!", 1);

    return (($CHECKLOGIN_STATUS &
	     (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_ISADMIN)) ==
	    (CHECKLOGIN_LOGGEDIN|CHECKLOGIN_ISADMIN));
}

645
646
647
#
# Toggle current login admin bit. Must be an administrator of course!
#
648
649
function SETADMINMODE($onoff) {
    global $CHECKLOGIN_HASHKEY, $CHECKLOGIN_IDX;
650
651
652
653
654
    
    # This makes sure the user is actually logged in secure (https).
    if (! ISADMINISTRATOR())
	return;

655
656
    # Be pedantic.
    if (! ($CHECKLOGIN_HASHKEY && $CHECKLOGIN_IDX))
657
	return;
658

659
    $onoff   = addslashes($onoff);
660
661
    $curhash = addslashes($CHECKLOGIN_HASHKEY);
    $uid_idx = $CHECKLOGIN_IDX;
662
663
    
    DBQueryFatal("update login set adminon='$onoff' ".
664
		 "where uid_idx='$uid_idx' and hashkey='$curhash'");
665
666
}

667
# Is this user a planetlab user? Returns 1 if they are, 0 if not.
668
669
670
function ISPLABUSER() {
    global $CHECKLOGIN_STATUS;

671
672
673
674
675
676
677
678
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
	#
	# For users who are not logged in, we need to check the database
	#
	$uid = GETUID();
	if (!$uid) {
	    return 0;
	}
679
680
	# Lookup sanitizes argument.
	if (! ($user = User::Lookup($uid)))
681
	    return 0;
682

683
684
685
686
	if ($user->user_interface()) {
	    return ($user->user_interface() == TBDB_USER_INTERFACE_PLAB);
	}
	else {
687
688
	    return 0;
	}
689
    } else {
690
	#
691
	# For logged-in users, we recorded it in the the login status
692
693
	#
	return (($CHECKLOGIN_STATUS &
694
695
		 (CHECKLOGIN_PLABUSER)) ==
		(CHECKLOGIN_PLABUSER));
696
697
698
    }
}

699
700
701
702
703
704
705
706
707
708
709
710
#
# Check to see if a user is allowed, in some project, to use the given node
# type. Returns 1 if allowed, 0 if not.
#
# NOTE: This is NOT intended as a real permissions check. It is intended only
# for display purposes (ie. deciding whether or not to give the user a link to
# the plab_ez page.) It does not require the user to be actually logged in, so
# that it still works for pages fetched through http. Thus, it may be possible
# for a clever user to fake it out.
#
function NODETYPE_ALLOWED($type) {
    global $CHECKLOGIN_NODETYPES;
711
712

    if (! GETUID())
713
	return 0;
714

Leigh B. Stoller's avatar
Leigh B. Stoller committed
715
    if (isset($CHECKLOGIN_NODETYPES[$type])) {
716
717
718
719
720
721
	return 1;
    } else {
	return 0;
    }
}

722
723
724
#
# Attempt a login.
# 
725
function DOLOGIN($token, $password, $adminmode = 0) {
726
727
    global $TBAUTHCOOKIE, $TBAUTHDOMAIN, $TBAUTHTIMEOUT;
    global $TBNAMECOOKIE, $TBLOGINCOOKIE, $TBSECURECOOKIES;
728
    global $TBMAIL_OPS, $TBMAIL_AUDIT, $TBMAIL_WWW;
729
    global $WIKISUPPORT, $WIKICOOKIENAME;
730
    global $BUGDBSUPPORT, $BUGDBCOOKIENAME;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
731
732
    
    # Caller makes these checks too.
733
734
    if ((!TBvalid_uid($token) && !TBvalid_email($token)) ||
	!isset($password) || $password == "") {
735
736
	return -1;
    }
737
    $now = time();
738

739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
    #
    # Check for a frozen IP address; too many failures.
    #
    unset($iprow);
    unset($IP);
    if (isset($_SERVER['REMOTE_ADDR'])) {
	$IP = $_SERVER['REMOTE_ADDR'];
	
	$ip_result =
	    DBQueryFatal("select * from login_failures ".
			 "where IP='$IP'");

	if ($iprow = mysql_fetch_array($ip_result)) {
	    $ipfrozen = $iprow['frozen'];

	    if ($ipfrozen) {
		DBQueryFatal("update login_failures set ".
			     "       failcount=failcount+1, ".
			     "       failstamp='$now' ".
			     "where IP='$IP'");
		return -1;
	    }
	}
    }

764
765
766
767
768
769
770
    if (TBvalid_email($token)) {
	$user = User::LookupByEmail($token);
    }
    else {
	$user = User::Lookup($token);
    }
	    
771
772
773
    #
    # Check password in the database against provided. 
    #
774
    do {
775
776
777
778
779
780
781
782
783
      if ($user) {
	$uid         = $user->uid();
        $db_encoding = $user->pswd();
	$isadmin     = $user->admin();
	$frozen      = $user->weblogin_frozen();
	$failcount   = $user->weblogin_failcount();
	$failstamp   = $user->weblogin_failstamp();
	$usr_email   = $user->email();
	$usr_name    = $user->name();
784
	$uid_idx     = $user->uid_idx();
785
	$usr_email   = $user->email();
786

787
788
	# Check for frozen accounts. We do not update the IP record when
	# an account is frozen.
789
	if ($frozen) {
790
	    $user->UpdateWebLoginFail();
791
792
793
	    return -1;
	}
	
794
        $encoding = crypt("$password", $db_encoding);
795
        if (strcmp($encoding, $db_encoding)) {
796
	    #
797
	    # Bump count and check for too many consecutive failures.
798
799
800
	    #
	    $failcount++;
	    if ($failcount > DOLOGIN_MAXUSERATTEMPTS) {
801
802
		$user->SetWebFreeze(1);
		
803
804
805
806
807
808
809
810
811
812
		TBMAIL("$usr_name '$uid' <$usr_email>",
		   "Web Login Freeze: '$uid'",
		   "Your login has been frozen because there were too many\n".
		   "login failures from " . $_SERVER['REMOTE_ADDR'] . ".\n\n".
		   "Testbed Operations has been notified.\n",
		   "From: $TBMAIL_OPS\n".
		   "Cc: $TBMAIL_OPS\n".
		   "Bcc: $TBMAIL_AUDIT\n".
		   "Errors-To: $TBMAIL_WWW");
	    }
813
	    $user->UpdateWebLoginFail();
814
            break;
815
        }
816
	#
817
	# Pass!
818
819
	#

820
	#
821
	# Set adminmode off on new logins, unless user requested to be
822
823
824
825
	# logged in as admin (and is an admin of course!). This is
	# primarily to bypass the nologins directive which makes it
	# impossible for an admin to login when the web interface is
	# turned off. 
826
	#
827
	$adminon = 0;
828
	if ($adminmode && $isadmin) {
829
	    $adminon = 1;
830
	}
831
832
833
834

        #
        # Insert a record in the login table for this uid.
	#
835
	if (DOLOGIN_MAGIC($uid, $uid_idx, $usr_email, $adminon) < 0) {
836
837
838
839
840
841
842
843
844
845
	    return -1;
	}

	#
	# Usage stats. 
	#
	DBQueryFatal("update user_stats set ".
		     " weblogin_count=weblogin_count+1, ".
		     " weblogin_last=now() ".
		     "where uid_idx='$uid_idx'");
846

847
848
849
850
	# Clear IP record since we have a sucessful login from the IP.
	if (isset($IP)) {
	    DBQueryFatal("delete from login_failures where IP='$IP'");
	}
851
	return 0;
852
853
      }
    } while (0);
854
855
856
    #
    # No such user
    #
857
    if (!isset($IP)) {
858
	return -1;
859
    }
860

861
    $ipfrozen = 0;
862
863
864
865
    if (isset($iprow)) {
	$ipfailcount = $iprow['failcount'];

        #
866
        # Bump count.
867
868
869
870
        #
	$ipfailcount++;
    }
    else {
871
872
873
	#
	# First failure.
	# 
874
875
	$ipfailcount = 1;
    }
876
877
878
879

    #
    # Check for too many consecutive failures.
    #
880
881
882
883
884
885
    if ($ipfailcount > DOLOGIN_MAXIPATTEMPTS) {
	$ipfrozen = 1;
	    
	TBMAIL($TBMAIL_OPS,
	       "Web Login Freeze: '$IP'",
	       "Logins has been frozen because there were too many login\n".
886
	       "failures from $IP. Last attempted uid was '$token'.\n\n",
887
888
889
890
891
892
893
894
	       "From: $TBMAIL_OPS\n".
	       "Bcc: $TBMAIL_AUDIT\n".
	       "Errors-To: $TBMAIL_WWW");
    }
    DBQueryFatal("replace into login_failures set ".
		 "       IP='$IP', ".
		 "       frozen='$ipfrozen', ".
		 "       failcount='$ipfailcount', ".
895
		 "       failstamp='$now'");
896
897
898
    return -1;
}

899
function DOLOGIN_MAGIC($uid, $uid_idx, $email = null, $adminon = 0)
900
901
{
    global $TBAUTHCOOKIE, $TBAUTHDOMAIN, $TBAUTHTIMEOUT;
902
    global $TBNAMECOOKIE, $TBLOGINCOOKIE, $TBSECURECOOKIES, $TBEMAILCOOKIE;
903
904
    global $TBMAIL_OPS, $TBMAIL_AUDIT, $TBMAIL_WWW;
    global $WIKISUPPORT, $WIKICOOKIENAME;
905
    global $BUGDBSUPPORT, $BUGDBCOOKIENAME, $TRACSUPPORT, $TRACCOOKIENAME;
906
907
908
909
910
    
    # Caller makes these checks too.
    if (!TBvalid_uid($uid)) {
	return -1;
    }
911
912
913
    if (!TBvalid_uididx($uid_idx)) {
	return -1;
    }
914
915
916
917
918
919
920
921
922
    $now = time();

    #
    # Insert a record in the login table for this uid with
    # the new hash value. If the user is already logged in, thats
    # okay; just update it in place with a new hash and timeout. 
    #
    $timeout = $now + $TBAUTHTIMEOUT;
    $hashkey = GENHASH();
923
924
    $crc     = bin2hex(mhash(MHASH_CRC32, $hashkey));

925
926
927
    DBQueryFatal("replace into login ".
		 "  (uid,uid_idx,hashkey,hashhash,timeout,adminon) values ".
		 "  ('$uid', $uid_idx, '$hashkey', '$crc', '$timeout', $adminon)");
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952

    #
    # Issue the cookie requests so that subsequent pages come back
    # with the hash value and auth usr embedded.

    #
    # For the hashkey, we use a zero timeout so that the cookie is
    # a session cookie; killed when the browser is exited. Hopefully this
    # keeps the key from going to disk on the client machine. The cookie
    # lives as long as the browser is active, but we age the cookie here
    # at the server so it will become invalid at some point.
    #
    setcookie($TBAUTHCOOKIE, $hashkey, 0, "/",
	      $TBAUTHDOMAIN, $TBSECURECOOKIES);

    #
    # Another cookie, to help in menu generation. See above in
    # checklogin. This cookie is a simple hash of the real hash,
    # intended to indicate if the current browser holds a real hash.
    # All this does is change the menu options presented, imparting
    # no actual privs. 
    #
    setcookie($TBLOGINCOOKIE, $crc, 0, "/", $TBAUTHDOMAIN, 0);

    #
953
954
955
956
    # We want to remember who the user was each time they load a page
    # NOTE: This cookie is integral to authorization, since we do not pass
    # around the UID anymore, but look for it in the cookie.
    #
957
    setcookie($TBNAMECOOKIE, $uid_idx, 0, "/", $TBAUTHDOMAIN, 0);
958
959
960
961
962
963
964
965
966

    #
    # This is a long term cookie so we can remember who the user was, and
    # and stick that in the login box.
    #
    if ($email) {
	$timeout = $now + (60 * 60 * 24 * 365);
	setcookie($TBEMAILCOOKIE, $email, $timeout, "/", $TBAUTHDOMAIN, 0);
    }
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987

    #
    # Clear the existing Wiki cookie so that there is not an old one
    # for a different user, sitting in the brower. 
    # 
    if ($WIKISUPPORT) {
	$flushtime = time() - 1000000;
	    
	setcookie($WIKICOOKIENAME, "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
    }
	
    #
    # Ditto for bugdb
    # 
    if ($BUGDBSUPPORT) {
	$flushtime = time() - 1000000;
	    
	setcookie($BUGDBCOOKIENAME, "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
    }
988
989
990
991
992
993
    # These cookie names are still in flux. 
    if ($TRACSUPPORT) {
	$flushtime = time() - 1000000;
	    
	setcookie("trac_auth_emulab", "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
994
995
	setcookie("trac_auth_emulab_priv", "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
996
997
	setcookie("trac_auth_protogeni", "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
998
999
	setcookie("trac_auth_protogeni_priv", "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
1000
    }
1001
	
1002
    DBQueryFatal("update users set ".
1003
		 "       weblogin_failcount=0,weblogin_failstamp=0 ".
1004
		 "where uid_idx='$uid_idx'");
1005
1006
1007

    return 0;
}
1008
1009
1010
1011
1012

#
# Verify a password
# 
function VERIFYPASSWD($uid, $password) {
1013
    if (! isset($password) || $password == "") {
1014
1015
1016
	return -1;
    }

1017
1018
    if (! ($user = User::Lookup($uid)))
	return -1;
1019
1020
1021
1022

    #
    # Check password in the database against provided. 
    #
1023
    $encoding = crypt("$password", $user->pswd());
1024
	
1025
    if ($encoding == $user->pswd()) {
1026
1027
1028
1029
1030
	return 0;
    }
    return -1;
}

1031
#
1032
# Log out a UID.
1033
#
1034
1035
1036
function DOLOGOUT($user) {
    global $CHECKLOGIN_STATUS, $CHECKLOGIN_USER;
    global $TBAUTHCOOKIE, $TBLOGINCOOKIE, $TBAUTHDOMAIN;
1037
    global $WIKISUPPORT, $WIKICOOKIENAME, $HTTP_COOKIE_VARS;
1038
    global $BUGDBSUPPORT, $BUGDBCOOKIENAME, $TRACSUPPORT, $TRACCOOKIENAME;
1039

1040
    if (! $CHECKLOGIN_USER)
1041
	return 1;
1042
1043
1044
1045
1046
1047
1048
1049
1050

    $uid_idx = $user->uid_idx();

    #
    # An admin logging out another user. Nothing else to do.
    #
    if (! $user->SameUser($CHECKLOGIN_USER)) {
	DBQueryFatal("delete from login where uid_idx='$uid_idx'");
	return 0;
1051
    }
1052

1053
1054
    $CHECKLOGIN_STATUS = CHECKLOGIN_NOTLOGGEDIN;

Leigh B. Stoller's avatar
Leigh B. Stoller committed
1055
1056
1057
1058
1059
1060
1061
1062
1063
    $curhash  = "";
    $hashhash = "";

    if (isset($HTTP_COOKIE_VARS[$TBAUTHCOOKIE])) {
	$curhash = $HTTP_COOKIE_VARS[$TBAUTHCOOKIE];
    }
    if (isset($HTTP_COOKIE_VARS[$TBLOGINCOOKIE])) {
	$hashhash = $HTTP_COOKIE_VARS[$TBLOGINCOOKIE];
    }
1064
1065
1066
1067
    
    #
    # We have to get at least one of the hashes. 
    #
Leigh B. Stoller's avatar
Leigh B. Stoller committed
1068
    if ($curhash == "" && $hashhash == "") {
1069
	return 1;
1070
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
1071
    if ($curhash != "" &&
1072
1073
1074
	! preg_match("/^[\w]+$/", $curhash)) {
	return 1;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
1075
    if ($hashhash != "" &&
1076
1077
1078
	! preg_match("/^[\w]+$/", $hashhash)) {
	return 1;
    }
1079

1080
    DBQueryFatal("delete from login ".
1081
		 " where uid_idx='$uid_idx' and ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
1082
1083
1084
		 ($curhash != "" ?
		  "hashkey='$curhash'" :
		  "hashhash='$hashhash'"));
1085

Leigh B. Stoller's avatar
Leigh B. Stoller committed
1086
1087
1088
    # Delete by giving timeout in the past
    $timeout = time() - 3600;

1089
    #
1090
    # Issue a cookie request to delete the cookies. Delete with timeout in past
1091
    #
1092
1093
    $timeout = time() - 3600;
    
1094
    setcookie($TBAUTHCOOKIE, "", $timeout, "/", $TBAUTHDOMAIN, 0);
1095
    setcookie($TBLOGINCOOKIE, "", $timeout, "/", $TBAUTHDOMAIN, 0);
1096
1097

    if ($TRACSUPPORT) {