Skip to content

Commit

Permalink
[8.x] Adds on-demand gate authorization (#39789)
Browse files Browse the repository at this point in the history
* Adds on-demand gate authorization.

* Removes lingering PHPDoc written by mistake.

* Style changes.

* Removes unused function.

* formatting

* Added exception when auth user is required and is guest.

* Minor line style fix.

* Use `canBeCalledWithUser()` instead.

* Additional tests.

* Moved logic to shared method.

* Removed `value` helper.

* Adds support for `Response` object. Minor formatting.

* Style changes.

* formatting

Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
DarkGhostHunter and taylorotwell committed Dec 2, 2021
1 parent 9ae7467 commit 8eb1827
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/Illuminate/Auth/Access/Gate.php
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Auth\Access;

use Closure;
use Exception;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Container\Container;
Expand Down Expand Up @@ -117,6 +118,64 @@ public function has($ability)
return true;
}

/**
* Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is false.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function allowIf($condition, $message = null, $code = null)
{
return $this->authorizeOnDemand($condition, $message, $code, true);
}

/**
* Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is true.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function denyIf($condition, $message = null, $code = null)
{
return $this->authorizeOnDemand($condition, $message, $code, false);
}

/**
* Authorize a given condition or callback.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @param bool $allowWhenResponseIs
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
protected function authorizeOnDemand($condition, $message, $code, $allowWhenResponseIs)
{
$user = $this->resolveUser();

if ($condition instanceof Closure) {
$response = $this->canBeCalledWithUser($user, $condition)
? $condition($user)
: new Response(false, $message, $code);
} else {
$response = $condition;
}

return with($response instanceof Response ? $response : new Response(
(bool) $response === $allowWhenResponseIs, $message, $code
))->authorize();
}

/**
* Define a new ability.
*
Expand Down
2 changes: 2 additions & 0 deletions src/Illuminate/Support/Facades/Gate.php
Expand Up @@ -8,6 +8,8 @@
* @method static \Illuminate\Auth\Access\Gate guessPolicyNamesUsing(callable $callback)
* @method static \Illuminate\Auth\Access\Response authorize(string $ability, array|mixed $arguments = [])
* @method static \Illuminate\Auth\Access\Response inspect(string $ability, array|mixed $arguments = [])
* @method static \Illuminate\Auth\Access\Response allowIf(\Closure|bool $condition, string|null $message = null, mixed $code = null)
* @method static \Illuminate\Auth\Access\Response denyIf(\Closure|bool $condition, string|null $message = null, mixed $code = null)
* @method static \Illuminate\Contracts\Auth\Access\Gate after(callable $callback)
* @method static \Illuminate\Contracts\Auth\Access\Gate before(callable $callback)
* @method static \Illuminate\Contracts\Auth\Access\Gate define(string $ability, callable|string $callback)
Expand Down
276 changes: 276 additions & 0 deletions tests/Auth/AuthAccessGateTest.php
Expand Up @@ -689,6 +689,282 @@ public function testAuthorizeReturnsAnAllowedResponseForATruthyReturn()
$this->assertNull($response->message());
}

public function testAllowIfAuthorizesTrue()
{
$response = $this->getBasicGate()->allowIf(true);

$this->assertTrue($response->allowed());
}

public function testAllowIfAuthorizesTruthy()
{
$response = $this->getBasicGate()->allowIf('truthy');

$this->assertTrue($response->allowed());
}

public function testAllowIfAuthorizesIfGuest()
{
$response = $this->getBasicGate()->forUser(null)->allowIf(true);

$this->assertTrue($response->allowed());
}

public function testAllowIfAuthorizesCallbackTrue()
{
$response = $this->getBasicGate()->allowIf(function ($user) {
$this->assertSame(1, $user->id);

return true;
}, 'foo', 'bar');

$this->assertTrue($response->allowed());
$this->assertSame('foo', $response->message());
$this->assertSame('bar', $response->code());
}

public function testAllowIfAuthorizesResponseAllowed()
{
$response = $this->getBasicGate()->allowIf(Response::allow('foo', 'bar'));

$this->assertTrue($response->allowed());
$this->assertSame('foo', $response->message());
$this->assertSame('bar', $response->code());
}

public function testAllowIfAuthorizesCallbackResponseAllowed()
{
$response = $this->getBasicGate()->allowIf(function () {
return Response::allow('quz', 'qux');
}, 'foo', 'bar');

$this->assertTrue($response->allowed());
$this->assertSame('quz', $response->message());
$this->assertSame('qux', $response->code());
}

public function testAllowsIfCallbackAcceptsGuestsWhenAuthenticated()
{
$response = $this->getBasicGate()->allowIf(function (stdClass $user = null) {
return $user !== null;
});

$this->assertTrue($response->allowed());
}

public function testAllowIfCallbackAcceptsGuestsWhenUnauthenticated()
{
$gate = $this->getBasicGate()->forUser(null);

$response = $gate->allowIf(function (stdClass $user = null) {
return $user === null;
});

$this->assertTrue($response->allowed());
}

public function testAllowIfThrowsExceptionWhenFalse()
{
$this->expectException(AuthorizationException::class);

$this->getBasicGate()->allowIf(false);
}

public function testAllowIfThrowsExceptionWhenCallbackFalse()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$this->getBasicGate()->allowIf(function () {
return false;
}, 'foo', 'bar');
}

public function testAllowIfThrowsExceptionWhenResponseDenied()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$this->getBasicGate()->allowIf(Response::deny('foo', 'bar'));
}

public function testAllowIfThrowsExceptionWhenCallbackResponseDenied()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('quz');
$this->expectExceptionCode('qux');

$this->getBasicGate()->allowIf(function () {
return Response::deny('quz', 'qux');
}, 'foo', 'bar');
}

public function testAllowIfThrowsExceptionIfUnauthenticated()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$gate = $this->getBasicGate()->forUser(null);

$gate->allowIf(function () {
return true;
}, 'foo', 'bar');
}

public function testAllowIfThrowsExceptionIfAuthUserExpectedWhenGuest()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$gate = $this->getBasicGate()->forUser(null);

$gate->allowIf(function (stdClass $user) {
return true;
}, 'foo', 'bar');
}

public function testDenyIfAuthorizesFalse()
{
$response = $this->getBasicGate()->denyIf(false);

$this->assertTrue($response->allowed());
}

public function testDenyIfAuthorizesFalsy()
{
$response = $this->getBasicGate()->denyIf(0);

$this->assertTrue($response->allowed());
}

public function testDenyIfAuthorizesIfGuest()
{
$response = $this->getBasicGate()->forUser(null)->denyIf(false);

$this->assertTrue($response->allowed());
}

public function testDenyIfAuthorizesCallbackFalse()
{
$response = $this->getBasicGate()->denyIf(function ($user) {
$this->assertSame(1, $user->id);

return false;
}, 'foo', 'bar');

$this->assertTrue($response->allowed());
$this->assertSame('foo', $response->message());
$this->assertSame('bar', $response->code());
}

public function testDenyIfAuthorizesResponseAllowed()
{
$response = $this->getBasicGate()->denyIf(Response::allow('foo', 'bar'));

$this->assertTrue($response->allowed());
$this->assertSame('foo', $response->message());
$this->assertSame('bar', $response->code());
}

public function testDenyIfAuthorizesCallbackResponseAllowed()
{
$response = $this->getBasicGate()->denyIf(function () {
return Response::allow('quz', 'qux');
}, 'foo', 'bar');

$this->assertTrue($response->allowed());
$this->assertSame('quz', $response->message());
$this->assertSame('qux', $response->code());
}

public function testDenyIfCallbackAcceptsGuestsWhenAuthenticated()
{
$response = $this->getBasicGate()->denyIf(function (stdClass $user = null) {
return $user === null;
});

$this->assertTrue($response->allowed());
}

public function testDenyIfCallbackAcceptsGuestsWhenUnauthenticated()
{
$gate = $this->getBasicGate()->forUser(null);

$response = $gate->denyIf(function (stdClass $user = null) {
return $user !== null;
});

$this->assertTrue($response->allowed());
}

public function testDenyIfThrowsExceptionWhenTrue()
{
$this->expectException(AuthorizationException::class);

$this->getBasicGate()->denyIf(true);
}

public function testDenyIfThrowsExceptionWhenCallbackTrue()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$this->getBasicGate()->denyIf(function () {
return true;
}, 'foo', 'bar');
}

public function testDenyIfThrowsExceptionWhenResponseDenied()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$this->getBasicGate()->denyIf(Response::deny('foo', 'bar'));
}

public function testDenyIfThrowsExceptionWhenCallbackResponseDenied()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('quz');
$this->expectExceptionCode('qux');

$this->getBasicGate()->denyIf(function () {
return Response::deny('quz', 'qux');
}, 'foo', 'bar');
}

public function testDenyIfThrowsExceptionIfUnauthenticated()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$gate = $this->getBasicGate()->forUser(null);

$gate->denyIf(function () {
return false;
}, 'foo', 'bar');
}

public function testDenyIfThrowsExceptionIfAuthUserExpectedWhenGuest()
{
$this->expectException(AuthorizationException::class);
$this->expectExceptionMessage('foo');
$this->expectExceptionCode('bar');

$gate = $this->getBasicGate()->forUser(null);

$gate->denyIf(function (stdClass $user) {
return false;
}, 'foo', 'bar');
}

protected function getBasicGate($isAdmin = false)
{
return new Gate(new Container, function () use ($isAdmin) {
Expand Down

0 comments on commit 8eb1827

Please sign in to comment.