Skip to content

Commit 5bbab62

Browse files
committed
disband, reinstate PI groups
1 parent e49dbcc commit 5bbab62

20 files changed

+676
-74
lines changed

LDAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ Terminology:
33
- **qualified user**: a user who is currently a PI or a member of at least one PI group
44
- **native user**: a user who's entries exist in the OUs given in `config.ini`
55
- it is up to the administrator to ensure that no non-native entries exist in these OUs
6+
- **disabled group**: a PI group that was disabled by its owner or had its owner disabled
7+
- memberuid attribute should be empty

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ rm "$prod" && ln -s "$old" "$prod"
123123

124124
### Version-specific update instructions:
125125

126+
### 1.6 -> 1.7
127+
128+
- a new LDAP schema needs to be added:
129+
```shell
130+
scp tools/docker-dev/identity/unity-cluster-schema.ldif root@your-ldap-server:/root/unity-cluster-schema.ldif
131+
ssh root@your-ldap-server ldapadd -Y EXTERNAL -H ldapi:/// -f /root/unity-cluster-schema.ldif
132+
```
133+
126134
### 1.5 -> 1.6
127135

128136
- the `[site]getting_started_url` option should be defined

resources/lib/UnityGroup.php

Lines changed: 89 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,64 @@ public function requestGroup(?bool $send_mail_to_admins = null, bool $send_mail
6363
}
6464
}
6565

66+
public function disable(bool $send_mail = true): void
67+
{
68+
$this->SQL->addLog("disable_pi_group", $this->gid);
69+
$memberuids = $this->getMemberUIDs();
70+
if ($send_mail) {
71+
$member_attributes = $this->LDAP->getUsersAttributes($memberuids, ["mail"]);
72+
$member_mails = array_map(fn($x) => $x["mail"][0], $member_attributes);
73+
$this->MAILER->sendMail($member_mails, "group_disabled", [
74+
"group_name" => $this->gid,
75+
]);
76+
}
77+
$this->setIsDisabled(true);
78+
if (count($memberuids) > 0) {
79+
$this->entry->setAttribute("memberuid", []);
80+
}
81+
// TODO optimmize
82+
// UnityUser::__construct() makes one LDAP query for each user
83+
// updateIsQualified() makes one LDAP query for each member
84+
// if user is no longer in any PI group, disqualify them
85+
// FIXME uncomment
86+
// foreach ($memberuids as $uid) {
87+
// $user = new UnityUser($uid, $this->LDAP, $this->SQL, $this->MAILER, $this->WEBHOOK);
88+
// $user->updateIsQualified($send_mail);
89+
// }
90+
}
91+
92+
private function reenable(bool $send_mail = true)
93+
{
94+
$this->SQL->addLog("reenabled_pi_group", $this->gid);
95+
if ($send_mail) {
96+
$this->MAILER->sendMail($this->getOwner()->getMail(), "group_reenabled", [
97+
"group_name" => $this->gid,
98+
]);
99+
}
100+
$this->setIsDisabled(false);
101+
$owner_uid = $this->getOwner()->uid;
102+
if (!$this->memberUIDExists($owner_uid)) {
103+
$this->addMemberUID($owner_uid);
104+
}
105+
// FIXME uncomment
106+
// $this->getOwner()->updateIsQualified($send_mail);
107+
}
108+
66109
/**
67110
* This method will create the group (this is what is executed when an admin approved the group)
68111
*/
69112
public function approveGroup(bool $send_mail = true): void
70113
{
71114
$uid = $this->getOwner()->uid;
72115
$request = $this->SQL->getRequest($uid, UnitySQL::REQUEST_BECOME_PI);
73-
if ($this->exists()) {
74-
return;
75-
}
76116
\ensure($this->getOwner()->exists());
77-
$this->init();
117+
if (!$this->entry->exists()) {
118+
$this->init();
119+
} elseif ($this->getIsDisabled()) {
120+
$this->reenable();
121+
} else {
122+
throw new Exception("cannot approve group that already exists and is not disabled");
123+
}
78124
$this->SQL->removeRequest($this->getOwner()->uid, UnitySQL::REQUEST_BECOME_PI);
79125
$this->SQL->addLog("approved_group", $this->getOwner()->uid);
80126
if ($send_mail) {
@@ -126,42 +172,6 @@ public function cancelGroupJoinRequest(UnityUser $user, bool $send_mail = true):
126172
}
127173
}
128174

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-
165175
/**
166176
* This method is executed when a user is approved to join the group
167177
* (either by admin or the group owner)
@@ -226,7 +236,7 @@ public function removeUser(UnityUser $new_user, bool $send_mail = true): void
226236
return;
227237
}
228238
if ($new_user->uid == $this->getOwner()->uid) {
229-
throw new Exception("Cannot delete group owner from group. Disband group instead");
239+
throw new Exception("Cannot delete group owner from group. Disable group instead");
230240
}
231241
// remove request, this will fail silently if the request doesn't exist
232242
$this->removeMemberUID($new_user->uid);
@@ -329,7 +339,7 @@ private function init(): void
329339
\ensure(!$this->entry->exists());
330340
$nextGID = $this->LDAP->getNextPIGIDNumber();
331341
$this->entry->create([
332-
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
342+
"objectclass" => ["unityClusterPIGroup", "posixGroup", "top"],
333343
"gidnumber" => strval($nextGID),
334344
"memberuid" => [$owner->uid],
335345
]);
@@ -385,4 +395,40 @@ public function getGroupMembersAttributes(array $attributes, array $default_valu
385395
$default_values,
386396
);
387397
}
398+
399+
public function getIsDisabled(): bool
400+
{
401+
$value = $this->entry->getAttribute("isDisabled");
402+
switch (count($value)) {
403+
case 0:
404+
return false;
405+
case 1:
406+
switch ($value[0]) {
407+
case "TRUE":
408+
return true;
409+
case "FALSE":
410+
return false;
411+
default:
412+
throw new \RuntimeException(
413+
sprintf(
414+
"unexpected value for isDisabled: '%s'. expected 'TRUE' or 'FALSE'",
415+
$value[0],
416+
),
417+
);
418+
}
419+
default:
420+
throw new \RuntimeException(
421+
sprintf(
422+
"expected value of length 0 or 1, found value %s of length %s",
423+
jsonEncode($value),
424+
count($value),
425+
),
426+
);
427+
}
428+
}
429+
430+
public function setIsDisabled(bool $new_value): void
431+
{
432+
$this->entry->setAttribute("isDisabled", $new_value ? "TRUE" : "FALSE");
433+
}
388434
}

resources/lib/UnityLDAP.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class UnityLDAP extends LDAPConn
3030
"ldapPublicKey",
3131
];
3232

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

3536
private string $custom_mappings_path =
3637
__DIR__ . "/../../" . CONFIG["ldap"]["custom_user_mappings_dir"];
@@ -182,7 +183,7 @@ public function getAllNativeUsersAttributes(
182183
);
183184
}
184185

185-
public function getAllPIGroups(
186+
public function getAllNonDisabledPIGroups(
186187
UnitySQL $UnitySQL,
187188
UnityMailer $UnityMailer,
188189
UnityWebhook $UnityWebhook,
@@ -191,6 +192,7 @@ public function getAllPIGroups(
191192
$pi_groups_attributes = $this->pi_groupOU->getChildrenArrayStrict(
192193
attributes: ["cn"],
193194
recursive: false,
195+
filter: self::$NON_DEFUNCT_FILTER,
194196
);
195197
foreach ($pi_groups_attributes as $attributes) {
196198
array_push(
@@ -201,33 +203,39 @@ public function getAllPIGroups(
201203
return $out;
202204
}
203205

204-
public function getAllPIGroupsAttributes(array $attributes, array $default_values = []): array
205-
{
206+
public function getAllNonDisabledPIGroupsAttributes(
207+
array $attributes,
208+
array $default_values = [],
209+
): array {
206210
return $this->pi_groupOU->getChildrenArrayStrict(
207211
$attributes,
208212
false, // non-recursive
209-
"objectClass=posixGroup",
213+
self::$NON_DEFUNCT_FILTER,
210214
$default_values,
211215
);
212216
}
213217

214-
public function getPIGroupGIDsWithMemberUID(string $uid): array
218+
public function getNonDisabledPIGroupGIDsWithMemberUID(string $uid): array
215219
{
216220
return array_map(
217221
fn($x) => $x["cn"][0],
218222
$this->pi_groupOU->getChildrenArrayStrict(
219223
["cn"],
220224
false,
221-
"(memberuid=" . ldap_escape($uid, "", LDAP_ESCAPE_FILTER) . ")",
225+
sprintf(
226+
"(&(memberuid=%s)%s)",
227+
ldap_escape($uid, "", LDAP_ESCAPE_FILTER),
228+
self::$NON_DEFUNCT_FILTER,
229+
),
222230
),
223231
);
224232
}
225233

226-
public function getAllPIGroupOwnerUIDs(): array
234+
public function getAllNonDisabledPIGroupOwnerUIDs(): array
227235
{
228236
return array_map(
229237
fn($x) => UnityGroup::GID2OwnerUID($x["cn"][0]),
230-
$this->pi_groupOU->getChildrenArrayStrict(["cn"]),
238+
$this->getAllNonDisabledPIGroupsAttributes(["cn"]),
231239
);
232240
}
233241

@@ -238,7 +246,7 @@ public function getUID2PIGIDs(): array
238246
{
239247
$uid2pigids = [];
240248
// for each PI group, append that GID to the member list for each of its member UIDs
241-
$pi_groups_attributes = $this->getAllPIGroupsAttributes(
249+
$pi_groups_attributes = $this->getAllNonDisabledPIGroupsAttributes(
242250
["cn", "memberuid"],
243251
default_values: ["memberuid" => []],
244252
);

resources/lib/UnityOrg.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function init(): void
3737
\ensure(!$this->entry->exists());
3838
$nextGID = $this->LDAP->getNextOrgGIDNumber();
3939
$this->entry->create([
40-
"objectclass" => UnityLDAP::POSIX_GROUP_CLASS,
40+
"objectclass" => ["posixGroup", "top"],
4141
"gidnumber" => strval($nextGID),
4242
]);
4343
}

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());
@@ -344,7 +344,7 @@ public function getHomeDir(): string
344344
*/
345345
public function isPI(): bool
346346
{
347-
return $this->getPIGroup()->exists();
347+
return $this->getPIGroup()->exists() && !$this->getPIGroup()->getIsDisabled();
348348
}
349349

350350
public function getPIGroup(): UnityGroup
@@ -374,7 +374,7 @@ public function getOrgGroup(): UnityOrg
374374
*/
375375
public function getPIGroupGIDs(): array
376376
{
377-
return $this->LDAP->getPIGroupGIDsWithMemberUID($this->uid);
377+
return $this->LDAP->getNonDisabledPIGroupGIDsWithMemberUID($this->uid);
378378
}
379379

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

3-
// This template is sent to all users of a group when that group is disbanded (deleted)
4-
$this->Subject = "PI Group Disbanded"; ?>
3+
// This template is sent to all users of a group when that group is disabled
4+
$this->Subject = "PI Group Disabled"; ?>
55

66
<p>Hello,</p>
77

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

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

resources/mail/group_reenabled.php

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

resources/mail/user_flag_removed.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?php use UnityWebPortal\lib\UserFlag; ?>
22
<?php switch ($data["flag"]):
33
case UserFlag::QUALIFIED: ?>
4-
<?php $this->Subject = "User Deactivated"; ?>
4+
<?php $this->Subject = "User Disabled"; ?>
55
<p>Hello,</p>
6-
<p>Your account on the UnityHPC Platform has been deactivated.</p>
6+
<p>Your account on the UnityHPC Platform has been disabled.</p>
77
<p>If you believe this to be a mistake, please reply to this email as soon as possible.</p>
88
<?php break; ?>
99

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
ensurePIGroupDoesNotExist();
7676
}
7777
}
78+
79+
public function testReenableGroup()
80+
{
81+
global $USER, $SSO, $LDAP, $SQL, $MAILER, $WEBHOOK;
82+
$this->switchUser("ResurrectedOwnerOfDisabledPIGroup");
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+
$pi_group->setIsDisabled(true);
97+
assert(!$user->isPI());
98+
}
99+
}
100+
}
78101
}

0 commit comments

Comments
 (0)