Skip to content
Merged
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
1 change: 1 addition & 0 deletions LDAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Terminology:

- **qualified user**: a user who is currently a PI or a member of at least one PI group
- **unqualified user**: inverse of qualified
- **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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ See the Docker Compose environment (`tools/docker-dev/`) for an (unsafe for prod
- Restricted access to `webroot/admin/`
- Global access (with valid authentication) to `webroot/`
- No access anywhere else
1. Authorization for your other services based on user flag groups
- in order to access your services, a user should be in the `qualified` group and should not be in the `locked`, `idlelocked`, or `disabled` groups
- (what services you offer) and (how you implement this authorization) are out of scope

## Configuration

Expand Down Expand Up @@ -123,6 +126,11 @@ rm "$prod" && ln -s "$old" "$prod"

### Version-specific update instructions:

### 1.6 -> 1.7

- the `update-qualified-users-group.php` worker should be executed
- this may remove a large number of users from your qualified users group

### 1.5 -> 1.6

- the `[site]getting_started_url` option should be defined
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ parameters:
- resources
- webroot
- test
excludePaths:
- test/Template.php
ignoreErrors:
# $this, $data comes from UnityMailer
- messages:
Expand All @@ -21,6 +23,7 @@ parameters:
- '#Negated boolean expression is always false\.#'
paths:
- test/functional/PIRemoveUserTest.php
- test/functional/LeaveGroupTest.php
- messages:
- '#If condition is always false\.#'
paths:
Expand Down
2 changes: 2 additions & 0 deletions resources/init.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
$_SESSION["is_pi"] = $USER->isPI();

$SQL->addLog("user_login", $OPERATOR->uid);

$USER->updateIsQualified(); // in case manual changes have been made to PI groups
}

$LOC_HEADER = __DIR__ . "/templates/header.php";
Expand Down
5 changes: 5 additions & 0 deletions resources/lib/PosixGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,9 @@ public function memberUIDExists(string $uid): bool
{
return in_array($uid, $this->getMemberUIDs());
}

public function overwriteMemberUIDs(array $uids): void
{
$this->entry->setAttribute("memberuid", $uids);
}
}
12 changes: 4 additions & 8 deletions resources/lib/UnityGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function approveGroup(bool $send_mail = true): void
$this->MAILER->sendMail($this->getOwner()->getMail(), "group_created");
}
// having your own group makes you qualified
$this->getOwner()->setFlag(UserFlag::QUALIFIED, true);
$this->getOwner()->updateIsQualified($send_mail);
}

/**
Expand Down Expand Up @@ -188,13 +188,7 @@ public function approveUser(UnityUser $new_user, bool $send_mail = true): void
"org" => $new_user->getOrg(),
]);
}
// being in a group makes you qualified
$new_user->setFlag(
UserFlag::QUALIFIED,
true,
doSendMail: $send_mail,
doSendMailAdmin: false,
);
$new_user->updateIsQualified($send_mail); // being in a group makes you qualified
}

public function denyUser(UnityUser $new_user, bool $send_mail = true): void
Expand Down Expand Up @@ -245,6 +239,8 @@ public function removeUser(UnityUser $new_user, bool $send_mail = true): void
"org" => $new_user->getOrg(),
]);
}
// if user is no longer in any PI group, disqualify them
$new_user->updateIsQualified($send_mail);
}

public function newUserRequest(UnityUser $new_user, bool $send_mail = true): void
Expand Down
10 changes: 10 additions & 0 deletions resources/lib/UnityUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,4 +417,14 @@ public function isInGroup(string $uid, UnityGroup $group): bool
{
return in_array($uid, $group->getMemberUIDs());
}

public function updateIsQualified(bool $send_mail = true)
{
$this->setFlag(
UserFlag::QUALIFIED,
count($this->getPIGroupGIDs()) !== 0,
doSendMail: $send_mail,
doSendMailAdmin: false,
);
}
}
8 changes: 6 additions & 2 deletions resources/mail/user_flag_added.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<?php use UnityWebPortal\lib\UserFlag; ?>
<?php switch ($data["flag"]):
case UserFlag::QUALIFIED: ?>
<?php $this->Subject = "User Activated"; ?>
<?php $this->Subject = "User Qualified"; ?>
<p>Hello,</p>
<p>Your account on the UnityHPC Platform has been activated. Your account details are below:</p>
<p>
Your account on the UnityHPC Platform has been qualified.
You should now be able to access UnityHPC Platform services.
Your account details are below:
</p>
<p>
<strong>Username</strong> <?php echo $data["user"]; ?>
<br>
Expand Down
8 changes: 6 additions & 2 deletions resources/mail/user_flag_removed.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<?php use UnityWebPortal\lib\UserFlag; ?>
<?php switch ($data["flag"]):
case UserFlag::QUALIFIED: ?>
<?php $this->Subject = "User Deactivated"; ?>
<?php $this->Subject = "User Disqualified"; ?>
<p>Hello,</p>
<p>Your account on the UnityHPC Platform has been deactivated.</p>
<p>
Your account on the UnityHPC Platform has been disqualified.
You should no longer be able to access UnityHPC Platform services.
</p>
<p>In order to be qualified, you must be a PI or be a member of at least one PI group.</p>
<p>If you believe this to be a mistake, please reply to this email as soon as possible.</p>
<?php break; ?>

Expand Down
4 changes: 2 additions & 2 deletions resources/mail/user_flag_removed_admin.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?php use UnityWebPortal\lib\UserFlag; ?>
<?php switch ($data["flag"]):
case UserFlag::QUALIFIED: ?>
<?php $this->Subject = "User Dequalified"; ?>
<?php $this->Subject = "User Disqualified"; ?>
<p>Hello,</p>
<p>User "<?php echo $data["user"] ?>" has been dequalified. </p>
<p>User "<?php echo $data["user"] ?>" has been disqualified. </p>
<?php break; ?>

<?php /////////////////////////////////////////////////////////////////////////////////////////// ?>
Expand Down
18 changes: 18 additions & 0 deletions test/Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use PHPUnit\Framework\Attributes\DataProvider;
use TRegx\PhpUnit\DataProviders\DataProvider as TRegxDataProvider;

class FoobarTest extends UnityWebPortalTestCase
{
public static function provider(): TRegxDataProvider
{
return TRegxDataProvider::list("foo", "bar");
}

#[DataProvider("provider")]
public function testFoobar(string $x)
{
$this->assertTrue(true);
}
}
68 changes: 68 additions & 0 deletions test/functional/LeaveGroupTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
use UnityWebPortal\lib\UnityUser;
use UnityWebPortal\lib\UserFlag;
use UnityWebPortal\lib\UnityGroup;

class LeaveGroupTest extends UnityWebPortalTestCase
{
public function testLeaveGroupDisqualified()
{
global $USER, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("Normal");
$pi_gids = $USER->getPIGroupGIDs();
$this->assertEquals(1, count($pi_gids));
$gid = $pi_gids[0];
$pi_group = new UnityGroup($gid, $LDAP, $SQL, $MAILER, $WEBHOOK);
$this->assertTrue($pi_group->memberUIDExists($USER->uid));
$this->assertTrue($USER->getFlag(UserFlag::QUALIFIED));
try {
http_post(__DIR__ . "/../../webroot/panel/groups.php", [
"form_type" => "removePIForm",
"pi" => $gid,
]);
$this->assertFalse($pi_group->memberUIDExists($USER->uid));
$this->assertFalse($USER->getFlag(UserFlag::QUALIFIED));
} finally {
if (!$pi_group->memberUIDExists($USER->uid)) {
$pi_group->newUserRequest($USER);
$pi_group->approveUser($USER);
}
$this->assertTrue($pi_group->memberUIDExists($USER->uid));
$this->assertTrue($USER->getFlag(UserFlag::QUALIFIED));
}
}

// if an admin goes messing around with LDAP entries by hand and also forgets to update the
// qualified users group, the portal should update the user's qualified status the next time
// they log in
public function testRemovedFromGroupManuallyDisqualifiedOnLogin()
{
global $USER, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("Normal");
$pi_gids = $USER->getPIGroupGIDs();
$this->assertEquals(1, count($pi_gids));
$gid = $pi_gids[0];
$pi_group = new UnityGroup($gid, $LDAP, $SQL, $MAILER, $WEBHOOK);
$this->assertTrue($pi_group->memberUIDExists($USER->uid));
$this->assertTrue($USER->getFlag(UserFlag::QUALIFIED));
try {
$old_memberuids = $pi_group->getMemberUIDs();
$new_memberuids = array_values(
array_filter($old_memberuids, fn($x) => $x !== $USER->uid),
);
$pi_group_entry = $LDAP->getPIGroupEntry($pi_group->gid);
$pi_group_entry->setAttribute("memberuid", $new_memberuids);
$this->assertTrue($USER->getFlag(UserFlag::QUALIFIED));
session_write_close();
http_get(__DIR__ . "/../../resources/init.php");
$this->assertFalse($USER->getFlag(UserFlag::QUALIFIED));
} finally {
if (!$pi_group->memberUIDExists($USER->uid)) {
$pi_group->newUserRequest($USER);
$pi_group->approveUser($USER);
}
$this->assertTrue($pi_group->memberUIDExists($USER->uid));
$this->assertTrue($USER->getFlag(UserFlag::QUALIFIED));
}
}
}
1 change: 1 addition & 0 deletions test/functional/PIBecomeApproveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function testApprovePI()
$this->assertRequestedPIGroup(false);
} finally {
ensurePIGroupDoesNotExist($pi_group->gid);
$this->assertFalse($USER->getFlag(UserFlag::QUALIFIED));
}
}
}
40 changes: 34 additions & 6 deletions test/functional/PIRemoveUserTest.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
<?php

use PHPUnit\Framework\Attributes\DataProvider;
use UnityWebPortal\lib\UnityUser;
use UnityWebPortal\lib\UserFlag;
use TRegx\PhpUnit\DataProviders\DataProvider as TRegxDataProvider;
use PHPUnit\Framework\Attributes\DataProvider;

class PIRemoveUserTest extends UnityWebPortalTestCase
{
private function removeUser(string $uid)
private function removeUserByPI(string $uid, string $gid)
{
global $USER;
assert($USER->getPIGroup()->gid === $gid, "signed in user must be the group owner");
http_post(__DIR__ . "/../../webroot/panel/pi.php", [
"form_type" => "remUser",
"uid" => $uid,
]);
}

public function testRemoveUser()
private function removeUserByAdmin(string $uid, string $gid)
{
global $USER;
$this->switchUser("Admin");
try {
http_post(__DIR__ . "/../../webroot/admin/pi-mgmt.php", [
"form_type" => "remUserChild",
"pi" => $gid,
"uid" => $uid,
]);
} finally {
$this->switchBackUser();
}
}

public static function provider(): TRegxDataProvider
{
return TRegxDataProvider::list("removeUserByPI", "removeUserByAdmin");
}

#[DataProvider("provider")]
public function testRemoveUser($methodName)
{
global $USER, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("NormalPI");
Expand All @@ -34,10 +59,12 @@ public function testRemoveUser()
}
}
$this->assertNotEquals($pi->uid, $memberToDelete->uid);
$this->assertTrue($memberToDelete->getFlag(UserFlag::QUALIFIED));
$this->assertTrue($piGroup->memberUIDExists($memberToDelete->uid));
try {
$this->removeUser($memberToDelete->uid);
$this->$methodName($memberToDelete->uid, $piGroup->gid);
$this->assertFalse($piGroup->memberUIDExists($memberToDelete->uid));
$this->assertFalse($memberToDelete->getFlag(UserFlag::QUALIFIED));
} finally {
if (!$piGroup->memberUIDExists($memberToDelete->uid)) {
$piGroup->newUserRequest($memberToDelete);
Expand All @@ -46,15 +73,16 @@ public function testRemoveUser()
}
}

public function testRemovePIFromTheirOwnGroup()
#[DataProvider("provider")]
public function testRemovePIFromTheirOwnGroup($methodName)
{
global $USER, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("NormalPI");
$pi = $USER;
$piGroup = $USER->getPIGroup();
$this->expectException(Exception::class);
try {
$this->removeUser($pi->uid);
$this->$methodName($piGroup->getOwner()->uid, $piGroup->gid);
$this->assertTrue($piGroup->memberUIDExists($pi->uid));
} finally {
if (!$piGroup->memberUIDExists($pi->uid)) {
Expand Down
57 changes: 57 additions & 0 deletions test/functional/WorkerUpdateQualifiedUsersGroupTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

use UnityWebPortal\lib\UserFlag;

class WorkerUpdateQualifiedUsersGroupTest extends UnityWebPortalTestCase
{
public function testQualifyUser()
{
global $USER, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("EmptyPIGroupOwner");
$pi_group = $USER->getPIGroup();
$this->switchUser("Blank");
$user = $USER;
$expectedOutput = ["added" => [$user->uid], "removed" => []];
try {
$pi_group_entry = $LDAP->getPIGroupEntry($pi_group->gid);
$pi_group_entry->appendAttribute("memberuid", $user->uid);
[$_, $output_lines] = executeWorker("update-qualified-users-group.php");
$output_str = implode("\n", $output_lines);
$output = jsonDecode($output_str, associative: true);
$this->assertEquals($expectedOutput, $output);
// refresh LDAP to pick up changes from subprocess
unset($GLOBALS["ldapconn"]);
$this->switchUser("Blank", validate: false);
$user = $USER;
$this->assertTrue($user->getFlag(UserFlag::QUALIFIED));
} finally {
if ($pi_group->memberUIDExists($user->uid)) {
$pi_group->removeUser($user);
}
$this->assertFalse($user->getFlag(UserFlag::QUALIFIED));
}
}

public function testDisqualifyUser()
{
global $USER, $LDAP, $SQL, $MAILER, $WEBHOOK;
$this->switchUser("Blank");
$expectedOutput = ["added" => [], "removed" => [$USER->uid]];
try {
$qualified_user_group = $LDAP->userFlagGroups["qualified"];
$qualified_user_group->addMemberUID($USER->uid);
[$_, $output_lines] = executeWorker("update-qualified-users-group.php");
$output_str = implode("\n", $output_lines);
$output = jsonDecode($output_str, associative: true);
$this->assertEquals($expectedOutput, $output);
// refresh LDAP to pick up changes from subprocess
unset($GLOBALS["ldapconn"]);
$this->switchUser("Blank", validate: false);
$this->assertFalse($USER->getFlag(UserFlag::QUALIFIED));
} finally {
if ($USER->getFlag(UserFlag::QUALIFIED)) {
$USER->setFlag(UserFlag::QUALIFIED, false);
}
}
}
}
Loading