diff options
author | Arnold Daniels <arnold@jasny.net> | 2016-12-27 15:02:41 +0100 |
---|---|---|
committer | Arnold Daniels <arnold@jasny.net> | 2016-12-27 15:04:05 +0100 |
commit | 53e44b637e73de67a96b6923c33edb1e41ba47da (patch) | |
tree | 151512acd0772384470d71a2aa50337a7a8f1557 | |
parent | 723cc3bda66330a9f7daa7947f893e6fb87d146c (diff) | |
download | auth-53e44b637e73de67a96b6923c33edb1e41ba47da.zip auth-53e44b637e73de67a96b6923c33edb1e41ba47da.tar.gz auth-53e44b637e73de67a96b6923c33edb1e41ba47da.tar.bz2 |
Added tests for Authz\ByGroup
Authz interface now has `getRoles()` method
Removed $groups property infavor of using abstract function `getGroupStructure()`
-rw-r--r-- | README.md | 55 | ||||
-rw-r--r-- | composer.json | 2 | ||||
-rw-r--r-- | src/Authz.php | 7 | ||||
-rw-r--r-- | src/Authz/ByGroup.php | 105 | ||||
-rw-r--r-- | tests/Auth/SessionsTest.php | 28 | ||||
-rw-r--r-- | tests/Authz/ByGroupTest.php | 94 |
6 files changed, 229 insertions, 62 deletions
@@ -182,25 +182,52 @@ must return the access level of the user, either as string or as integer. The `Auth\ByGroup` traits implements authorization using access groups. An access group may supersede other groups. +You must implement the `getGroupStructure()` method which should return an array. The keys are the names of the +groups. The value should be an array with groups the group supersedes. + ```php class Auth extends Jasny\Auth implements Jasny\Authz { use Jasny\Authz\ByGroup; - /** - * Authorization groups and each group is supersedes. - * @var array - */ - protected $groups = [ - 'users' => [], - 'managers' => [], - 'employees' => ['user'], - 'developers' => ['employees'], - 'paralegals' => ['employees'], - 'lawyers' => ['paralegals'], - 'lead-developers' => ['developers', 'managers'], - 'firm-partners' => ['lawyers', 'managers'] - ]; + protected function getGroupStructure() + { + return [ + 'users' => [], + 'managers' => [], + 'employees' => ['user'], + 'developers' => ['employees'], + 'paralegals' => ['employees'], + 'lawyers' => ['paralegals'], + 'lead-developers' => ['developers', 'managers'], + 'firm-partners' => ['lawyers', 'managers'] + ]; + } +} +``` + +If you get the values from a database, make sure to save them in a property for performance. + +```php +class Auth extends Jasny\Auth implements Jasny\Authz +{ + use Jasny\Authz\ByGroup; + + protected $groups; + + protected function getGroupStructure() + { + if (!isset($this->groups)) { + $this->groups = []; + $result = $this->db->query("SELECT ..."); + + while (($row = $result->fetchAssoc())) { + $this->groups[$row['group']] = explode(';', $row['supersedes']); + } + } + + return $this->groups; + } } ``` diff --git a/composer.json b/composer.json index 832bf96..5d3baa4 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ } }, "require-dev": { - "jasny/php-code-quality": "^2.0", + "jasny/php-code-quality": "^2.1.1", "hashids/hashids": "~2.0.0" }, "suggest": { diff --git a/src/Authz.php b/src/Authz.php index a1f0d97..9df9752 100644 --- a/src/Authz.php +++ b/src/Authz.php @@ -8,6 +8,13 @@ namespace Jasny; interface Authz { /** + * Get all auth roles + * + * @return array + */ + public function getRoles(); + + /** * Check if the current user is logged in and has specified role. * * <code> diff --git a/src/Authz/ByGroup.php b/src/Authz/ByGroup.php index 5b295b4..7f8c203 100644 --- a/src/Authz/ByGroup.php +++ b/src/Authz/ByGroup.php @@ -2,6 +2,8 @@ namespace Jasny\Authz; +use Jasny\Authz\User; + /** * Authorize by access group. * Can be used for ACL (Access Control List). @@ -11,56 +13,107 @@ namespace Jasny\Authz; * { * use Jasny\Authz\ByGroup; * - * protected $groups = [ - * 'user' => [], - * 'developer' => ['user'], - * 'accountant' => ['user'], - * 'admin' => ['developer', 'accountant'] - * ]; + * protected function getGroupStructure() + * { + * return [ + * 'user' => [], + * 'accountant' => ['user'], + * 'moderator' => ['user'], + * 'developer' => ['user'], + * 'admin' => ['moderator', 'developer'] + * ]; + * } * } * </code> */ trait ByGroup { /** - * Authentication groups - * @internal Overwrite this in your child class + * Get the authenticated user + * + * @return User + */ + abstract public function user(); + + /** + * Get the groups and the groups it supersedes. * - * @var string[] + * @return array */ - protected $groups = [ - 'user' => [] - ]; + abstract protected function getGroupStructure(); + /** - * Get all auth groups + * Get group and all groups it supersedes (recursively). * + * @param string|array $group Single group or array of groups * @return array */ - public function getGroups() + protected function expandGroup($group) { - return $this->groups; + $groups = (array)$group; + $structure = $this->getGroupStructure(); + + $expanded = []; + + foreach ($groups as $group) { + if (!isset($structure[$group])) { + continue; + } + + $expanded[] = $group; + $expanded = array_merge($expanded, $this->expandGroup((array)$structure[$group])); + } + + return array_unique($expanded); } + /** - * Get group and all groups it embodies. + * Get all auth roles * - * @param string|array $groups Single group or array of groups * @return array */ - public function expandGroup($groups) + public function getRoles() { - if (!is_array($groups)) $groups = (array)$groups; + $structure = $this->getGroupStructure(); - $allGroups = static::getGroups(); - $expanded = $groups; + if (!is_array($structure)) { + throw new \UnexpectedValueException("Group structure should be an array"); + } - foreach ($groups as $group) { - if (!empty($allGroups[$group])) { - $expanded = array_merge($groups, static::expandGroup($allGroups[$group])); - } + return array_keys($structure); + } + + /** + * Check if the current user is logged in and has specified role. + * + * <code> + * if (!$auth->is('manager')) { + * http_response_code(403); // Forbidden + * echo "You are not allowed to view this page"; + * exit(); + * } + * </code> + * + * @param string $group + * @return boolean + */ + public function is($group) + { + if (!in_array($group, $this->getRoles())) { + trigger_error("Unknown role '$group'", E_USER_NOTICE); + return false; } - return array_unique($expanded); + $user = $this->user(); + + if (!isset($user)) { + return false; + } + + $userGroups = $this->expandGroup($user->getRole()); + + return in_array($group, $userGroups); } } diff --git a/tests/Auth/SessionsTest.php b/tests/Auth/SessionsTest.php index ead1251..8cc2771 100644 --- a/tests/Auth/SessionsTest.php +++ b/tests/Auth/SessionsTest.php @@ -5,12 +5,15 @@ namespace Jasny\Auth; use Jasny\Auth; use PHPUnit_Framework_TestCase as TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Jasny\TestHelper; /** * @covers Jasny\Auth\Sessions */ class SessionsAuth extends TestCase { + use TestHelper; + /** * @var Auth\Sessions|MockObject */ @@ -52,34 +55,17 @@ class SessionsAuth extends TestCase } - /** - * Call a protected method - * - * @param object $object - * @param string $method - * @param array $args - * @return mixed - */ - protected function callProtectedMethod($object, $method, array $args = []) - { - $refl = new \ReflectionMethod(get_class($object), $method); - $refl->setAccessible(true); - - return $refl->invokeArgs($object, $args); - } - - public function testGetCurrentUserIdWithUser() { $_SESSION['auth_uid'] = 123; - $id = $this->callProtectedMethod($this->auth, 'getCurrentUserId'); + $id = $this->callPrivateMethod($this->auth, 'getCurrentUserId'); $this->assertEquals(123, $id); } public function testGetCurrentUserIdWithoutUser() { - $id = $this->callProtectedMethod($this->auth, 'getCurrentUserId'); + $id = $this->callPrivateMethod($this->auth, 'getCurrentUserId'); $this->assertNull($id); } @@ -93,7 +79,7 @@ class SessionsAuth extends TestCase $this->auth->expects($this->once())->method('user')->willReturn($user); - $this->callProtectedMethod($this->auth, 'persistCurrentUser'); + $this->callPrivateMethod($this->auth, 'persistCurrentUser'); $this->assertEquals(['foo' => 'bar', 'auth_uid' => 123], $_SESSION); } @@ -105,7 +91,7 @@ class SessionsAuth extends TestCase $this->auth->expects($this->once())->method('user')->willReturn(null); - $this->callProtectedMethod($this->auth, 'persistCurrentUser'); + $this->callPrivateMethod($this->auth, 'persistCurrentUser'); $this->assertEquals(['foo' => 'bar'], $_SESSION); } diff --git a/tests/Authz/ByGroupTest.php b/tests/Authz/ByGroupTest.php new file mode 100644 index 0000000..5aee83e --- /dev/null +++ b/tests/Authz/ByGroupTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Jasny\Authz; + +use Jasny\Authz; +use Jasny\Authz\User; +use PHPUnit_Framework_TestCase as TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Jasny\TestHelper; + +/** + * @covers Jasny\Authz\ByGroup + */ +class ByGroupTest extends TestCase +{ + use TestHelper; + + /** + * @var Authz\ByGroup|MockObject + */ + protected $auth; + + public function setUp() + { + $this->auth = $this->getMockForTrait(Authz\ByGroup::class); + + $this->auth->method('getGroupStructure')->willReturn([ + 'user' => [], + 'client' => ['user'], + 'mod' => ['user'], + 'dev' => ['user'], + 'admin' => ['mod', 'dev'] + ]); + } + + public function testGetRoles() + { + $this->assertEquals(['user', 'client', 'mod', 'dev', 'admin'], $this->auth->getRoles()); + } + + /** + * @expectedException \UnexpectedValueException + */ + public function testGetRolesWithInvalidStructure() + { + $this->auth = $this->getMockForTrait(Authz\ByGroup::class); + $this->auth->method('getGroupStructure')->willReturn('foo bar'); + + $this->auth->getRoles(); + } + + + public function testIsWithoutUser() + { + $this->assertFalse($this->auth->is('user')); + } + + public function roleProvider() + { + return [ + ['user', ['user' => true, 'client' => false, 'mod' => false, 'dev' => false, 'admin' => false]], + ['client', ['user' => true, 'client' => true, 'mod' => false, 'dev' => false, 'admin' => false]], + ['admin', ['user' => true, 'client' => false, 'mod' => true, 'dev' => true, 'admin' => true]], + [['mod', 'client'], ['user' => true, 'client' => true, 'mod' => true, 'dev' => false, 'admin' => false]], + [['user', 'foo'], ['user' => true, 'client' => false, 'mod' => false, 'dev' => false, 'admin' => false]], + ]; + } + + /** + * @dataProvider roleProvider + * + * @param string|array $role + * @param array $expect + */ + public function testIsWithUser($role, array $expect) + { + $user = $this->createMock(User::class); + $user->method('getRole')->willReturn($role); + + $this->auth->method('user')->willReturn($user); + + $this->assertSame($expect['user'], $this->auth->is('user')); + $this->assertSame($expect['client'], $this->auth->is('client')); + $this->assertSame($expect['mod'], $this->auth->is('mod')); + $this->assertSame($expect['dev'], $this->auth->is('dev')); + $this->assertSame($expect['admin'], $this->auth->is('admin')); + } + + public function testIsWithUnknownRole() + { + $this->assertFalse(@$this->auth->is('foo')); + $this->assertLastError(E_USER_NOTICE, "Unknown role 'foo'"); + } +} |