tbauth.php3 31.1 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
386
387
    if ($expired)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_PSWDEXPIRED;
    if ($admin)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ISADMIN;
388
389
    if ($adminon)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ADMINON;
390
391
    if ($webonly)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_WEBONLY;
392
393
    if ($wikionly)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_WIKIONLY;
394
395
    if ($trusted)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_TRUSTED;
396
397
    if ($stud)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_STUDLY;
398
399
    if ($cvsweb)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_CVSWEB;
400
    if ($interface == TBDB_USER_INTERFACE_PLAB)
401
	$CHECKLOGIN_STATUS |= CHECKLOGIN_PLABUSER;
402
403
404
405
406
407
408
409
    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;
410
411
    if (isset($wikiname) && $wikiname != "")
	$CHECKLOGIN_WIKINAME = $wikiname;
412
413
    if ($opsguy)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_OPSGUY;
414
415
    if ($foreign_admin)
	$CHECKLOGIN_STATUS |= CHECKLOGIN_ISFOREIGN_ADMIN;
416

417
418
419
    #
    # Set the magic enviroment variable, if appropriate, for the sake of
    # any processes we might spawn. We prepend an HTTP_ on the front of
420
    # the variable name, so that it will get through suexec.
421
    #
422
    if ($admin && $adminon) {
423
424
    	putenv("HTTP_WITH_TB_ADMIN_PRIVS=1");
    }
425
426
427
428
429
430
431
432
433
    #
    # 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());
    
434
435
436
    # XXX Temporary.
    if ($stud) {
	$EXPOSEARCHIVE = 1;
437
438
439
    }
    if ($workbench) {
	$EXPOSETEMPLATES = $EXPOSEARCHIVE = 1;
440
    }
441
    return $CHECKLOGIN_STATUS;
442
443
444
445
}

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

453
    #
454
455
456
457
458
    # 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
459
460
461
462
463
464
    #
    if (!$login_url) {
	$login_url = "$TBBASE/login.php3?refer=1";
    }

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

467
    $status = LoginStatus();
468
469
470

    switch ($status & CHECKLOGIN_STATUSMASK) {
    case CHECKLOGIN_NOTLOGGEDIN:
471
        USERERROR("You do not appear to be logged in! $link", 1);
472
        break;
473
    case CHECKLOGIN_TIMEDOUT:
474
        USERERROR("Your login has timed out! $link", 1);
475
        break;
476
477
    case CHECKLOGIN_MAYBEVALID:
        USERERROR("Your login cannot be verified. Are cookies turned on? ".
478
		  "Are you using https? Are you logged in using another ".
479
		  "browser or another machine? $link", 1);
480
        break;
481
    case CHECKLOGIN_LOGGEDIN:
482
483
484
485
486
487
        #
	# 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.
        #
488
489
	if (! is_null($CHECKLOGIN_HASHKEY)) {
	    $timeout = time() + $TBAUTHTIMEOUT;
490

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

    $status = $status & ~$modifier;

    #
    # Check other conditions.
    #
    if ($status & CHECKLOGIN_PSWDEXPIRED)
506
507
        USERERROR("Your password has expired. ".
		  "<a href=moduserinfo.php3>Please change it now!</a>", 1);
508
509
510
511
512
513
    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);
514
    if (($status & CHECKLOGIN_WEBONLY) && ! ISADMIN())
515
        USERERROR("Your account does not permit you to access this page!", 1);
516
    if (($status & CHECKLOGIN_WIKIONLY) && ! ISADMIN())
517
        USERERROR("Your account does not permit you to access this page!", 1);
518
519
520
521
522

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

527
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
    # 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;
561
562
563
}

#
564
565
566
567
# 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.
568
#
569
function ISADMIN() {
570
571
    global $CHECKLOGIN_STATUS;
    
572
    if ($CHECKLOGIN_STATUS == CHECKLOGIN_NOSTATUS) {
573
	return 0;
574
    }
575

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

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

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

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

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

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

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

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

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

632
# Is this user a real administrator (ignore onoff bit).
633
function ISADMINISTRATOR() {
634
635
636
637
638
639
640
641
642
643
    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));
}

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

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

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

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

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

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

698
699
700
701
702
703
704
705
706
707
708
709
#
# 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;
710
711

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

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

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

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

763
764
765
766
767
768
769
    if (TBvalid_email($token)) {
	$user = User::LookupByEmail($token);
    }
    else {
	$user = User::Lookup($token);
    }
	    
770
771
772
    #
    # Check password in the database against provided. 
    #
773
    do {
774
775
776
777
778
779
780
781
782
      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();
783
	$uid_idx     = $user->uid_idx();
784
	$usr_email   = $user->email();
785

786
787
	# Check for frozen accounts. We do not update the IP record when
	# an account is frozen.
788
	if ($frozen) {
789
	    $user->UpdateWebLoginFail();
790
791
792
	    return -1;
	}
	
793
        $encoding = crypt("$password", $db_encoding);
794
        if (strcmp($encoding, $db_encoding)) {
795
	    #
796
	    # Bump count and check for too many consecutive failures.
797
798
799
	    #
	    $failcount++;
	    if ($failcount > DOLOGIN_MAXUSERATTEMPTS) {
800
801
		$user->SetWebFreeze(1);
		
802
803
804
805
806
807
808
809
810
811
		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");
	    }
812
	    $user->UpdateWebLoginFail();
813
            break;
814
        }
815
	#
816
	# Pass!
817
818
	#

819
	#
820
	# Set adminmode off on new logins, unless user requested to be
821
822
823
824
	# 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. 
825
	#
826
	$adminon = 0;
827
	if ($adminmode && $isadmin) {
828
	    $adminon = 1;
829
	}
830
831
832
833

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

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

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

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

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

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

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

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

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

    #
952
953
954
955
    # 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.
    #
956
    setcookie($TBNAMECOOKIE, $uid_idx, 0, "/", $TBAUTHDOMAIN, 0);
957
958
959
960
961
962
963
964
965

    #
    # 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);
    }
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986

    #
    # 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);
    }
987
988
989
990
991
992
993
994
995
    # These cookie names are still in flux. 
    if ($TRACSUPPORT) {
	$flushtime = time() - 1000000;
	    
	setcookie("trac_auth_emulab", "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
	setcookie("trac_auth_protogeni", "", $flushtime, "/",
		  $TBAUTHDOMAIN, $TBSECURECOOKIES);
    }
996
	
997
    DBQueryFatal("update users set ".
998
		 "       weblogin_failcount=0,weblogin_failstamp=0 ".
999
		 "where uid_idx='$uid_idx'");
1000