Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f3eb806
add user expiry table to SQL
simonLeary42 Jan 16, 2026
d972d70
wip
simonLeary42 Jan 16, 2026
a31f9ef
add functions
simonLeary42 Jan 16, 2026
957fb79
switch to json
simonLeary42 Jan 16, 2026
1273e4d
use plain Exception
simonLeary42 Jan 16, 2026
7f45645
get user last login
simonLeary42 Jan 16, 2026
ffcec66
wip
simonLeary42 Jan 17, 2026
01bac38
must be
simonLeary42 Jan 20, 2026
8102563
wip
simonLeary42 Jan 20, 2026
d8fe607
move verbose, dry run to function
simonLeary42 Jan 20, 2026
c5810fa
more functions
simonLeary42 Jan 20, 2026
86819b4
one big for loop
simonLeary42 Jan 20, 2026
c630a55
flip conditional
simonLeary42 Jan 20, 2026
17e41d4
remove action log
simonLeary42 Jan 20, 2026
e345e6a
don't send non_pi template to pi with 0 members
simonLeary42 Jan 20, 2026
38d8438
write emails
simonLeary42 Jan 20, 2026
5f87181
record warnings to sql
simonLeary42 Jan 20, 2026
9bd72b8
private
simonLeary42 Jan 20, 2026
6d1f3d8
add hyperlink
simonLeary42 Jan 20, 2026
684f2c3
remove user with conflicting number
simonLeary42 Jan 20, 2026
5548e26
bootstrap last_login table
simonLeary42 Jan 20, 2026
7ae77ca
whitespace
simonLeary42 Jan 20, 2026
609c36d
phpstan
simonLeary42 Jan 28, 2026
637de1b
remove .php from sendMail
simonLeary42 Jan 28, 2026
1289377
don't send member warning to owner
simonLeary42 Jan 28, 2026
2614b2b
renamed util
simonLeary42 Jan 28, 2026
963e907
var is not array
simonLeary42 Jan 28, 2026
41a2ec8
renamed function
simonLeary42 Jan 28, 2026
1694939
don't remove mail, remove constant
simonLeary42 Jan 28, 2026
d046f63
more reenable
simonLeary42 Jan 28, 2026
9f9b0f9
add buttons to user-mgmt, signal danger
simonLeary42 Jan 28, 2026
dd181f0
revert user-mgmt
simonLeary42 Jan 29, 2026
2cfbf5b
fix return type
simonLeary42 Jan 29, 2026
668faa2
fix config
simonLeary42 Jan 29, 2026
cb03df2
fix column name
simonLeary42 Jan 29, 2026
e2f54c2
reset LDAP automatically
simonLeary42 Jan 29, 2026
6f6e8a9
fix sql syntax bug
simonLeary42 Jan 29, 2026
bdad562
fixup
simonLeary42 Jan 29, 2026
1442158
override HTTP_HOST in executeWorker
simonLeary42 Jan 29, 2026
29fe2fb
made some progress
simonLeary42 Jan 29, 2026
41ad2d2
move big regex to function
simonLeary42 Jan 29, 2026
c60c457
de-nest
simonLeary42 Jan 29, 2026
b77f5f2
SQL use insert instead of update
simonLeary42 Jan 29, 2026
678aae1
no more sprintf
simonLeary42 Jan 29, 2026
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
11 changes: 11 additions & 0 deletions defaults/config.ini.default
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,14 @@ url = "https://hooks.slack.com/services/T04BB3N3M26/B050A55CBNX/IGm1YA0VhjczAfs5
[page] ; which sql objects to use for the content on these pages
home = "home"
support = "support"

[expiry]
idlelock_warning_days[] = 180
idlelock_warning_days[] = 200
idlelock_warning_days[] = 219
idlelock_day = 220

disable_warning_days[] = 360
disable_warning_days[] = 380
disable_warning_days[] = 399
disable_day = 400
9 changes: 9 additions & 0 deletions deployment/overrides/phpunit/config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ allow_die = false
enable_verbose_error_log = false
enable_exception_handler = false
enable_error_handler = false

[expiry]
idlelock_warning_days[] = 2
idlelock_warning_days[] = 3
idlelock_day = 4

disable_warning_days[] = 6
disable_warning_days[] = 7
disable_day = 8
7 changes: 0 additions & 7 deletions resources/lib/UnityLDAP.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ class UnityLDAP extends LDAPConn
{
private const string RDN = "cn"; // The defauls RDN for LDAP entries is set to "common name"

public const array POSIX_ACCOUNT_CLASS = [
"inetorgperson",
"posixAccount",
"top",
"ldapPublicKey",
];

// isDisabled unset or set to "FALSE"
private static string $NON_DISABLED_FILTER = "(|(!(isDisabled=*))(isDisabled=FALSE))";

Expand Down
7 changes: 5 additions & 2 deletions resources/lib/UnityMailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ public function __construct()
}
}

/** @param string|string[] $recipients */
public function sendMail(string|array $recipients, string $template, mixed $data = null): bool
/**
* @param string|string[] $recipients
* @param ?mixed[] $data
*/
public function sendMail(string|array $recipients, string $template, ?array $data = null): bool
{
$this->setFrom($this->MSG_SENDER_EMAIL, $this->MSG_SENDER_NAME);
$this->addReplyTo($this->MSG_SUPPORT_EMAIL, $this->MSG_SUPPORT_NAME);
Expand Down
123 changes: 123 additions & 0 deletions resources/lib/UnitySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

use PDO;

enum UserExpiryWarningType: string
{
case IDLELOCK = "idlelock";
case DISABLE = "disable";
}

/**
* @phpstan-type account_deletion_request array{timestamp: string, uid: string}
* @phpstan-type user_last_login array{operator: string, last_login: string}
Expand All @@ -14,6 +20,8 @@
private const string TABLE_REQS = "requests";
private const string TABLE_AUDIT_LOG = "audit_log";
private const string TABLE_ACCOUNT_DELETION_REQUESTS = "account_deletion_requests";
private const string TABLE_USER_LAST_LOGINS = "user_last_logins";
private const string TABLE_USER_EXPIRY = "user_expiry";
// FIXME this string should be changed to something more intuitive, requires production change
public const string REQUEST_BECOME_PI = "admin";

Expand Down Expand Up @@ -194,4 +202,119 @@
$stmt->execute();
return $stmt->fetchAll();
}

/**
* returns an array where each key is a UID and each value is another array
* where each key is a warning type and each value is a sorted list of
* day numbers when a warning was sent
*
* day numbers count up from the last day that the user logged in,
* so on day 5 the user has been idle for 5 days
* for example, a user might get idlelock warnings on day numbers 180, 200, 219
* before actually getting idlelocked on day 220
* in this case, the output would be:
* ['username_here' => ['idlelock' => [180, 200, 219], 'disable' => []], ...]
* @return array<string, array<string, int[]>>
*/
public function getAllUsersExpirationWarningDaysSent(): array
{
$stmt = $this->conn->prepare("SELECT * FROM " . self::TABLE_USER_EXPIRY);
$stmt->execute();
$records = $stmt->fetchAll();
$output = [];
foreach ($records as $record) {
$uid = $record["uid"];
$idlelock = _json_decode($record["idlelock_warning_days_sent"]);
$disable = _json_decode($record["disable_warning_days_sent"]);
$output[$uid] = ["idlelock" => $idlelock, "disable" => $disable];
}
return $output;
}

/**
* example output: ['idlelock' => [1,2,3], 'disable' => [4,5,6]]
* @return array<string, int[]>
*/
public function getUserExpirationWarningDaysSent(string $uid): array
{
$stmt = $this->conn->prepare(
sprintf("SELECT * FROM %s WHERE uid=:uid", self::TABLE_USER_EXPIRY),
);
$stmt->bindParam(":uid", $uid);
$stmt->execute();
$records = $stmt->fetchAll();
switch (count($records)) {
case 0:
return ["idlelock" => [], "disable" => []];
case 1:
$record = $records[0];
$uid = $record["uid"];
$idlelock = _json_decode($record["idlelock_warning_days_sent"]);
$disable = _json_decode($record["disable_warning_days_sent"]);
return ["idlelock" => $idlelock, "disable" => $disable];
default:
throw new \Exception("multiple records found with uid='$uid'");
}
}

public function recordUserExpirationWarningDaySent(
string $uid,
UserExpiryWarningType $warning_type,
int $day,
): void {
$warning_type_to_days_sent = $this->getUserExpirationWarningDaysSent($uid);
array_push($warning_type_to_days_sent[$warning_type->value], $day);
sort($warning_type_to_days_sent[$warning_type->value]);
$idlelock_days_sent_str = _json_encode($warning_type_to_days_sent["idlelock"]);
$disable_days_sent_str = _json_encode($warning_type_to_days_sent["disable"]);
$table = self::TABLE_USER_EXPIRY;
$stmt = $this->conn->prepare("
INSERT INTO $table
(uid, idlelock_warning_days_sent, disable_warning_days_sent)
VALUES(:uid, :idlelock_days, :disable_days)
ON DUPLICATE KEY UPDATE
idlelock_warning_days_sent=:idlelock_days, disable_warning_days_sent=:disable_days
");
$stmt->bindParam(":uid", $uid);
$stmt->bindParam(":idlelock_days", $idlelock_days_sent_str);
$stmt->bindParam(":disable_days", $disable_days_sent_str);
$stmt->execute();
}

public function resetUserExpirationWarningDaysSent(string $uid): void
{
$table = self::TABLE_USER_EXPIRY;
$stmt = $this->conn->prepare("
INSERT INTO $table
(uid, idlelock_warning_days_sent, disable_warning_days_sent)
VALUES (:uid, '[]', '[]')
ON DUPLICATE KEY UPDATE
idlelock_warning_days_sent='[]', disable_warning_days_sent='[]'
");
$stmt->bindParam(":uid", $uid);
$stmt->execute();
}

/** @return string[] */
public function getAllUserLastLogins(): array
{
$stmt = $this->conn->prepare("SELECT * FROM " . self::TABLE_USER_LAST_LOGINS);
$stmt->execute();
return $stmt->fetchAll();
}

/* for testing purposes */
private function setUserLastLoginDaysAgo(string $uid, int $days): void

Check failure on line 307 in resources/lib/UnitySQL.php

View workflow job for this annotation

GitHub Actions / phpstan

Method UnityWebPortal\lib\UnitySQL::setUserLastLoginDaysAgo() is unused.
{
$datetime = date("Y-m-d H:i:s", time() - $days * 24 * 60 * 60);
$stmt = $this->conn->prepare(
sprintf(
"UPDATE %s SET last_login=:datetime WHERE operator=:uid",
self::TABLE_USER_LAST_LOGINS,
),
);
$stmt->bindParam(":uid", $uid);
$stmt->bindParam(":datetime", $datetime);
$stmt->execute();
}
}
3 changes: 2 additions & 1 deletion resources/lib/UnityUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function init(
]);
\ensure(!$this->entry->exists());
$this->entry->create([
"objectclass" => UnityLDAP::POSIX_ACCOUNT_CLASS,
"objectclass" => ["inetorgperson", "posixAccount", "top", "ldapPublicKey"],
"uid" => $this->uid,
"givenname" => $firstname,
"sn" => $lastname,
Expand Down Expand Up @@ -91,6 +91,7 @@ public function getFlag(UserFlag $flag): bool
return $this->LDAP->userFlagGroups[$flag->value]->memberUIDExists($this->uid);
}

/** if you want to set the "disabled" flag, you should probably use disable() or reEnable() */
public function setFlag(
UserFlag $flag,
bool $newValue,
Expand Down
25 changes: 25 additions & 0 deletions resources/mail/user_expiry_disable_warning_member.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<p>Hello,</p>
<p>
<?php

$this->Subject = "PI Group Expiration Warning";

$idle_days = $data["idle_days"];
$expiration_date = $data["expiration_date"];
$warning_number = $data["warning_number"];
$is_final_warning = $data["is_final_warning"];
$pi_group_gid = $data["pi_group_gid"];

echo "The PI group $pi_group_gid is set to be disabled on $expiration_date because the group owner has been idle for too long.\n";
if ($is_final_warning) {
echo "This is the final warning.\n";
} else {
echo "This is warning number $warning_number.\n";
}
?>
<p>
Upon expiration, this group's files will be permanently deleted,
and you will lose access to UnityHPC Platform services unless you are a member of any other PI group.
If you don't wish for this to happen,
remind your PI to reset the inactivity timer by simply logging in to the <?php echo getHyperlink("Unity account portal") ?>.
</p>
24 changes: 24 additions & 0 deletions resources/mail/user_expiry_disable_warning_non_pi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<p>Hello,</p>
<p>
<?php

$this->Subject = "Account Expiration Warning";

$idle_days = $data["idle_days"];
$expiration_date = $data["expiration_date"];
$warning_number = $data["warning_number"];
$is_final_warning = $data["is_final_warning"];

echo "Your account is set to be disabled on $expiration_date because you have been idle for too long.\n";
if ($is_final_warning) {
echo "This is the final warning.\n";
} else {
echo "This is warning number $warning_number.\n";
}
?>
</p>
<p>
Upon expiration, you will lose access to UnityHPC Platform services.
If you don't wish for this to happen,
reset the inactivity timer by simply logging in to the <?php echo getHyperlink("Unity account portal") ?>.
</p>
26 changes: 26 additions & 0 deletions resources/mail/user_expiry_disable_warning_pi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<p>Hello,</p>
<p>
<?php

$this->Subject = "Account Expiration Warning";

$idle_days = $data["idle_days"];
$expiration_date = $data["expiration_date"];
$warning_number = $data["warning_number"];
$is_final_warning = $data["is_final_warning"];
$pi_group_gid = $data["pi_group_gid"];

echo "Your account and PI group are set to be disabled on $expiration_date because you have been idle for too long.\n";
if ($is_final_warning) {
echo "This is the final warning.\n";
} else {
echo "This is warning number $warning_number.\n";
}
?>
<p>
Upon expiration, your files and your PI group's files will be permanently deleted,
you will lose access to UnityHPC Platform services,
and your group members also may lose access unless they are a member of some other group.
If you don't wish for this to happen,
reset the inactivity timer by simply logging in to the <?php echo getHyperlink("Unity account portal") ?>.
</p>
21 changes: 21 additions & 0 deletions resources/mail/user_expiry_idlelock_warning.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<p>Hello,</p>
<p>
<?php

$this->Subject = "Account Expiration Warning";

$idle_days = $data["idle_days"];
$expiration_date = $data["expiration_date"];
$warning_number = $data["warning_number"];
$is_final_warning = $data["is_final_warning"];

echo "Your account is set to be disabled on $expiration_date because you have been idle for too long.\n";
if ($is_final_warning) {
echo "This is the final warning.\n";
} else {
echo "This is warning number $warning_number.\n";
}
?>
<p>
Upon expiration, you will lose access to UnityHPC Platform services until you reset the inactivity timer by logging in to the <?php echo getHyperlink("Unity account portal") ?>.
</p>
Loading
Loading