Skip to content

Commit 4e68abb

Browse files
committed
add api to bump last login date
1 parent 219bbed commit 4e68abb

File tree

14 files changed

+108
-21
lines changed

14 files changed

+108
-21
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ deployment/**/*
2222
!deployment/overrides/course-creator/
2323
!deployment/overrides/course-creator/config/
2424
!deployment/overrides/course-creator/config/config.ini
25+
!deployment/overrides/account-portal-docker-web/
26+
!deployment/overrides/account-portal-docker-web/config/
27+
!deployment/overrides/account-portal-docker-web/config/config.ini
2528
!deployment/overrides/account-portal-docker-web-green:8000/
2629
!deployment/overrides/account-portal-docker-web-green:8000/config/
2730
!deployment/overrides/account-portal-docker-web-green:8000/config/config.ini

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ For details on the changes in each release, see [the Releases page](https://gith
3434
- a new location `/lan` needs to be configured in your webserver
3535
- authorization: only IP addresses in your local area network should be allowed
3636
- authentication: none
37+
- `CGIPassAuth On`
3738
- a new LDAP posixGroup needs to be created for "immortal" users, who are exempt from automatic account expiration
3839
- the `[ldap]user_flag_groups[immortal]` open must also be defined
3940
- the `[site]account_policy_url` option has been renamed to `[site]pi_qualification_docs_url`
@@ -42,6 +43,9 @@ For details on the changes in each release, see [the Releases page](https://gith
4243
```sql
4344
drop trigger update_last_login;
4445
```
46+
- `[api]keys` can now be specified in the config file
47+
- `ServerName` must be specified in apache site if not already
48+
- `UseCanonicalName` must be set to `On` in apache site
4549

4650
### 1.5 -> 1.6
4751

@@ -74,7 +78,7 @@ Now, LDAP entries are created immediately for every user, so this is no longer n
7478
- Create LDAP entries for all existing requests
7579
```php
7680
use UnityWebPortal\lib\UnityUser;
77-
$_SERVER["HTTP_HOST"] = "worker"; // see deployment/overrides/worker/
81+
$_SERVER["SERVER_NAME"] = "worker"; // see deployment/overrides/worker/
7882
$_SERVER["REMOTE_ADDR"] = "127.0.0.1";
7983
require_once __DIR__ . "/../resources/autoload.php";
8084
foreach ($SQL->getAllRequests() as $request) {

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ See the Docker Compose environment (`tools/docker-dev/`) for an (unsafe for prod
6767
- `httpd` Authorization
6868
- Restricted access to `webroot/admin/`
6969
- Global access (with valid authentication) to `webroot/`
70+
- IP-based access (no authentication) to `lan/`
71+
- any IP address in your local area network should be authorized
7072
- No access anywhere else
7173
1. Authorization for your other services based on user flag groups
7274
- in order to access your services, a user should be in the `qualified` group and should not be in the `locked`, `idlelocked`, or `disabled` groups

defaults/config.ini.default

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,6 @@ disable_warning_days[] = 360
130130
disable_warning_days[] = 380
131131
disable_warning_days[] = 399
132132
disable_day = 400
133+
134+
[api]
135+
; keys[] = "INSERT_KEY_HERE_AND_UNCOMMENT"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[api]
2+
keys[] = "dev_environment_api_key"

deployment/overrides/phpunit/config/config.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ idlelock_day = 4
1515
disable_warning_days[] = 6
1616
disable_warning_days[] = 7
1717
disable_day = 8
18+
19+
[api]
20+
keys[] = "phpunit_api_key"

resources/lib/UnityConfig.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ public static function getConfig(string $def_config_loc, string $deploy_loc): ar
1111
{
1212
$CONFIG = _parse_ini_file($def_config_loc . "/config.ini.default", true, INI_SCANNER_TYPED);
1313
$CONFIG = self::pullConfig($CONFIG, $deploy_loc);
14-
if (array_key_exists("HTTP_HOST", $_SERVER)) {
15-
$cur_url = $_SERVER["HTTP_HOST"];
16-
self::assertHttpHostValid($cur_url);
17-
$url_override_path = $deploy_loc . "/overrides/" . $cur_url;
14+
if (array_key_exists("SERVER_NAME", $_SERVER)) {
15+
$server_name = $_SERVER["SERVER_NAME"];
16+
self::validateServerName($server_name);
17+
$url_override_path = $deploy_loc . "/overrides/" . $server_name;
1818
if (is_dir($url_override_path)) {
1919
$CONFIG = self::pullConfig($CONFIG, $url_override_path);
2020
}
@@ -126,10 +126,10 @@ public static function validateConfig(array $CONFIG): void
126126
}
127127
}
128128

129-
private static function assertHttpHostValid(string $host): void
129+
private static function validateServerName(string $server_name): void
130130
{
131-
if (!_preg_match("/^[a-zA-Z0-9._:-]+$/", $host)) {
132-
throw new \Exception("HTTP_HOST '$host' contains invalid characters!");
131+
if (!_preg_match("/^[a-zA-Z0-9._:-]+$/", $server_name)) {
132+
throw new \Exception("SERVER_NAME '$server_name' contains invalid characters!");
133133
}
134134
}
135135
}

resources/lib/UnityHTTPD.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ public static function gracefulDie(
8181
$user_message_body .= " $suffix";
8282
}
8383
self::errorLog($log_title, $log_message, data: $data, error: $error, errorid: $errorid);
84-
if (($_SERVER["REQUEST_METHOD"] ?? "") == "POST") {
84+
if (
85+
($_SERVER["REQUEST_METHOD"] ?? "") == "POST" &&
86+
!str_starts_with($_SERVER["REQUEST_URI"], "/lan/api/")
87+
) {
8588
self::messageError($user_message_title, $user_message_body);
8689
self::redirect();
8790
} else {
@@ -420,4 +423,19 @@ public static function getCSRFTokenHiddenFormInput(): string
420423
$token = htmlspecialchars(CSRFToken::generate());
421424
return "<input type='hidden' name='csrf_token' value='$token'>";
422425
}
426+
427+
public static function validateAPIKey(): void
428+
{
429+
$authorization = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
430+
if (!str_starts_with($authorization, "Bearer ")) {
431+
self::badRequest("HTTP_AUTHORIZATION is not Bearer", "invalid HTTP_AUTHORIZATION");
432+
}
433+
$key = trim(substr($authorization, strlen("Bearer ")));
434+
if ($key === "") {
435+
self::forbidden("empty API key", "forbidden");
436+
}
437+
if (!in_array($key, CONFIG["api"]["keys"])) {
438+
self::forbidden("API key not found in config", "forbidden");
439+
}
440+
}
423441
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
class BumpLastLoginApiTest extends UnityWebPortalTestCase
4+
{
5+
public function testBumpLastLoginApi()
6+
{
7+
global $USER, $SQL;
8+
$this->switchUser("Blank");
9+
$last_login_before = $SQL->getUserLastLogin($USER->uid);
10+
try {
11+
$this_year = date("Y");
12+
// set last login to one day after epoch
13+
callPrivateMethod($SQL, "setUserLastLogin", $USER->uid, 1 * 24 * 60 * 60);
14+
$old_timestamp_year = date("Y", $SQL->getUserLastLogin($USER->uid));
15+
$this->assertNotEquals($this_year, $old_timestamp_year);
16+
$this->http_post(
17+
__DIR__ . "/../../webroot/lan/api/bump-last-login.php",
18+
[],
19+
query_params: ["uid" => $USER->uid],
20+
bearer_token: "phpunit_api_key",
21+
);
22+
$new_timestamp_year = date("Y", $SQL->getUserLastLogin($USER->uid));
23+
$this->assertEquals($this_year, $new_timestamp_year);
24+
} finally {
25+
callPrivateMethod($SQL, "setUserLastLogin", $USER->uid, $last_login_before);
26+
}
27+
}
28+
}

test/phpunit-bootstrap.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
use PHPUnit\Framework\TestCase;
4040
use TRegx\PhpUnit\DataProviders\DataProvider as TRegxDataProvider;
4141

42-
$_SERVER["HTTP_HOST"] = "phpunit"; // used for config override
42+
$_SERVER["SERVER_NAME"] = "phpunit"; // used for config override
4343
require_once __DIR__ . "/../resources/config.php";
4444

4545
global $HTTP_HEADER_TEST_INPUTS;
@@ -82,7 +82,7 @@ function executeWorker(
8282
): array {
8383
global $LDAP;
8484
$command = sprintf(
85-
"HTTP_HOST=phpunit %s %s %s 2>&1",
85+
"SERVER_NAME=phpunit %s %s %s 2>&1",
8686
escapeshellarg(PHP_BINARY),
8787
escapeshellarg(__DIR__ . "/../workers/" . $basename),
8888
$args,
@@ -513,7 +513,7 @@ function switchUser(
513513
// session_start will be called on the first post()
514514
$_SERVER["REMOTE_USER"] = $eppn;
515515
$_SERVER["REMOTE_ADDR"] = "127.0.0.1";
516-
$_SERVER["HTTP_HOST"] = "phpunit"; // used for config override
516+
$_SERVER["SERVER_NAME"] = "phpunit"; // used for config override
517517
$_SERVER["eppn"] = $eppn;
518518
$_SERVER["givenName"] = $given_name;
519519
$_SERVER["sn"] = $sn;
@@ -559,9 +559,10 @@ function assertNoWarningErrorMessages()
559559
function http_post(
560560
string $phpfile,
561561
array $post_data,
562-
array $query_parameters = [],
562+
array $query_params = [],
563563
bool $do_generate_csrf_token = true,
564564
bool $do_validate_messages = true,
565+
?string $bearer_token = null,
565566
): string {
566567
global $LDAP,
567568
$SQL,
@@ -580,11 +581,14 @@ function http_post(
580581
$_SERVER["REQUEST_METHOD"] = "POST";
581582
$_SERVER["PHP_SELF"] = _preg_replace("/.*webroot\//", "/", $phpfile);
582583
$_SERVER["REQUEST_URI"] = _preg_replace("/.*webroot\//", "/", $phpfile); // Slightly imprecise because it doesn't include get parameters
584+
if ($bearer_token !== null) {
585+
$_SERVER["HTTP_AUTHORIZATION"] = "Bearer $bearer_token";
586+
}
583587
if (!array_key_exists("csrf_token", $post_data) && $do_generate_csrf_token) {
584588
$post_data["csrf_token"] = CSRFToken::generate();
585589
}
586590
$_POST = $post_data;
587-
$_GET = $query_parameters;
591+
$_GET = $query_params;
588592
ob_start();
589593
try {
590594
$post_did_redirect_or_die = false;
@@ -609,8 +613,9 @@ function http_post(
609613

610614
function http_get(
611615
string $phpfile,
612-
array $get_data = [],
616+
array $query_params = [],
613617
bool $ignore_die = false,
618+
?string $bearer_token = null,
614619
$do_validate_messages = true,
615620
): string {
616621
global $LDAP,
@@ -630,7 +635,10 @@ function http_get(
630635
$_SERVER["REQUEST_METHOD"] = "GET";
631636
$_SERVER["PHP_SELF"] = _preg_replace("/.*webroot\//", "/", $phpfile);
632637
$_SERVER["REQUEST_URI"] = _preg_replace("/.*webroot\//", "/", $phpfile); // Slightly imprecise because it doesn't include get parameters
633-
$_GET = $get_data;
638+
if ($bearer_token !== null) {
639+
$_SERVER["HTTP_AUTHORIZATION"] = "Bearer $bearer_token";
640+
}
641+
$_GET = $query_params;
634642
ob_start();
635643
try {
636644
include $phpfile;

0 commit comments

Comments
 (0)