Skip to content

Commit 57d92f2

Browse files
committed
fix: support parameterless policy methods for guest access
Laravel's Gate denies guest access when a policy method has zero parameters because policyAllowsGuests() requires a nullable \$user first param to opt in. Add checkPolicyMethod() to AuthorizableModels which uses ReflectionMethod to detect a zero-parameter policy method and calls it directly, bypassing Gate's guest-check logic. Add ParameterlessPolicyTest covering the unauthenticated-allowed, unauthenticated-denied, and authenticated-allowed scenarios.
1 parent a3408ac commit 57d92f2

2 files changed

Lines changed: 123 additions & 5 deletions

File tree

src/Traits/AuthorizableModels.php

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Http\Request;
1010
use Illuminate\Support\Collection;
1111
use Illuminate\Support\Facades\Gate;
12+
use ReflectionMethod;
1213

1314
/**
1415
* Could be used as a trait in a model class and in a repository class.
@@ -31,8 +32,10 @@ public static function authorizedToUseRepository(Request $request): bool
3132
}
3233

3334
$resolver = function () {
34-
return method_exists(Gate::getPolicyFor(static::newModel()), 'allowRestify')
35-
? Gate::check('allowRestify', get_class(static::newModel()))
35+
$policy = Gate::getPolicyFor(static::newModel());
36+
37+
return method_exists($policy, 'allowRestify')
38+
? static::checkPolicyMethod($policy, 'allowRestify', get_class(static::newModel()))
3639
: false;
3740
};
3841

@@ -75,7 +78,11 @@ public static function authorizeToStoreBulk(Request $request): void
7578
public static function authorizedToStore(Request $request): bool
7679
{
7780
if (static::authorizable()) {
78-
return Gate::check('store', static::guessModelClassName());
81+
$policy = Gate::getPolicyFor(static::newModel());
82+
83+
return method_exists($policy, 'store')
84+
? static::checkPolicyMethod($policy, 'store', static::guessModelClassName())
85+
: false;
7986
}
8087

8188
return false;
@@ -84,7 +91,11 @@ public static function authorizedToStore(Request $request): bool
8491
public static function authorizedToStoreBulk(Request $request): bool
8592
{
8693
if (static::authorizable()) {
87-
return Gate::check('storeBulk', static::guessModelClassName());
94+
$policy = Gate::getPolicyFor(static::newModel());
95+
96+
return method_exists($policy, 'storeBulk')
97+
? static::checkPolicyMethod($policy, 'storeBulk', static::guessModelClassName())
98+
: false;
8899
}
89100

90101
return false;
@@ -206,7 +217,15 @@ public function authorizedTo(Request $request, iterable|string $ability): bool
206217

207218
return PolicyCache::resolve(
208219
PolicyCache::keyForPolicyMethods(static::uriKey(), $ability, $this->resource->getKey()),
209-
fn () => Gate::check($ability, $this->resource),
220+
function () use ($ability) {
221+
$policy = Gate::getPolicyFor($this->model());
222+
223+
if ($policy && is_string($ability) && method_exists($policy, $ability)) {
224+
return static::checkPolicyMethod($policy, $ability, $this->resource);
225+
}
226+
227+
return Gate::check($ability, $this->resource);
228+
},
210229
$this->model(),
211230
);
212231
}
@@ -215,4 +234,23 @@ public static function isRepositoryContext(): bool
215234
{
216235
return new static instanceof Repository;
217236
}
237+
238+
/**
239+
* Check a policy method, calling it directly when it has no parameters.
240+
*
241+
* Laravel's Gate requires a nullable $user first parameter to allow guest
242+
* access. When the policy method has zero parameters Gate denies guests
243+
* automatically. This helper detects that case and calls the method
244+
* directly so policies stay clean.
245+
*/
246+
protected static function checkPolicyMethod(object $policy, string $method, mixed ...$arguments): bool
247+
{
248+
$reflection = new ReflectionMethod($policy, $method);
249+
250+
if ($reflection->getNumberOfParameters() === 0) {
251+
return $policy->{$method}();
252+
}
253+
254+
return Gate::check($method, $arguments);
255+
}
218256
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\Tests\Feature;
4+
5+
use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post;
6+
use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository;
7+
use Binaryk\LaravelRestify\Tests\IntegrationTestCase;
8+
use Illuminate\Foundation\Testing\RefreshDatabase;
9+
use Illuminate\Support\Facades\Gate;
10+
11+
class ParameterlessPolicyTest extends IntegrationTestCase
12+
{
13+
use RefreshDatabase;
14+
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$_SERVER['restify.parameterless.allowRestify'] = true;
20+
}
21+
22+
public function test_unauthenticated_request_is_allowed_when_parameterless_allow_restify_returns_true(): void
23+
{
24+
Gate::policy(Post::class, ParameterlessAllowPolicy::class);
25+
26+
$this->logout();
27+
28+
$_SERVER['restify.parameterless.allowRestify'] = true;
29+
30+
$this->getJson(PostRepository::route())
31+
->assertOk();
32+
}
33+
34+
public function test_unauthenticated_request_is_denied_when_parameterless_allow_restify_returns_false(): void
35+
{
36+
Gate::policy(Post::class, ParameterlessAllowPolicy::class);
37+
38+
$this->logout();
39+
40+
$_SERVER['restify.parameterless.allowRestify'] = false;
41+
42+
$this->getJson(PostRepository::route())
43+
->assertForbidden();
44+
}
45+
46+
public function test_authenticated_request_is_allowed_when_parameterless_allow_restify_returns_true(): void
47+
{
48+
Gate::policy(Post::class, ParameterlessAllowPolicy::class);
49+
50+
$_SERVER['restify.parameterless.allowRestify'] = true;
51+
52+
$this->getJson(PostRepository::route())
53+
->assertOk();
54+
}
55+
}
56+
57+
/**
58+
* A policy where allowRestify() has zero parameters — no $user argument at all.
59+
*
60+
* Laravel's Gate denies guest access when a policy method is missing a nullable
61+
* $user parameter. The checkPolicyMethod() helper in AuthorizableModels detects
62+
* this and calls the method directly, bypassing Gate's guest-check logic.
63+
*/
64+
class ParameterlessAllowPolicy
65+
{
66+
public function allowRestify(): bool
67+
{
68+
return $_SERVER['restify.parameterless.allowRestify'] ?? true;
69+
}
70+
71+
public function show(): bool
72+
{
73+
return true;
74+
}
75+
76+
public function store(): bool
77+
{
78+
return true;
79+
}
80+
}

0 commit comments

Comments
 (0)