Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ For details on the changes in each release, see [the Releases page](https://gith
- the `overrides` directory has been renamed to `domain_overrides`
- the size of the `recipient` column in the `audit_log` SQL table should be increased from 128 to 768
- the `check-ssh-keys.php` worker should be executed and any invalid SSH keys should be manually removed by an admin and any affected users should be notified
- the `pi_group_expiration_dates` SQL table should be created according to `bootstrap.sql`
- expiration dates should be set for all temporary groups

### 1.6 -> 1.7

Expand Down
67 changes: 67 additions & 0 deletions resources/lib/UnitySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

/**
* @phpstan-type user_last_login array{operator: string, last_login: int}
* @phpstan-type pi_group_expiration_date array{gid: string, expiration_date: int}
* @phpstan-type request array{request_for: string, uid: string, timestamp: string}
*/
class UnitySQL
{
private const string TABLE_REQS = "requests";
private const string TABLE_AUDIT_LOG = "audit_log";
private const string TABLE_USER_LAST_LOGINS = "user_last_logins";
private const string TABLE_PI_GROUP_EXPIRATION_DATES = "pi_group_expiration_dates";
// FIXME this string should be changed to something more intuitive, requires production change
public const string REQUEST_BECOME_PI = "admin";
private const int TABLE_AUDIT_LOG_RECIPIENT_MAX_MB_STR_LEN = 768;
Expand Down Expand Up @@ -276,4 +278,69 @@ public function getUserLastLogin(string $uid): ?int
$timestamp_str = $result[0]["last_login"];
return strtotime($timestamp_str);
}

/**
* @throws PDOException
* @throws \Exception if multiple records are found (this should never happen)
*/
public function getPIGroupExpirationDate(string $gid): int|null
{
$table = self::TABLE_PI_GROUP_EXPIRATION_DATES;
$stmt = $this->conn->prepare("SELECT * FROM $table WHERE gid=:gid");
$stmt->bindParam(":gid", $gid);
$stmt->execute();
$result = $stmt->fetchAll();
if (count($result) == 0) {
return null;
}
if (count($result) > 1) {
throw new \Exception("multiple records found with gid '$gid'");
}
$timestamp_str = $result[0]["expiration_date"];
return strtotime($timestamp_str);
}

/** @throws PDOException */
public function setPIGroupExpirationDate(string $gid, int $expiration_date): void
{
$table = self::TABLE_PI_GROUP_EXPIRATION_DATES;
$stmt = $this->conn->prepare("
INSERT INTO $table
VALUES (:gid, :expiration_date)
ON DUPLICATE KEY
UPDATE expiration_date=:expiration_date
");
$stmt->bindParam(":gid", $gid);
$expiration_date_str = date("Y-m-d H:i:s", $expiration_date);
$stmt->bindParam(":expiration_date", $expiration_date_str);
$stmt->execute();
}

/** @throws PDOException */
public function removePIGroupExpirationDate(string $gid): void
{
$table = self::TABLE_PI_GROUP_EXPIRATION_DATES;
$stmt = $this->conn->prepare("DELETE FROM $table WHERE gid=:gid");
$stmt->bindParam(":gid", $gid);
$stmt->execute();
}

/**
* @throws PDOException
* @return pi_group_expiration_date[]
*/
public function getAllPIGroupExpirationDates(): array
{
$stmt = $this->conn->prepare("SELECT * FROM " . self::TABLE_PI_GROUP_EXPIRATION_DATES);
$stmt->execute();
$records = $stmt->fetchAll();
$output = [];
foreach ($records as $record) {
array_push($output, [
"gid" => $record["gid"],
"expiration_date" => strtotime($record["expiration_date"]),
]);
}
return $output;
}
}
66 changes: 66 additions & 0 deletions test/functional/WorkerGroupExpiryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

use UnityWebPortal\lib\UnityGroup;

class WorkerGroupExpiryTest extends UnityWebPortalTestCase
{
private function runGroupExpiryWorker(int $timestamp)
{
[$_, $output_lines] = executeWorker("group-expiry.php", "--timestamp=$timestamp");
return $output_lines;
}

public function testGroupExpiryWorker()
{
global $USER, $LDAP, $SQL;
$this->switchUser("NormalPI");
$gid = UnityGroup::ownerUID2GID($USER->uid);
$pi_group_entry = $LDAP->getPIGroupEntry($gid);
$member_uids_before = $pi_group_entry->getAttribute("memberUid");
sort($member_uids_before);
$manager_uids_before = $pi_group_entry->getAttribute("managerUid");
$disabled_before = $pi_group_entry->getAttribute("isDisabled")[0] ?? null;
Comment thread
simonLeary42 marked this conversation as resolved.
$expiration_date_before = $SQL->getPIGroupExpirationDate($gid);
try {
$time = time();
$SQL->setPIGroupExpirationDate($gid, $time);
$output_lines = $this->runGroupExpiryWorker($time - 1);
$this->assertEquals([], $output_lines);
$output_lines = $this->runGroupExpiryWorker($time + 1);
$this->assertEquals(
[
sprintf(
"group '%s' expired on %s, disabling group and removing members %s",
$gid,
date("Y/m/d", $time),
_json_encode($member_uids_before),
),
],
$output_lines,
);
$output_lines = $this->runGroupExpiryWorker($time + 1);
$this->assertEquals([], $output_lines);
} finally {
$pi_group_entry->setAttribute("memberUid", $member_uids_before);
$pi_group_entry->setAttribute("managerUid", $manager_uids_before);
Comment thread
simonLeary42 marked this conversation as resolved.
if ($disabled_before === null) {
if ($pi_group_entry->hasAttribute("isDisabled")) {
$pi_group_entry->removeAttribute("isDisabled");
}
} else {
$pi_group_entry->setAttribute("isDisabled", $disabled_before);
}
if ($expiration_date_before === null) {
$SQL->removePIGroupExpirationDate($gid);
} else {
$SQL->setPIGroupExpirationDate($gid, $expiration_date_before);
}
$LDAP->userFlagGroups["qualified"]->overwriteMemberUIDs(
array_merge(
$LDAP->userFlagGroups["qualified"]->getMemberUIDs(),
$member_uids_before,
),
);
}
}
}
10 changes: 7 additions & 3 deletions test/functional/WorkerUnityCourseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ class WorkerUnityCourseTest extends UnityWebPortalTestCase
private static string $manager_uid = "user2_org1_test";
private static string $manager_mail = "user2@org1.test";
private static string $courseOwnerMail = "user2+cs124@org1.test";
private static string $expirationDate = "1970/01/02";

public function testCreateCourse()
{
global $LDAP, $USER;
global $LDAP, $USER, $SQL;
$this->switchUser("Blank");
$this->assertEquals(self::$manager_uid, $USER->uid);
$this->assertEquals(self::$manager_mail, $USER->getMail());
Expand All @@ -25,13 +26,12 @@ public function testCreateCourse()
self::$course_owner_name[1],
self::$course_owner_uid,
self::$manager_uid,
self::$expirationDate,
]);
$stdin_file_path = getPathFromFileHandle($stdin_file);
try {
executeWorker("unity-course.php", stdinFilePath: $stdin_file_path);
// error_log(implode("\n", $output_lines));
// our LDAP conn doesn't know about changes from subprocess
unset($GLOBALS["ldapconn"]);
Comment on lines -33 to -34

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer necessary since executeWorker was updated to pullObject on all LDAP entries in #598

$this->switchUser("Admin");
$pi_group_entry = $LDAP->getPIGroupEntry(self::$course_gid);
$owner_user_entry = $LDAP->getUserEntry(self::$course_owner_uid);
Expand All @@ -46,10 +46,14 @@ public function testCreateCourse()
[$manager->uid],
$pi_group_entry->getAttribute("manageruid"),
);
$newExpirationDate = $SQL->getPIGroupExpirationDate(self::$course_gid);
$this->assertNotNull($newExpirationDate);
$this->assertEquals(self::$expirationDate, date("Y/m/d", $newExpirationDate));
} finally {
ensurePIGroupDoesNotExist(self::$course_gid);
ensureUserDoesNotExist(self::$course_owner_uid);
unlink($stdin_file_path);
$SQL->removePIGroupExpirationDate(self::$course_gid);
}
}
}
5 changes: 5 additions & 0 deletions tools/docker-dev/sql/bootstrap.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ CREATE TABLE `requests` (
`uid` varchar(128) NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `pi_group_expiration_dates` (
`gid` varchar(131) NOT NULL PRIMARY KEY,
`expiration_date` timestamp NOT NULL
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
36 changes: 36 additions & 0 deletions workers/group-expiry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env php
<?php
include __DIR__ . "/init.php";
use Garden\Cli\Cli;
use UnityWebPortal\lib\UnityGroup;

$cli = new Cli();
$cli->description("Disable any PI groups which are past their expiration date.")
->opt("dry-run", "Print actions without actually doing anything.", type: "boolean")
->opt("timestamp", "Use this unix timestamp instead of right now", type: "int");
$args = $cli->parse($argv, true);
$dry_run = $args->getOpt("dry-run", false);
$now = $args->getOpt("timestamp", time());

foreach ($SQL->getAllPIGroupExpirationDates() as $record) {
$expiration_date = $record["expiration_date"];
if ($expiration_date <= $now) {
$group = new UnityGroup($record["gid"], $LDAP, $SQL, $MAILER);
if (!$group->getIsDisabled()) {
printf(
"group '%s' expired on %s, disabling group and removing members %s\n",
$group->gid,
date("Y/m/d", $expiration_date),
_json_encode($group->getMemberUIDs()),
);
if (!$dry_run) {
$group->disable();
}
}
}
}

if ($dry_run) {
echo "[DRY RUN]\n";
}

5 changes: 5 additions & 0 deletions workers/unity-course.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function flatten_attributes(array $attributes): array
$manager_uid = trim(
readline("Enter the UID of the group manager (example: simonleary_umass_edu): "),
);
$expiration = strtotime(trim(
readline("Enter the expiration date for the course (example: 2026/6/11): "),
));
$org_gid = cn2org($cn);

$manager = new UnityUser($manager_uid, $LDAP, $SQL, $MAILER);
Expand Down Expand Up @@ -62,6 +65,8 @@ function flatten_attributes(array $attributes): array
$course_pi_group->approveUser($manager);
$course_pi_group->addManagerUID($manager_uid);

$SQL->setPIGroupExpirationDate($course_pi_group->gid, $expiration);

print "LDAP entries created:\n";
print _json_encode(
[
Expand Down
Loading