Skip to content

Commit 975b1ff

Browse files
disband, reinstate PI groups (#560)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 84055fd commit 975b1ff

20 files changed

+702
-70
lines changed

LDAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ Terminology:
55
- **native user**: a user created by this account portal
66
- **non-native user**: inverse of native
77
- 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
8+
- **disabled group**: a PI group that was disabled by its owner or had its owner disabled
9+
- memberuid attribute should be empty

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ rm "$prod" && ln -s "$old" "$prod"
137137
- the `home` page can be copied over to `deployment/templates_overrides/home.php`
138138
- the `support` page should be moved over to wherever you host your documentation
139139
- the `notices` SQL table should be droppped
140+
- a new LDAP schema needs to be added:
141+
```shell
142+
scp tools/docker-dev/identity/unity-cluster-schema.ldif root@your-ldap-server:/root/unity-cluster-schema.ldif
143+
ssh root@your-ldap-server ldapadd -Y EXTERNAL -H ldapi:/// -f /root/unity-cluster-schema.ldif
144+
```
140145

141146
### 1.5 -> 1.6
142147

resources/lib/UnityGroup.php

Lines changed: 99 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function __toString(): string
4040
public function requestGroup(?bool $send_mail_to_admins = null, bool $send_mail = true): void
4141
{
4242
$send_mail_to_admins ??= CONFIG["mail"]["send_pimesg_to_admins"];
43-
if ($this->exists()) {
43+
if ($this->exists() && !$this->getIsDisabled()) {
4444
return;
4545
}
4646
if ($this->SQL->accDeletionRequestExists($this->getOwner()->uid)) {
@@ -63,18 +63,74 @@ public function requestGroup(?bool $send_mail_to_admins = null, bool $send_mail
6363
}
6464
}
6565

66+
/**
67+
* email all members that group is now disabled, remove all members, log, set attribute
68+
* the owner must manually remove all members first, but admins don't have this requirement
69+
*/
70+
public function disable(bool $send_mail = true): void
71+
{
72+
if ($this->getIsDisabled()) {
73+
throw new Exception("cannot disable an already disabled group");
74+
}
75+
$this->SQL->addLog("disable_pi_group", $this->gid);
76+
$memberuids = $this->getMemberUIDs();
77+
if ($send_mail) {
78+
$member_attributes = $this->LDAP->getUsersAttributes($memberuids, ["mail"]);
79+
$member_mails = array_map(fn($x) => (string) $x["mail"][0], $member_attributes);
80+
if (count($member_mails) > 0) {
81+
$this->MAILER->sendMail($member_mails, "group_disabled", [
82+
"group_name" => $this->gid,
83+
]);
84+
}
85+
}
86+
$this->setIsDisabled(true);
87+
if (count($memberuids) > 0) {
88+
$this->entry->setAttribute("memberuid", []);
89+
}
90+
// TODO optimize
91+
// UnityUser::__construct() makes one LDAP query for each user
92+
// updateIsQualified() makes one LDAP query for each member
93+
// if user is no longer in any PI group, disqualify them
94+
foreach ($memberuids as $uid) {
95+
$user = new UnityUser($uid, $this->LDAP, $this->SQL, $this->MAILER, $this->WEBHOOK);
96+
$user->updateIsQualified($send_mail);
97+
}
98+
}
99+
100+
private function reenable(bool $send_mail = true): void
101+
{
102+
if (!$this->getIsDisabled()) {
103+
throw new Exception("cannot re-enable a group that is not disabled");
104+
}
105+
$this->SQL->addLog("reenabled_pi_group", $this->gid);
106+
if ($send_mail) {
107+
$this->MAILER->sendMail($this->getOwner()->getMail(), "group_reenabled", [
108+
"group_name" => $this->gid,
109+
]);
110+
}
111+
$this->setIsDisabled(false);
112+
$owner_uid = $this->getOwner()->uid;
113+
if (!$this->memberUIDExists($owner_uid)) {
114+
$this->addMemberUID($owner_uid);
115+
}
116+
$this->getOwner()->updateIsQualified($send_mail);
117+
}
118+
66119
/**
67120
* This method will create the group (this is what is executed when an admin approved the group)
68121
*/
69122
public function approveGroup(bool $send_mail = true): void
70123
{
71124
$uid = $this->getOwner()->uid;
72125
$request = $this->SQL->getRequest($uid, UnitySQL::REQUEST_BECOME_PI);
73-
if ($this->exists()) {
74-
return;
75-
}
76126
\ensure($this->getOwner()->exists());
77-
$this->init();
127+
if (!$this->entry->exists()) {
128+
$this->init();
129+
} elseif ($this->getIsDisabled()) {
130+
$this->reenable();
131+
} else {
132+
throw new Exception("cannot approve group that already exists and is not disabled");
133+
}
78134
$this->SQL->removeRequest($this->getOwner()->uid, UnitySQL::REQUEST_BECOME_PI);
79135
$this->SQL->addLog("approved_group", $this->getOwner()->uid);
80136
if ($send_mail) {
@@ -126,42 +182,6 @@ public function cancelGroupJoinRequest(UnityUser $user, bool $send_mail = true):
126182
}
127183
}
128184

129-
// /**
130-
// * This method will delete the group, either by admin action or PI action
131-
// */
132-
// public function removeGroup($send_mail = true)
133-
// {
134-
// // remove any pending requests
135-
// // this will silently fail if the request doesn't exist (which is what we want)
136-
// $this->SQL->removeRequests($this->gid);
137-
138-
// // we don't need to do anything extra if the group is already deleted
139-
// if (!$this->exists()) {
140-
// return;
141-
// }
142-
143-
// // first, we must record the users in the group currently
144-
// $users = $this->getGroupMembers();
145-
146-
// // now we delete the ldap entry
147-
// $this->entry->ensureExists();
148-
// $this->entry->delete();
149-
150-
// // Logs the change
151-
// $this->SQL->addLog("removed_group", $this->gid);
152-
153-
// // send email to every user of the now deleted PI group
154-
// if ($send_mail) {
155-
// foreach ($users as $user) {
156-
// $this->MAILER->sendMail(
157-
// $user->getMail(),
158-
// "group_disband",
159-
// array("group_name" => $this->gid)
160-
// );
161-
// }
162-
// }
163-
// }
164-
165185
/**
166186
* This method is executed when a user is approved to join the group
167187
* (either by admin or the group owner)
@@ -220,7 +240,7 @@ public function removeUser(UnityUser $new_user, bool $send_mail = true): void
220240
return;
221241
}
222242
if ($new_user->uid == $this->getOwner()->uid) {
223-
throw new Exception("Cannot delete group owner from group. Disband group instead");
243+
throw new Exception("Cannot delete group owner from group. Disable group instead");
224244
}
225245
$this->removeMemberUID($new_user->uid);
226246
$this->SQL->addLog(
@@ -326,7 +346,7 @@ private function init(): void
326346
\ensure(!$this->entry->exists());
327347
$nextGID = $this->LDAP->getNextPIGIDNumber();
328348
$this->entry->create([
329-
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
349+
"objectclass" => ["unityClusterPIGroup", "posixGroup", "top"],
330350
"gidnumber" => strval($nextGID),
331351
"memberuid" => [$owner->uid],
332352
]);
@@ -376,4 +396,40 @@ public function getGroupMembersAttributes(array $attributes, array $default_valu
376396
$default_values,
377397
);
378398
}
399+
400+
public function getIsDisabled(): bool
401+
{
402+
$value = $this->entry->getAttribute("isDisabled");
403+
switch (count($value)) {
404+
case 0:
405+
return false;
406+
case 1:
407+
switch ($value[0]) {
408+
case "TRUE":
409+
return true;
410+
case "FALSE":
411+
return false;
412+
default:
413+
throw new \RuntimeException(
414+
sprintf(
415+
"unexpected value for isDisabled: '%s'. expected 'TRUE' or 'FALSE'",
416+
$value[0],
417+
),
418+
);
419+
}
420+
default:
421+
throw new \RuntimeException(
422+
sprintf(
423+
"expected value of length 0 or 1, found value %s of length %s",
424+
_json_encode($value),
425+
count($value),
426+
),
427+
);
428+
}
429+
}
430+
431+
private function setIsDisabled(bool $new_value): void
432+
{
433+
$this->entry->setAttribute("isDisabled", $new_value ? "TRUE" : "FALSE");
434+
}
379435
}

resources/lib/UnityLDAP.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class UnityLDAP extends LDAPConn
3232
"ldapPublicKey",
3333
];
3434

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

3738
private string $custom_mappings_path =
3839
__DIR__ . "/../../" . CONFIG["ldap"]["custom_user_mappings_dir"];
@@ -198,7 +199,7 @@ public function getAllNativeUsersAttributes(
198199
}
199200

200201
/** @return UnityGroup[] */
201-
public function getAllPIGroups(
202+
public function getAllNonDisabledPIGroups(
202203
UnitySQL $UnitySQL,
203204
UnityMailer $UnityMailer,
204205
UnityWebhook $UnityWebhook,
@@ -207,6 +208,7 @@ public function getAllPIGroups(
207208
$pi_groups_attributes = $this->pi_groupOU->getChildrenArrayStrict(
208209
attributes: ["cn"],
209210
recursive: false,
211+
filter: self::$NON_DISABLED_FILTER,
210212
);
211213
foreach ($pi_groups_attributes as $attributes) {
212214
array_push(
@@ -222,35 +224,41 @@ public function getAllPIGroups(
222224
* @param attributes $default_values
223225
* @return attributes[]
224226
*/
225-
public function getAllPIGroupsAttributes(array $attributes, array $default_values = []): array
226-
{
227+
public function getAllNonDisabledPIGroupsAttributes(
228+
array $attributes,
229+
array $default_values = [],
230+
): array {
227231
return $this->pi_groupOU->getChildrenArrayStrict(
228232
$attributes,
229233
false, // non-recursive
230-
"objectClass=posixGroup",
234+
self::$NON_DISABLED_FILTER,
231235
$default_values,
232236
);
233237
}
234238

235239
/** @return string[] */
236-
public function getPIGroupGIDsWithMemberUID(string $uid): array
240+
public function getNonDisabledPIGroupGIDsWithMemberUID(string $uid): array
237241
{
238242
return array_map(
239243
fn($x) => $x["cn"][0],
240244
$this->pi_groupOU->getChildrenArrayStrict(
241245
["cn"],
242246
false,
243-
"(memberuid=" . ldap_escape($uid, flags: LDAP_ESCAPE_FILTER) . ")",
247+
sprintf(
248+
"(&(memberuid=%s)%s)",
249+
ldap_escape($uid, flags: LDAP_ESCAPE_FILTER),
250+
self::$NON_DISABLED_FILTER,
251+
),
244252
),
245253
);
246254
}
247255

248256
/** @return string[] */
249-
public function getAllPIGroupOwnerUIDs(): array
257+
public function getAllNonDisabledPIGroupOwnerUIDs(): array
250258
{
251259
return array_map(
252-
fn($x) => UnityGroup::GID2OwnerUID($x["cn"][0]),
253-
$this->pi_groupOU->getChildrenArrayStrict(["cn"]),
260+
fn($x) => UnityGroup::GID2OwnerUID((string) $x["cn"][0]),
261+
$this->getAllNonDisabledPIGroupsAttributes(["cn"]),
254262
);
255263
}
256264

@@ -280,7 +288,7 @@ public function getUID2PIGIDs(): array
280288
{
281289
$uid2pigids = [];
282290
// for each PI group, append that GID to the member list for each of its member UIDs
283-
$pi_groups_attributes = $this->getAllPIGroupsAttributes(
291+
$pi_groups_attributes = $this->getAllNonDisabledPIGroupsAttributes(
284292
["cn", "memberuid"],
285293
default_values: ["memberuid" => []],
286294
);

resources/lib/UnityOrg.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function init(): void
2424
\ensure(!$this->entry->exists());
2525
$nextGID = $this->LDAP->getNextOrgGIDNumber();
2626
$this->entry->create([
27-
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
27+
"objectclass" => ["posixGroup", "top"],
2828
"gidnumber" => strval($nextGID),
2929
]);
3030
}

resources/lib/UnityUser.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function init(
6060
$id = $this->LDAP->getNextUIDGIDNumber($this->uid);
6161
\ensure(!$ldapGroupEntry->exists());
6262
$ldapGroupEntry->create([
63-
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
63+
"objectclass" => ["posixGroup", "top"],
6464
"gidnumber" => strval($id),
6565
]);
6666
\ensure(!$this->entry->exists());
@@ -346,7 +346,7 @@ public function getHomeDir(): string
346346
*/
347347
public function isPI(): bool
348348
{
349-
return $this->getPIGroup()->exists();
349+
return $this->getPIGroup()->exists() && !$this->getPIGroup()->getIsDisabled();
350350
}
351351

352352
public function getPIGroup(): UnityGroup
@@ -371,7 +371,7 @@ public function getOrgGroup(): UnityOrg
371371
*/
372372
public function getPIGroupGIDs(): array
373373
{
374-
return $this->LDAP->getPIGroupGIDsWithMemberUID($this->uid);
374+
return $this->LDAP->getNonDisabledPIGroupGIDsWithMemberUID($this->uid);
375375
}
376376

377377
/**
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<?php
22

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

55
<p>Hello,</p>
66

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

1010
<p>If you believe this to be a mistake, please reply to this email</p>

resources/mail/group_reenabled.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
$this->Subject = "PI Group Re-Enabled"; ?>
4+
5+
<p>Hello,</p>
6+
7+
<p>
8+
Your PI group, <?php echo $data["group_name"]; ?>, has been re-enabled on the UnityHPC Platform.
9+
</p>
10+
11+
<p>If you believe this to be a mistake, please reply to this email.</p>

test/functional/PIBecomeApproveTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,27 @@ public function testApprovePI()
7575
$this->assertFalse($USER->getFlag(UserFlag::QUALIFIED));
7676
}
7777
}
78+
79+
public function testReenableGroup()
80+
{
81+
global $USER, $SSO, $LDAP, $SQL, $MAILER, $WEBHOOK;
82+
$this->switchUser("ReenabledOwnerOfDisabledPIGroup");
83+
$this->assertFalse($USER->isPI());
84+
$user = $USER;
85+
$pi_group = $USER->getPIGroup();
86+
$approve_uid = $USER->uid;
87+
try {
88+
$this->requestGroupCreation();
89+
$this->assertRequestedPIGroup(true);
90+
$this->switchUser("Admin");
91+
$this->approveGroup($approve_uid);
92+
$this->assertTrue($user->isPI());
93+
} finally {
94+
if ($pi_group->memberUIDExists($approve_uid)) {
95+
$pi_group->removeMemberUID($approve_uid);
96+
callPrivateMethod($pi_group, "setIsDisabled", true);
97+
assert(!$user->isPI());
98+
}
99+
}
100+
}
78101
}

0 commit comments

Comments
 (0)