summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArnold Daniels <arnold@jasny.net>2016-12-27 15:02:41 +0100
committerArnold Daniels <arnold@jasny.net>2016-12-27 15:04:05 +0100
commit53e44b637e73de67a96b6923c33edb1e41ba47da (patch)
tree151512acd0772384470d71a2aa50337a7a8f1557
parent723cc3bda66330a9f7daa7947f893e6fb87d146c (diff)
downloadauth-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.md55
-rw-r--r--composer.json2
-rw-r--r--src/Authz.php7
-rw-r--r--src/Authz/ByGroup.php105
-rw-r--r--tests/Auth/SessionsTest.php28
-rw-r--r--tests/Authz/ByGroupTest.php94
6 files changed, 229 insertions, 62 deletions
diff --git a/README.md b/README.md
index 01daff5..a117c23 100644
--- a/README.md
+++ b/README.md
@@ -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'");
+ }
+}