diff --git a/composer.json b/composer.json index 2b2bd81..74737c5 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,9 @@ "Jasny\\": "src/" } }, + "autoload-dev": { + "classmap": ["tests/support"] + }, "require-dev": { "jasny/php-code-quality": "^2.1.2", "hashids/hashids": "~2.0.0" diff --git a/src/Auth.php b/src/Auth.php index 3f70fbf..25ea33f 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -3,6 +3,7 @@ namespace Jasny; use Jasny\Auth\User; +use Jasny\Auth\Middleware; /** * Authentication and access control @@ -165,4 +166,17 @@ public function logout() $this->user = false; $this->persistCurrentUser(); } + + + /** + * Create auth middleware interface for access control. + * + * @param Authz $auth + * @param callable $getRequiredRole + * @return Middleware + */ + public function asMiddleware($getRequiredRole) + { + return new Middleware($this, $getRequiredRole); + } } diff --git a/src/Auth/Middleware.php b/src/Auth/Middleware.php index 1700125..248feac 100644 --- a/src/Auth/Middleware.php +++ b/src/Auth/Middleware.php @@ -26,10 +26,10 @@ class Middleware /** * Class constructor * - * @param Authz $auth + * @param Auth $auth * @param callable $getRequiredRole */ - public function __construct(Authz $auth, $getRequiredRole) + public function __construct(Auth $auth, $getRequiredRole) { $this->auth = $auth; @@ -43,15 +43,23 @@ public function __construct(Authz $auth, $getRequiredRole) /** * Check if the current user has one of the roles * - * @param array $roles + * @param array|string|boolean $roles * @return */ - protected function hasRole(array $roles) + protected function hasRole($requiredRole) { + if (is_bool($requiredRole)) { + return $this->auth->user() !== null; + } + $ret = false; - foreach ($roles as $role) { - $ret = $ret || $this->auth->is($role); + if ($this->auth instanceof Authz) { + $roles = (array)$requiredRole; + + foreach ($roles as $role) { + $ret = $ret || $this->auth->is($role); + } } return $ret; @@ -66,7 +74,7 @@ protected function hasRole(array $roles) */ protected function forbidden(ServerRequestInterface $request, ResponseInterface $response) { - $unauthorized = $this->auth instanceof Auth && $this->auth->user() === null; + $unauthorized = $this->auth->user() === null; $forbiddenResponse = $response->withStatus($unauthorized ? 401 : 403); $forbiddenResponse->getBody()->write('Access denied'); @@ -90,7 +98,7 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $requiredRole = call_user_func($this->getRequiredRole, $request); - if (!empty($requiredRole) && !$this->hasRole((array)$requiredRole)) { + if (!empty($requiredRole) && !$this->hasRole($requiredRole)) { return $this->forbidden($request, $response); } diff --git a/tests/Auth/MiddlewareTest.php b/tests/Auth/MiddlewareTest.php index a07486f..bba5357 100644 --- a/tests/Auth/MiddlewareTest.php +++ b/tests/Auth/MiddlewareTest.php @@ -4,6 +4,7 @@ use Jasny\Auth; use Jasny\Authz; +use Jasny\AuthAuthz; use Jasny\Auth\Middleware; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; @@ -32,7 +33,8 @@ class MiddlewareTest extends TestCase public function setUp() { - $this->auth = $this->createMock(Authz::class); + $this->auth = $this->createMock(AuthAuthz::class); + $this->middleware = new Middleware($this->auth, function(ServerRequestInterface $request) { return $request->getAttribute('auth'); }); @@ -45,11 +47,34 @@ public function testConstructInvalidArgument() { new Middleware($this->auth, 'foo bar zoo'); } + - public function testInvokeWithoutRequiredRole() + public function testInvokeWithoutRequiredUser() { - $this->auth->expects($this->never())->method('is'); + $this->auth->expects($this->never())->method('user'); + + $request = $this->createMock(ServerRequestInterface::class); + $request->expects($this->once())->method('getAttribute')->with('auth')->willReturn(false); + + $finalResponse = $this->createMock(ResponseInterface::class); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->never())->method('withStatus'); + + $next = $this->createCallbackMock( + $this->once(), + function(InvocationMocker $invoke) use ($request, $response, $finalResponse) { + $invoke->with($this->identicalTo($request), $this->identicalTo($response))->willReturn($finalResponse); + } + ); + + $result = call_user_func($this->middleware, $request, $response, $next); + $this->assertSame($finalResponse, $result); + } + + public function testInvokeWithoutRequiredRole() + { $request = $this->createMock(ServerRequestInterface::class); $request->expects($this->once())->method('getAttribute')->with('auth')->willReturn(null); @@ -70,6 +95,31 @@ function(InvocationMocker $invoke) use ($request, $response, $finalResponse) { $this->assertSame($finalResponse, $result); } + public function testInvokeWithRequiredUser() + { + $user = $this->createMock(Authz\User::class); + $this->auth->expects($this->once())->method('user')->willReturn($user); + + $request = $this->createMock(ServerRequestInterface::class); + $request->expects($this->once())->method('getAttribute')->with('auth')->willReturn(true); + + $finalResponse = $this->createMock(ResponseInterface::class); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->never())->method('withStatus'); + + $next = $this->createCallbackMock( + $this->once(), + function(InvocationMocker $invoke) use ($request, $response, $finalResponse) { + $invoke->with($this->identicalTo($request), $this->identicalTo($response))->willReturn($finalResponse); + } + ); + + $result = call_user_func($this->middleware, $request, $response, $next); + + $this->assertSame($finalResponse, $result); + } + public function testInvokeWithRequiredRole() { $this->auth->expects($this->once())->method('is')->with('user')->willReturn(true); @@ -94,10 +144,13 @@ function(InvocationMocker $invoke) use ($request, $response, $finalResponse) { $this->assertSame($finalResponse, $result); } - public function testInvokeForbidden() + public function testInvokeUnauthorized() { + $this->auth->expects($this->once())->method('user')->willReturn(null); $this->auth->expects($this->once())->method('is')->with('user')->willReturn(false); + $this->setPrivateProperty($this->middleware, 'auth', $this->auth); + $request = $this->createMock(ServerRequestInterface::class); $request->expects($this->once())->method('getAttribute')->with('auth')->willReturn('user'); @@ -108,7 +161,7 @@ public function testInvokeForbidden() $forbiddenResponse->expects($this->once())->method('getBody')->willReturn($stream); $response = $this->createMock(ResponseInterface::class); - $response->expects($this->once())->method('withStatus')->with(403)->willReturn($forbiddenResponse); + $response->expects($this->once())->method('withStatus')->with(401)->willReturn($forbiddenResponse); $next = $this->createCallbackMock($this->never()); @@ -117,19 +170,13 @@ public function testInvokeForbidden() $this->assertSame($forbiddenResponse, $result); } - public function testInvokeUnauthorized() + public function testInvokeForbidden() { - $this->auth = $this->getMockBuilder(Auth::class) - ->disableProxyingToOriginalMethods() - ->setMethods(['user', 'is', 'persistCurrentUser', 'getCurrentUserId', 'fetchUserById', - 'fetchUserByUsername']) - ->getMock(); + $user = $this->createMock(Authz\User::class); - $this->auth->expects($this->once())->method('user')->willReturn(null); + $this->auth->expects($this->once())->method('user')->willReturn($user); $this->auth->expects($this->once())->method('is')->with('user')->willReturn(false); - $this->setPrivateProperty($this->middleware, 'auth', $this->auth); - $request = $this->createMock(ServerRequestInterface::class); $request->expects($this->once())->method('getAttribute')->with('auth')->willReturn('user'); @@ -140,7 +187,7 @@ public function testInvokeUnauthorized() $forbiddenResponse->expects($this->once())->method('getBody')->willReturn($stream); $response = $this->createMock(ResponseInterface::class); - $response->expects($this->once())->method('withStatus')->with(401)->willReturn($forbiddenResponse); + $response->expects($this->once())->method('withStatus')->with(403)->willReturn($forbiddenResponse); $next = $this->createCallbackMock($this->never()); diff --git a/tests/AuthTest.php b/tests/AuthTest.php index 40177bd..be3f837 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -5,12 +5,15 @@ use Jasny\Auth; use PHPUnit_Framework_TestCase as TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Jasny\TestHelper; /** * @covers Jasny\Auth */ class AuthTest extends TestCase { + use TestHelper; + /** * @var Auth|MockObject */ @@ -177,4 +180,15 @@ public function testLogout() // Logout again shouldn't really do anything $this->auth->logout(); } + + + public function testAsMiddleware() + { + $callback = $this->createCallbackMock($this->never()); + $middleware = $this->auth->asMiddleware($callback); + + $this->assertInstanceOf(Auth\Middleware::class, $middleware); + $this->assertAttributeEquals($this->auth, 'auth', $middleware); + $this->assertAttributeEquals($callback, 'getRequiredRole', $middleware); + } } diff --git a/tests/support/TestAuth.php b/tests/support/TestAuth.php new file mode 100644 index 0000000..eadfacc --- /dev/null +++ b/tests/support/TestAuth.php @@ -0,0 +1,10 @@ +