Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2fce169
disband, reinstate PI groups
simonLeary42 Jan 12, 2026
94f3ab1
update phpunit-bootstrap
simonLeary42 Jan 14, 2026
5ebcaba
assert not isPI()
simonLeary42 Jan 14, 2026
3dec60b
test cases moved to other PR
simonLeary42 Jan 14, 2026
b151375
fix nickname
simonLeary42 Jan 14, 2026
05841b2
Update resources/mail/group_reenabled.php
simonLeary42 Jan 14, 2026
14a55bb
Update resources/lib/UnityGroup.php
simonLeary42 Jan 14, 2026
ba21aee
handle double click, send messages
simonLeary42 Jan 14, 2026
bccf20e
Update resources/mail/group_reenabled.php
simonLeary42 Jan 14, 2026
68fa90b
Update tools/docker-dev/identity/unity-cluster-schema.ldif
simonLeary42 Jan 14, 2026
c0ae97c
handle edge case no members
simonLeary42 Jan 14, 2026
5624eb6
fix test
simonLeary42 Jan 15, 2026
9b8fc45
Revert "comment out functions not yet implemented"
simonLeary42 Jan 16, 2026
5004f5f
update function name
simonLeary42 Jan 16, 2026
e6c1334
uncomment
simonLeary42 Jan 16, 2026
2b6ddff
fix function name
simonLeary42 Jan 16, 2026
09daf39
rename variable
simonLeary42 Jan 16, 2026
99bff86
remove comment
simonLeary42 Jan 16, 2026
6432721
remove dangling reference
simonLeary42 Jan 20, 2026
e4fa67f
fix bugs
simonLeary42 Jan 21, 2026
6424a0e
cast for phpstan
simonLeary42 Jan 21, 2026
9a2520f
cast for phpstan
simonLeary42 Jan 21, 2026
ebca706
cast different
simonLeary42 Jan 21, 2026
d015f72
disallow disable if members
simonLeary42 Jan 22, 2026
7a004f0
prettier
simonLeary42 Jan 22, 2026
830887e
use label
simonLeary42 Jan 22, 2026
9dc866a
explain, flexbox
simonLeary42 Jan 22, 2026
5d2515c
align
simonLeary42 Jan 22, 2026
84a3999
simplify disable() since there are no members
simonLeary42 Jan 22, 2026
cc3c2e2
setIsDisabled should be private
simonLeary42 Jan 22, 2026
0e64af8
remove test
simonLeary42 Jan 22, 2026
30473cb
Revert "simplify disable() since there are no members"
simonLeary42 Jan 22, 2026
0470612
admins can nuke a group
simonLeary42 Jan 22, 2026
88d3b60
Revert "remove test"
simonLeary42 Jan 22, 2026
544c5a1
update test
simonLeary42 Jan 22, 2026
553f122
add test
simonLeary42 Jan 22, 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
2 changes: 2 additions & 0 deletions LDAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Terminology:
- **native user**: a user created by this account portal
- **non-native user**: inverse of native
- users created for administrative purposes should not be mixed with native users in the LDAP OUs given in `config.ini` or else this account portal may get confused
- **disabled group**: a PI group that was disabled by its owner or had its owner disabled
- memberuid attribute should be empty
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ rm "$prod" && ln -s "$old" "$prod"
- the `home` page can be copied over to `deployment/templates_overrides/home.php`
- the `support` page should be moved over to wherever you host your documentation
- the `notices` SQL table should be droppped
- a new LDAP schema needs to be added:
```shell
scp tools/docker-dev/identity/unity-cluster-schema.ldif root@your-ldap-server:/root/unity-cluster-schema.ldif
ssh root@your-ldap-server ldapadd -Y EXTERNAL -H ldapi:/// -f /root/unity-cluster-schema.ldif
```

### 1.5 -> 1.6

Expand Down
142 changes: 99 additions & 43 deletions resources/lib/UnityGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function __toString(): string
public function requestGroup(?bool $send_mail_to_admins = null, bool $send_mail = true): void
{
$send_mail_to_admins ??= CONFIG["mail"]["send_pimesg_to_admins"];
if ($this->exists()) {
if ($this->exists() && !$this->getIsDisabled()) {
return;
}
if ($this->SQL->accDeletionRequestExists($this->getOwner()->uid)) {
Expand All @@ -63,18 +63,74 @@ public function requestGroup(?bool $send_mail_to_admins = null, bool $send_mail
}
}

/**
* email all members that group is now disabled, remove all members, log, set attribute
* the owner must manually remove all members first, but admins don't have this requirement
*/
public function disable(bool $send_mail = true): void
{
if ($this->getIsDisabled()) {
throw new Exception("cannot disable an already disabled group");
}
$this->SQL->addLog("disable_pi_group", $this->gid);
$memberuids = $this->getMemberUIDs();
if ($send_mail) {
$member_attributes = $this->LDAP->getUsersAttributes($memberuids, ["mail"]);
$member_mails = array_map(fn($x) => (string) $x["mail"][0], $member_attributes);
if (count($member_mails) > 0) {
$this->MAILER->sendMail($member_mails, "group_disabled", [
"group_name" => $this->gid,
]);
}
}
$this->setIsDisabled(true);
if (count($memberuids) > 0) {
$this->entry->setAttribute("memberuid", []);
}
// TODO optimize
// UnityUser::__construct() makes one LDAP query for each user
// updateIsQualified() makes one LDAP query for each member
// if user is no longer in any PI group, disqualify them
foreach ($memberuids as $uid) {
$user = new UnityUser($uid, $this->LDAP, $this->SQL, $this->MAILER, $this->WEBHOOK);
$user->updateIsQualified($send_mail);
}
}

private function reenable(bool $send_mail = true): void
{
if (!$this->getIsDisabled()) {
throw new Exception("cannot re-enable a group that is not disabled");
}
$this->SQL->addLog("reenabled_pi_group", $this->gid);
if ($send_mail) {
$this->MAILER->sendMail($this->getOwner()->getMail(), "group_reenabled", [
"group_name" => $this->gid,
]);
}
$this->setIsDisabled(false);
$owner_uid = $this->getOwner()->uid;
if (!$this->memberUIDExists($owner_uid)) {
$this->addMemberUID($owner_uid);
}
$this->getOwner()->updateIsQualified($send_mail);
}

/**
* This method will create the group (this is what is executed when an admin approved the group)
*/
public function approveGroup(bool $send_mail = true): void
{
$uid = $this->getOwner()->uid;
$request = $this->SQL->getRequest($uid, UnitySQL::REQUEST_BECOME_PI);
if ($this->exists()) {
return;
}
\ensure($this->getOwner()->exists());
$this->init();
if (!$this->entry->exists()) {
$this->init();
} elseif ($this->getIsDisabled()) {
$this->reenable();
} else {
throw new Exception("cannot approve group that already exists and is not disabled");
}
$this->SQL->removeRequest($this->getOwner()->uid, UnitySQL::REQUEST_BECOME_PI);
$this->SQL->addLog("approved_group", $this->getOwner()->uid);
if ($send_mail) {
Expand Down Expand Up @@ -126,42 +182,6 @@ public function cancelGroupJoinRequest(UnityUser $user, bool $send_mail = true):
}
}

// /**
// * This method will delete the group, either by admin action or PI action
// */
// public function removeGroup($send_mail = true)
// {
// // remove any pending requests
// // this will silently fail if the request doesn't exist (which is what we want)
// $this->SQL->removeRequests($this->gid);

// // we don't need to do anything extra if the group is already deleted
// if (!$this->exists()) {
// return;
// }

// // first, we must record the users in the group currently
// $users = $this->getGroupMembers();

// // now we delete the ldap entry
// $this->entry->ensureExists();
// $this->entry->delete();

// // Logs the change
// $this->SQL->addLog("removed_group", $this->gid);

// // send email to every user of the now deleted PI group
// if ($send_mail) {
// foreach ($users as $user) {
// $this->MAILER->sendMail(
// $user->getMail(),
// "group_disband",
// array("group_name" => $this->gid)
// );
// }
// }
// }

/**
* This method is executed when a user is approved to join the group
* (either by admin or the group owner)
Expand Down Expand Up @@ -220,7 +240,7 @@ public function removeUser(UnityUser $new_user, bool $send_mail = true): void
return;
}
if ($new_user->uid == $this->getOwner()->uid) {
throw new Exception("Cannot delete group owner from group. Disband group instead");
throw new Exception("Cannot delete group owner from group. Disable group instead");
}
$this->removeMemberUID($new_user->uid);
$this->SQL->addLog(
Expand Down Expand Up @@ -326,7 +346,7 @@ private function init(): void
\ensure(!$this->entry->exists());
$nextGID = $this->LDAP->getNextPIGIDNumber();
$this->entry->create([
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
"objectclass" => ["unityClusterPIGroup", "posixGroup", "top"],
"gidnumber" => strval($nextGID),
"memberuid" => [$owner->uid],
]);
Expand Down Expand Up @@ -376,4 +396,40 @@ public function getGroupMembersAttributes(array $attributes, array $default_valu
$default_values,
);
}

public function getIsDisabled(): bool
{
$value = $this->entry->getAttribute("isDisabled");
switch (count($value)) {
case 0:
return false;
case 1:
switch ($value[0]) {
case "TRUE":
return true;
case "FALSE":
return false;
default:
throw new \RuntimeException(
sprintf(
"unexpected value for isDisabled: '%s'. expected 'TRUE' or 'FALSE'",
$value[0],
),
);
}
default:
throw new \RuntimeException(
sprintf(
"expected value of length 0 or 1, found value %s of length %s",
_json_encode($value),
count($value),
),
);
}
}

private function setIsDisabled(bool $new_value): void
{
$this->entry->setAttribute("isDisabled", $new_value ? "TRUE" : "FALSE");
}
}
30 changes: 19 additions & 11 deletions resources/lib/UnityLDAP.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class UnityLDAP extends LDAPConn
"ldapPublicKey",
];

public const array POSIX_GROUP_CLASS = ["posixGroup", "top"];
// isDisabled unset or set to "FALSE"
private static string $NON_DISABLED_FILTER = "(|(!(isDisabled=*))(isDisabled=FALSE))";

private string $custom_mappings_path =
__DIR__ . "/../../" . CONFIG["ldap"]["custom_user_mappings_dir"];
Expand Down Expand Up @@ -198,7 +199,7 @@ public function getAllNativeUsersAttributes(
}

/** @return UnityGroup[] */
public function getAllPIGroups(
public function getAllNonDisabledPIGroups(
UnitySQL $UnitySQL,
UnityMailer $UnityMailer,
UnityWebhook $UnityWebhook,
Expand All @@ -207,6 +208,7 @@ public function getAllPIGroups(
$pi_groups_attributes = $this->pi_groupOU->getChildrenArrayStrict(
attributes: ["cn"],
recursive: false,
filter: self::$NON_DISABLED_FILTER,
);
foreach ($pi_groups_attributes as $attributes) {
array_push(
Expand All @@ -222,35 +224,41 @@ public function getAllPIGroups(
* @param attributes $default_values
* @return attributes[]
*/
public function getAllPIGroupsAttributes(array $attributes, array $default_values = []): array
{
public function getAllNonDisabledPIGroupsAttributes(
array $attributes,
array $default_values = [],
): array {
return $this->pi_groupOU->getChildrenArrayStrict(
$attributes,
false, // non-recursive
"objectClass=posixGroup",
self::$NON_DISABLED_FILTER,
$default_values,
);
}

/** @return string[] */
public function getPIGroupGIDsWithMemberUID(string $uid): array
public function getNonDisabledPIGroupGIDsWithMemberUID(string $uid): array
{
return array_map(
fn($x) => $x["cn"][0],
$this->pi_groupOU->getChildrenArrayStrict(
["cn"],
false,
"(memberuid=" . ldap_escape($uid, flags: LDAP_ESCAPE_FILTER) . ")",
sprintf(
"(&(memberuid=%s)%s)",
ldap_escape($uid, flags: LDAP_ESCAPE_FILTER),
self::$NON_DISABLED_FILTER,
),
),
);
}

/** @return string[] */
public function getAllPIGroupOwnerUIDs(): array
public function getAllNonDisabledPIGroupOwnerUIDs(): array
{
return array_map(
fn($x) => UnityGroup::GID2OwnerUID($x["cn"][0]),
$this->pi_groupOU->getChildrenArrayStrict(["cn"]),
fn($x) => UnityGroup::GID2OwnerUID((string) $x["cn"][0]),
$this->getAllNonDisabledPIGroupsAttributes(["cn"]),
);
}

Expand Down Expand Up @@ -280,7 +288,7 @@ public function getUID2PIGIDs(): array
{
$uid2pigids = [];
// for each PI group, append that GID to the member list for each of its member UIDs
$pi_groups_attributes = $this->getAllPIGroupsAttributes(
$pi_groups_attributes = $this->getAllNonDisabledPIGroupsAttributes(
["cn", "memberuid"],
default_values: ["memberuid" => []],
);
Expand Down
2 changes: 1 addition & 1 deletion resources/lib/UnityOrg.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function init(): void
\ensure(!$this->entry->exists());
$nextGID = $this->LDAP->getNextOrgGIDNumber();
$this->entry->create([
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
"objectclass" => ["posixGroup", "top"],
"gidnumber" => strval($nextGID),
]);
}
Expand Down
6 changes: 3 additions & 3 deletions resources/lib/UnityUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function init(
$id = $this->LDAP->getNextUIDGIDNumber($this->uid);
\ensure(!$ldapGroupEntry->exists());
$ldapGroupEntry->create([
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
"objectclass" => ["posixGroup", "top"],
"gidnumber" => strval($id),
]);
\ensure(!$this->entry->exists());
Expand Down Expand Up @@ -346,7 +346,7 @@ public function getHomeDir(): string
*/
public function isPI(): bool
{
return $this->getPIGroup()->exists();
return $this->getPIGroup()->exists() && !$this->getPIGroup()->getIsDisabled();
}

public function getPIGroup(): UnityGroup
Expand All @@ -371,7 +371,7 @@ public function getOrgGroup(): UnityOrg
*/
public function getPIGroupGIDs(): array
{
return $this->LDAP->getPIGroupGIDsWithMemberUID($this->uid);
return $this->LDAP->getNonDisabledPIGroupGIDsWithMemberUID($this->uid);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php

$this->Subject = "PI Group Disbanded"; ?>
$this->Subject = "PI Group Disabled"; ?>

<p>Hello,</p>

<p>Your PI group, <?php echo $data["group_name"]; ?>, has been disbanded on the UnityHPC Platform.
<p>Your PI group, <?php echo $data["group_name"]; ?>, has been disabled on the UnityHPC Platform.
Any jobs associated with this PI account have been killed.</p>

<p>If you believe this to be a mistake, please reply to this email</p>
11 changes: 11 additions & 0 deletions resources/mail/group_reenabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$this->Subject = "PI Group Re-Enabled"; ?>

<p>Hello,</p>

<p>
Your PI group, <?php echo $data["group_name"]; ?>, has been re-enabled on the UnityHPC Platform.
</p>

<p>If you believe this to be a mistake, please reply to this email.</p>
23 changes: 23 additions & 0 deletions test/functional/PIBecomeApproveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,27 @@ public function testApprovePI()
$this->assertFalse($USER->getFlag(UserFlag::QUALIFIED));
}
}

public function testReenableGroup()
{
global $USER, $SSO, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("ReenabledOwnerOfDisabledPIGroup");
$this->assertFalse($USER->isPI());
$user = $USER;
$pi_group = $USER->getPIGroup();
$approve_uid = $USER->uid;
try {
$this->requestGroupCreation();
$this->assertRequestedPIGroup(true);
$this->switchUser("Admin");
$this->approveGroup($approve_uid);
$this->assertTrue($user->isPI());
} finally {
if ($pi_group->memberUIDExists($approve_uid)) {
$pi_group->removeMemberUID($approve_uid);
callPrivateMethod($pi_group, "setIsDisabled", true);
assert(!$user->isPI());
}
}
}
}
Loading