Refactored privilege system

This commit is contained in:
Marcin Kurczewski 2014-09-06 10:00:26 +02:00
parent c70554330c
commit 8e8e983f28
15 changed files with 247 additions and 66 deletions

View file

@ -8,11 +8,10 @@ secret = change
minPasswordLength = 5
[security.privileges]
anonymous = register, viewUser
regularUser = listUsers, viewUser, deleteOwnAccount
powerUser = listUsers, viewUser, deleteOwnAccount
moderator = listUsers, viewUser, deleteOwnAccount
administrator = listUsers, viewUser, deleteOwnAccount, deleteUsers
register = anonymous
listUsers = regularUser, powerUser, moderator, administrator
deleteOwnAccount = regularUser, powerUser, moderator, administrator
deleteAllAccounts = administrator
[users]
minUserNameLength = 1

View file

@ -2,6 +2,13 @@ var App = App || {};
App.Auth = function(jQuery, util, api, appState, promise) {
var privileges = {
register: 'register',
listUsers: 'listUsers',
deleteOwnAccount: 'deleteOwnAccount',
deleteAllAccounts: 'deleteAllAccounts',
};
function loginFromCredentials(userName, password, remember) {
return promise.make(function(resolve, reject) {
promise.wait(api.post('/login', {userName: userName, password: password}))
@ -79,8 +86,14 @@ App.Auth = function(jQuery, util, api, appState, promise) {
appState.set('loggedIn', response.json.user && !!response.json.user.id);
}
function isLoggedIn() {
return appState.get('loggedIn');
function isLoggedIn(userName) {
if (!appState.get('loggedIn'))
return false;
if (typeof(userName) != 'undefined') {
if (getCurrentUser().name != userName)
return false;
}
return true;
}
function getCurrentUser() {
@ -105,11 +118,14 @@ App.Auth = function(jQuery, util, api, appState, promise) {
loginAnonymous: loginAnonymous,
tryLoginFromCookie: tryLoginFromCookie,
logout: logout,
startObservingLoginChanges: startObservingLoginChanges,
isLoggedIn: isLoggedIn,
getCurrentUser: getCurrentUser,
getCurrentPrivileges: getCurrentPrivileges,
hasPrivilege: hasPrivilege,
startObservingLoginChanges: startObservingLoginChanges,
privileges: privileges,
};
};

View file

@ -33,7 +33,7 @@ App.Presenters.TopNavigationPresenter = function(
$el.html(template({
loggedIn: auth.isLoggedIn(),
user: auth.getCurrentUser(),
canListUsers: auth.hasPrivilege('listUsers')
canListUsers: auth.hasPrivilege(auth.privileges.listUsers)
}));
$el.find('li.' + selectedElement).addClass('active');
};

View file

@ -5,6 +5,7 @@ App.Presenters.UserListPresenter = function(
jQuery,
util,
promise,
auth,
router,
pagedCollectionPresenter,
topNavigationPresenter,

View file

@ -21,7 +21,7 @@ App.Presenters.UserPresenter = function(
function init(args) {
userName = args.userName;
topNavigationPresenter.select(auth.isLoggedIn() && auth.getCurrentUser().name == userName ? 'my-account' : 'users');
topNavigationPresenter.select(auth.isLoggedIn(userName) ? 'my-account' : 'users');
promise.waitAll(
util.promiseTemplate('user'),
@ -51,8 +51,8 @@ App.Presenters.UserPresenter = function(
function render() {
var context = {
user: user,
canDeleteAccount: auth.hasPrivilege('deleteAccounts') ||
(auth.hasPrivilege('deleteOwnAccount') && auth.getCurrentUser().name == userName),
canDeleteAccount: auth.hasPrivilege(auth.privileges.deleteAllAccounts) ||
(auth.isLoggedIn(userName) && auth.hasPrivilege(auth.privileges.deleteOwnAccount)),
};
$el.html(template(context));
$el.find('.browsing-settings').html(browsingSettingsTemplate(context));

View file

@ -4,23 +4,20 @@ namespace Szurubooru\Controllers;
final class AuthController extends AbstractController
{
private $authService;
private $userService;
private $passwordService;
private $privilegeService;
private $inputReader;
private $userViewProxy;
private $tokenViewProxy;
public function __construct(
\Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\UserService $userService,
\Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\PrivilegeService $privilegeService,
\Szurubooru\Helpers\InputReader $inputReader,
\Szurubooru\Controllers\ViewProxies\UserViewProxy $userViewProxy,
\Szurubooru\Controllers\ViewProxies\TokenViewProxy $tokenViewProxy)
{
$this->authService = $authService;
$this->userService = $userService;
$this->passwordService = $passwordService;
$this->privilegeService = $privilegeService;
$this->inputReader = $inputReader;
$this->userViewProxy = $userViewProxy;
$this->tokenViewProxy = $tokenViewProxy;
@ -51,7 +48,7 @@ final class AuthController extends AbstractController
[
'token' => $this->tokenViewProxy->fromEntity($this->authService->getLoginToken()),
'user' => $this->userViewProxy->fromEntity($this->authService->getLoggedInUser()),
'privileges' => $this->authService->getCurrentPrivileges(),
'privileges' => $this->privilegeService->getCurrentPrivileges(),
];
}
}

View file

@ -3,18 +3,18 @@ namespace Szurubooru\Controllers;
final class UserController extends AbstractController
{
private $authService;
private $privilegeService;
private $userService;
private $inputReader;
private $userViewProxy;
public function __construct(
\Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\PrivilegeService $privilegeService,
\Szurubooru\Services\UserService $userService,
\Szurubooru\Helpers\InputReader $inputReader,
\Szurubooru\Controllers\ViewProxies\UserViewProxy $userViewProxy)
{
$this->authService = $authService;
$this->privilegeService = $privilegeService;
$this->userService = $userService;
$this->inputReader = $inputReader;
$this->userViewProxy = $userViewProxy;
@ -31,8 +31,6 @@ final class UserController extends AbstractController
public function getByName($name)
{
$this->authService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_VIEW_USER);
$user = $this->userService->getByName($name);
if (!$user)
throw new \DomainException('User with name "' . $name . '" was not found.');
@ -41,7 +39,7 @@ final class UserController extends AbstractController
public function getFiltered()
{
$this->authService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_LIST_USERS);
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_LIST_USERS);
$searchFormData = new \Szurubooru\FormData\SearchFormData($this->inputReader);
$searchResult = $this->userService->getFiltered($searchFormData);
@ -54,7 +52,7 @@ final class UserController extends AbstractController
public function register()
{
$this->authService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_REGISTER);
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_REGISTER);
$input = new \Szurubooru\FormData\RegistrationFormData($this->inputReader);
$user = $this->userService->register($input);
@ -68,10 +66,11 @@ final class UserController extends AbstractController
public function delete($name)
{
if ($name == $this->authService->getLoggedInUser()->name)
$this->authService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_DELETE_OWN_ACCOUNT);
else
$this->authService->assertPrivilege(\Szurubooru\Privilege::PRIVILEGE_DELETE_ACCOUNTS);
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($name)
? \Szurubooru\Privilege::PRIVILEGE_DELETE_OWN_ACCOUNT
: \Szurubooru\Privilege::PRIVILEGE_DELETE_ACCOUNTS);
return $this->userService->deleteByName($name);
}
}

View file

@ -3,6 +3,13 @@ namespace Szurubooru\Controllers\ViewProxies;
class UserViewProxy extends AbstractViewProxy
{
private $privilegeService;
public function __construct(\Szurubooru\Services\PrivilegeService $privilegeService)
{
$this->privilegeService = $privilegeService;
}
public function fromEntity($user)
{
$result = new \StdClass;
@ -10,6 +17,15 @@ class UserViewProxy extends AbstractViewProxy
{
$result->id = $user->id;
$result->name = $user->name;
$result->accessRank = \Szurubooru\Helpers\EnumHelper::accessRankToString($user->accessRank);
$result->registrationTime = $user->registrationTime;
$result->lastLoginTime = $user->lastLoginTime;
if ($this->privilegeService->hasPrivilege(\Szurubooru\Privilege::PRIVILEGE_VIEW_ALL_EMAIL_ADDRESSES) or
$this->privilegeService->isLoggedIn($user))
{
$result->email = $user->email;
}
}
return $result;
}

View file

@ -0,0 +1,19 @@
<?php
namespace Szurubooru\Helpers;
class EnumHelper
{
public static function accessRankToString($accessRank)
{
switch ($accessRank)
{
case \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS: return 'anonymous'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER: return 'regularUser'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_POWER_USER: return 'powerUser'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_MODERATOR: return 'moderator'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR: return 'administrator'; break;
default:
throw new \DomainException('Invalid access rank!');
}
}
}

View file

@ -3,9 +3,8 @@ namespace Szurubooru;
class Privilege
{
const PRIVILEGE_LIST_USERS = 'listUsers';
const PRIVILEGE_VIEW_USER = 'viewUser';
const PRIVILEGE_DELETE_ACCOUNTS = 'deleteAccounts';
const PRIVILEGE_DELETE_OWN_ACCOUNT = 'deleteOwnAccount';
const PRIVILEGE_REGISTER = 'register';
const PRIVILEGE_LIST_USERS = 'listUsers';
const PRIVILEGE_DELETE_OWN_ACCOUNT = 'deleteOwnAccount';
const PRIVILEGE_DELETE_ALL_ACCOUNTS = 'deleteAllAccounts';
}

View file

@ -7,7 +7,6 @@ class AuthService
private $loginToken = null;
private $validator;
private $config;
private $passwordService;
private $timeService;
private $userDao;
@ -15,7 +14,6 @@ class AuthService
public function __construct(
\Szurubooru\Validator $validator,
\Szurubooru\Config $config,
\Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\TimeService $timeService,
\Szurubooru\Dao\TokenDao $tokenDao,
@ -24,7 +22,6 @@ class AuthService
$this->loggedInUser = $this->getAnonymousUser();
$this->validator = $validator;
$this->config = $config;
$this->passwordService = $passwordService;
$this->timeService = $timeService;
$this->tokenDao = $tokenDao;
@ -108,32 +105,6 @@ class AuthService
$this->loginToken = null;
}
public function getCurrentPrivileges()
{
switch ($this->getLoggedInUser()->accessRank)
{
case \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS: $keyName = 'anonymous'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER: $keyName = 'regularUser'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_POWER_USER: $keyName = 'powerUser'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_MODERATOR: $keyName = 'moderator'; break;
case \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR: $keyName = 'administrator'; break;
default:
throw new \DomainException('Invalid access rank!');
}
return array_filter(preg_split('/[;,\s]+/', $this->config->security->privileges[$keyName]));
}
public function hasPrivilege($privilege)
{
return in_array($privilege, $this->getCurrentPrivileges());
}
public function assertPrivilege($privilege)
{
if (!$this->hasPrivilege($privilege))
throw new \DomainException('Unprivileged operation');
}
private function createAndSaveLoginToken(\Szurubooru\Entities\User $user)
{
$loginToken = new \Szurubooru\Entities\Token();

View file

@ -0,0 +1,60 @@
<?php
namespace Szurubooru\Services;
class PrivilegeService
{
private $authService;
private $privilegeMap;
public function __construct(
\Szurubooru\Config $config,
\Szurubooru\Services\AuthService $authService)
{
$this->authService = $authService;
if (isset($config->security->privileges))
{
foreach ($config->security->privileges as $privilegeName => $allowedAccessRanks)
{
$allowedAccessRanks = array_filter(preg_split('/[;,\s]+/', $allowedAccessRanks));
foreach ($allowedAccessRanks as $allowedAccessRank)
{
if (!isset($this->privilegeMap[$allowedAccessRank]))
$this->privilegeMap[$allowedAccessRank] = [];
$this->privilegeMap[$allowedAccessRank] []= $privilegeName;
}
}
}
}
public function getCurrentPrivileges()
{
$currentAccessRank = $this->authService->getLoggedInUser()->accessRank;
$currentAccessRankName = \Szurubooru\Helpers\EnumHelper::accessRankToString($currentAccessRank);
if (!isset($this->privilegeMap[$currentAccessRankName]))
return [];
return $this->privilegeMap[$currentAccessRankName];
}
public function hasPrivilege($privilege)
{
return in_array($privilege, $this->getCurrentPrivileges());
}
public function assertPrivilege($privilege)
{
if (!$this->hasPrivilege($privilege))
throw new \DomainException('Unprivileged operation');
}
public function isLoggedIn($userIdentifier)
{
$loggedInUser = $this->authService->getLoggedInUser();
if ($userIdentifier instanceof \Szurubooru\Entities\User)
return $loggedInUser->name == $userIdentifier->name;
elseif (is_string($userIdentifier))
return $loggedInUser->name == $userIdentifier;
else
throw new \InvalidArgumentException('Invalid user identifier.');
}
}

33
tests/PrivilegeTest.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace Szurubooru\Tests;
class PrivilegeTest extends \Szurubooru\Tests\AbstractTestCase
{
public function testConstNaming()
{
$refl = new \ReflectionClass(\Szurubooru\Privilege::class);
foreach ($refl->getConstants() as $key => $value)
{
$value = strtoupper('privilege_' . ltrim(preg_replace('/[A-Z]/', '_\0', $value), '_'));
$this->assertEquals($key, $value);
}
}
public function testConfigSectionNaming()
{
$refl = new \ReflectionClass(\Szurubooru\Privilege::class);
$constants = array_values($refl->getConstants());
$configPath = __DIR__
. DIRECTORY_SEPARATOR . '..'
. DIRECTORY_SEPARATOR . 'data'
. DIRECTORY_SEPARATOR . 'config.ini';
$config = new \Szurubooru\Config();
$config->loadFromIni($configPath);
foreach ($config->security->privileges as $key => $value)
{
$this->assertTrue(in_array($key, $constants), "$key not in constants");
}
}
}

View file

@ -4,7 +4,6 @@ namespace Szurubooru\Tests\Services;
class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
private $validatorMock;
private $configMock;
private $passwordServiceMock;
private $timeServiceMock;
private $tokenDaoMock;
@ -13,7 +12,6 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function setUp()
{
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->configMock = $this->mockConfig();
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->tokenDaoMock = $this->mock(\Szurubooru\Dao\TokenDao::class);
@ -97,7 +95,6 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
return new \Szurubooru\Services\AuthService(
$this->validatorMock,
$this->configMock,
$this->passwordServiceMock,
$this->timeServiceMock,
$this->tokenDaoMock,

View file

@ -0,0 +1,74 @@
<?php
namespace Szurubooru\Tests\Services;
class PrivilegeServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
private $configMock;
private $authServiceMock;
public function setUp()
{
$this->configMock = $this->mockConfig();
$this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class);
}
public function testReadingConfig()
{
$testUser = new \Szurubooru\Entities\User();
$testUser->name = 'dummy';
$testUser->accessRank = \Szurubooru\Entities\User::ACCESS_RANK_POWER_USER;
$this->authServiceMock->method('getLoggedInUser')->willReturn($testUser);
$privilege = \Szurubooru\Privilege::PRIVILEGE_LIST_USERS;
$this->configMock->set('security/privileges/' . $privilege, 'powerUser');
$privilegeService = $this->getPrivilegeService();
$this->assertEquals([$privilege], $privilegeService->getCurrentPrivileges());
$this->assertTrue($privilegeService->hasPrivilege($privilege));
}
public function testIsLoggedInByString()
{
$testUser1 = new \Szurubooru\Entities\User();
$testUser1->name = 'dummy';
$testUser2 = new \Szurubooru\Entities\User();
$testUser2->name = 'godzilla';
$this->authServiceMock->method('getLoggedInUser')->willReturn($testUser1);
$privilegeService = $this->getPrivilegeService();
$this->assertTrue($privilegeService->isLoggedIn($testUser1->name));
$this->assertFalse($privilegeService->isLoggedIn($testUser2->name));
}
public function testIsLoggedInByUser()
{
$testUser1 = new \Szurubooru\Entities\User();
$testUser1->name = 'dummy';
$testUser2 = new \Szurubooru\Entities\User();
$testUser2->name = 'godzilla';
$this->authServiceMock->method('getLoggedInUser')->willReturn($testUser1);
$privilegeService = $this->getPrivilegeService();
$this->assertTrue($privilegeService->isLoggedIn($testUser1));
$this->assertFalse($privilegeService->isLoggedIn($testUser2));
}
public function testIsLoggedInByInvalidObject()
{
$testUser = new \Szurubooru\Entities\User();
$testUser->name = 'dummy';
$this->authServiceMock->method('getLoggedInUser')->willReturn($testUser);
$rubbish = new \StdClass;
$privilegeService = $this->getPrivilegeService();
$this->setExpectedException(\InvalidArgumentException::class);
$this->assertTrue($privilegeService->isLoggedIn($rubbish));
}
private function getPrivilegeService()
{
return new \Szurubooru\Services\PrivilegeService(
$this->configMock,
$this->authServiceMock);
}
}