Skip to content

Commit

Permalink
Added Auth::asMiddleware()
Browse files Browse the repository at this point in the history
Middleware now works with Auth and Authz is optional
If getRequiredRole returns a boolean, just check if a user is logged in
  • Loading branch information
jasny committed Dec 28, 2016
1 parent 0d68005 commit 233a9ae
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 23 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Jasny;

use Jasny\Auth\User;
use Jasny\Auth\Middleware;

/**
* Authentication and access control
Expand Down Expand Up @@ -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);
}
}
24 changes: 16 additions & 8 deletions src/Auth/Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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');
Expand All @@ -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);
}

Expand Down
77 changes: 62 additions & 15 deletions tests/Auth/MiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
});
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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');

Expand All @@ -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());

Expand All @@ -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');

Expand All @@ -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());

Expand Down
14 changes: 14 additions & 0 deletions tests/AuthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}
}
10 changes: 10 additions & 0 deletions tests/support/TestAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Jasny;

/**
* Auth class that implements Authz
*/
abstract class AuthAuthz extends Auth implements Authz
{
}

0 comments on commit 233a9ae

Please sign in to comment.