Skip to content

Commit be03031

Browse files
authored
Api bump last login (#640)
1 parent 219bbed commit be03031

File tree

11 files changed

+91
-5
lines changed

11 files changed

+91
-5
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:8000/
26+
!deployment/overrides/account-portal-docker-web:8000/config/
27+
!deployment/overrides/account-portal-docker-web:8000/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: 2 additions & 0 deletions
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,7 @@ 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
4547

4648
### 1.5 -> 1.6
4749

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/UnityHTTPD.php

Lines changed: 20 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,20 @@ 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+
// this can happen when you don't enable apache CGIPassAuth
432+
self::badRequest("HTTP_AUTHORIZATION is not Bearer", "invalid HTTP_AUTHORIZATION");
433+
}
434+
$key = trim(substr($authorization, strlen("Bearer ")));
435+
if ($key === "") {
436+
self::forbidden("empty API key", "forbidden");
437+
}
438+
if (!in_array($key, CONFIG["api"]["keys"])) {
439+
self::forbidden("API key not found in config", "forbidden");
440+
}
441+
}
423442
}
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: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

tools/docker-dev/web/unity-apache.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<VirtualHost _default_:80>
2+
ServerName account-portal-docker-web
3+
UseCanonicalName On
24
DocumentRoot /var/www/unity-web-portal/webroot
35
<LocationMatch '^/(panel|admin)'>
46
AuthType Basic
@@ -13,5 +15,6 @@
1315
# in production you should require an address in your LAN, example "Require ip 10.0.0.0/8"
1416
AuthType basic
1517
Require all granted
18+
CGIPassAuth On
1619
</Location>
1720
</VirtualHost>

0 commit comments

Comments
 (0)