Arsse/tests/User/TestAuthorization.php

330 lines
19 KiB
PHP

<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use Phake;
/** @covers \JKingWeb\Arsse\User */
class TestAuthorization extends Test\AbstractTest {
const USERS = [
'user@example.com' => User\Driver::RIGHTS_NONE,
'user@example.org' => User\Driver::RIGHTS_NONE,
'dman@example.com' => User\Driver::RIGHTS_DOMAIN_MANAGER,
'dman@example.org' => User\Driver::RIGHTS_DOMAIN_MANAGER,
'dadm@example.com' => User\Driver::RIGHTS_DOMAIN_ADMIN,
'dadm@example.org' => User\Driver::RIGHTS_DOMAIN_ADMIN,
'gman@example.com' => User\Driver::RIGHTS_GLOBAL_MANAGER,
'gman@example.org' => User\Driver::RIGHTS_GLOBAL_MANAGER,
'gadm@example.com' => User\Driver::RIGHTS_GLOBAL_ADMIN,
'gadm@example.org' => User\Driver::RIGHTS_GLOBAL_ADMIN,
// invalid rights levels
'bad1@example.com' => User\Driver::RIGHTS_NONE+1,
'bad1@example.org' => User\Driver::RIGHTS_NONE+1,
'bad2@example.com' => User\Driver::RIGHTS_DOMAIN_MANAGER+1,
'bad2@example.org' => User\Driver::RIGHTS_DOMAIN_MANAGER+1,
'bad3@example.com' => User\Driver::RIGHTS_DOMAIN_ADMIN+1,
'bad3@example.org' => User\Driver::RIGHTS_DOMAIN_ADMIN+1,
'bad4@example.com' => User\Driver::RIGHTS_GLOBAL_MANAGER+1,
'bad4@example.org' => User\Driver::RIGHTS_GLOBAL_MANAGER+1,
'bad5@example.com' => User\Driver::RIGHTS_GLOBAL_ADMIN+1,
'bad5@example.org' => User\Driver::RIGHTS_GLOBAL_ADMIN+1,
];
const LEVELS = [
User\Driver::RIGHTS_NONE,
User\Driver::RIGHTS_DOMAIN_MANAGER,
User\Driver::RIGHTS_DOMAIN_ADMIN,
User\Driver::RIGHTS_GLOBAL_MANAGER,
User\Driver::RIGHTS_GLOBAL_ADMIN,
];
const DOMAINS = [
'@example.com',
'@example.org',
"",
];
protected $data;
public function setUp(string $drv = Test\User\DriverInternalMock::class, string $db = null) {
$this->clearData();
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userPreAuth = false;
Arsse::$conf = $conf;
if ($db !== null) {
Arsse::$db = new $db();
}
Arsse::$user = Phake::partialMock(User::class);
Phake::when(Arsse::$user)->authorize->thenReturn(true);
foreach (self::USERS as $user => $level) {
Arsse::$user->add($user, "");
Arsse::$user->rightsSet($user, $level);
}
Phake::reset(Arsse::$user);
}
public function tearDown() {
$this->clearData();
}
public function testToggleLogic() {
$this->assertTrue(Arsse::$user->authorizationEnabled());
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
$this->assertFalse(Arsse::$user->authorizationEnabled(false));
$this->assertFalse(Arsse::$user->authorizationEnabled(false));
$this->assertFalse(Arsse::$user->authorizationEnabled(true));
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
}
public function testSelfActionLogic() {
foreach (array_keys(self::USERS) as $user) {
Arsse::$user->auth($user, "");
// users should be able to do basic actions for themselves
$this->assertTrue(Arsse::$user->authorize($user, "userExists"), "User $user could not act for themselves.");
$this->assertTrue(Arsse::$user->authorize($user, "userRemove"), "User $user could not act for themselves.");
}
}
public function testRegularUserLogic() {
foreach (self::USERS as $actor => $rights) {
if ($rights != User\Driver::RIGHTS_NONE) {
continue;
}
Arsse::$user->auth($actor, "");
foreach (array_keys(self::USERS) as $affected) {
// regular users should only be able to act for themselves
if ($actor==$affected) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach (self::LEVELS as $level) {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
// they should not be able to list users
foreach (self::DOMAINS as $domain) {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
public function testDomainManagerLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) {
continue;
}
$actorDomain = substr($actor, strrpos($actor, "@")+1);
Arsse::$user->auth($actor, "");
foreach (self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
// domain managers should be able to check any user on the same domain
if ($actorDomain==$affectedDomain) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should only be able to act for regular users on the same domain
if ($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// and they should only be able to set their own rights to regular user
foreach (self::LEVELS as $level) {
if ($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
}
// they should also be able to list all users on their own domain
foreach (self::DOMAINS as $domain) {
if ($domain=="@".$actorDomain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
}
public function testDomainAdministratorLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) {
continue;
}
$actorDomain = substr($actor, strrpos($actor, "@")+1);
Arsse::$user->auth($actor, "");
$allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
foreach (self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
// domain admins should be able to check any user on the same domain
if ($actorDomain==$affectedDomain) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should be able to act for any user on the same domain who is not a global manager or admin
if ($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should be able to set rights for any user on their domain who is not a global manager or admin, up to domain admin level
foreach (self::LEVELS as $level) {
if ($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
}
// they should also be able to list all users on their own domain
foreach (self::DOMAINS as $domain) {
if ($domain=="@".$actorDomain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
}
public function testGlobalManagerLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
continue;
}
$actorDomain = substr($actor, strrpos($actor, "@")+1);
Arsse::$user->auth($actor, "");
foreach (self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
// global managers should be able to check any user
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
// they should only be able to act for regular users
if ($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// and they should only be able to set their own rights to regular user
foreach (self::LEVELS as $level) {
if ($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
}
// they should also be able to list all users
foreach (self::DOMAINS as $domain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
public function testGlobalAdministratorLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) {
continue;
}
Arsse::$user->auth($actor, "");
// global admins can do anything
foreach (self::USERS as $affected => $affectedRights) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
foreach (self::LEVELS as $level) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
}
}
foreach (self::DOMAINS as $domain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
public function testInvalidLevelLogic() {
foreach (self::USERS as $actor => $rights) {
if (in_array($rights, self::LEVELS)) {
continue;
}
Arsse::$user->auth($actor, "");
foreach (array_keys(self::USERS) as $affected) {
// users with unknown/invalid rights should be treated just like regular users and only be able to act for themselves
if ($actor==$affected) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach (self::LEVELS as $level) {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
// they should not be able to list users
foreach (self::DOMAINS as $domain) {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
public function testInternalExceptionLogic() {
$tests = [
// methods of User class to test, with parameters besides affected user
'exists' => [],
'remove' => [],
'add' => [''],
'passwordSet' => [''],
'propertiesGet' => [],
'propertiesSet' => [[]],
'rightsGet' => [],
'rightsSet' => [User\Driver::RIGHTS_GLOBAL_ADMIN],
'list' => [],
];
// try first with a global admin (there should be no exception)
Arsse::$user->auth("gadm@example.com", "");
$this->assertCount(0, $this->checkExceptions("user@example.org", $tests));
// next try with a regular user acting on another user (everything should fail)
Arsse::$user->auth("user@example.com", "");
$this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
}
public function testExternalExceptionLogic() {
// set up the test for an external driver
$this->setUp(Test\User\DriverExternalMock::class, Test\User\Database::class);
// run the previous test with the external driver set up
$this->testInternalExceptionLogic();
}
// meat of testInternalExceptionLogic and testExternalExceptionLogic
// calls each requested function with supplied arguments, catches authorization exceptions, and returns an array of caught failed calls
protected function checkExceptions(string $user, $tests): array {
$err = [];
foreach ($tests as $func => $args) {
// list method does not take an affected user, so do not unshift for that one
if ($func != "list") {
array_unshift($args, $user);
}
try {
call_user_func_array(array(Arsse::$user, $func), $args);
} catch (User\ExceptionAuthz $e) {
$err[] = $func;
}
}
return $err;
}
public function testMissingUserLogic() {
Arsse::$user->auth("gadm@example.com", "");
$this->assertTrue(Arsse::$user->authorize("user@example.com", "someFunction"));
$this->assertException("doesNotExist", "User");
Arsse::$user->authorize("this_user_does_not_exist@example.org", "someFunction");
}
}