Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Adds on-demand gate authorization #39789

Merged
merged 14 commits into from Dec 2, 2021
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