From 8eb1827d7290dba9c55e54fa296275eb6f4b491a Mon Sep 17 00:00:00 2001 From: Italo Date: Thu, 2 Dec 2021 18:22:29 -0300 Subject: [PATCH] [8.x] Adds on-demand gate authorization (#39789) * 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 --- src/Illuminate/Auth/Access/Gate.php | 59 +++++ src/Illuminate/Support/Facades/Gate.php | 2 + tests/Auth/AuthAccessGateTest.php | 276 ++++++++++++++++++++++++ 3 files changed, 337 insertions(+) diff --git a/src/Illuminate/Auth/Access/Gate.php b/src/Illuminate/Auth/Access/Gate.php index 75eba1dcea8f..fe8d93fcb4ec 100644 --- a/src/Illuminate/Auth/Access/Gate.php +++ b/src/Illuminate/Auth/Access/Gate.php @@ -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; @@ -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. * diff --git a/src/Illuminate/Support/Facades/Gate.php b/src/Illuminate/Support/Facades/Gate.php index 21355f2008a6..49d8b66d6a7f 100644 --- a/src/Illuminate/Support/Facades/Gate.php +++ b/src/Illuminate/Support/Facades/Gate.php @@ -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) diff --git a/tests/Auth/AuthAccessGateTest.php b/tests/Auth/AuthAccessGateTest.php index c9c0d7d08ef2..4c68e2f00974 100644 --- a/tests/Auth/AuthAccessGateTest.php +++ b/tests/Auth/AuthAccessGateTest.php @@ -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) {