diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index 4e661b81..3cd9cff0 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Gate; +use ReflectionMethod; /** * 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 } $resolver = function () { - return method_exists(Gate::getPolicyFor(static::newModel()), 'allowRestify') - ? Gate::check('allowRestify', get_class(static::newModel())) + $policy = Gate::getPolicyFor(static::newModel()); + + return method_exists($policy, 'allowRestify') + ? static::checkPolicyMethod($policy, 'allowRestify', get_class(static::newModel())) : false; }; @@ -75,7 +78,11 @@ public static function authorizeToStoreBulk(Request $request): void public static function authorizedToStore(Request $request): bool { if (static::authorizable()) { - return Gate::check('store', static::guessModelClassName()); + $policy = Gate::getPolicyFor(static::newModel()); + + return method_exists($policy, 'store') + ? static::checkPolicyMethod($policy, 'store', static::guessModelClassName()) + : false; } return false; @@ -84,7 +91,11 @@ public static function authorizedToStore(Request $request): bool public static function authorizedToStoreBulk(Request $request): bool { if (static::authorizable()) { - return Gate::check('storeBulk', static::guessModelClassName()); + $policy = Gate::getPolicyFor(static::newModel()); + + return method_exists($policy, 'storeBulk') + ? static::checkPolicyMethod($policy, 'storeBulk', static::guessModelClassName()) + : false; } return false; @@ -206,7 +217,15 @@ public function authorizedTo(Request $request, iterable|string $ability): bool return PolicyCache::resolve( PolicyCache::keyForPolicyMethods(static::uriKey(), $ability, $this->resource->getKey()), - fn () => Gate::check($ability, $this->resource), + function () use ($ability) { + $policy = Gate::getPolicyFor($this->model()); + + if ($policy && is_string($ability) && method_exists($policy, $ability)) { + return static::checkPolicyMethod($policy, $ability, $this->resource); + } + + return Gate::check($ability, $this->resource); + }, $this->model(), ); } @@ -215,4 +234,23 @@ public static function isRepositoryContext(): bool { return new static instanceof Repository; } + + /** + * Check a policy method, calling it directly when it has no parameters. + * + * Laravel's Gate requires a nullable $user first parameter to allow guest + * access. When the policy method has zero parameters Gate denies guests + * automatically. This helper detects that case and calls the method + * directly so policies stay clean. + */ + protected static function checkPolicyMethod(object $policy, string $method, mixed ...$arguments): bool + { + $reflection = new ReflectionMethod($policy, $method); + + if ($reflection->getNumberOfParameters() === 0) { + return $policy->{$method}(); + } + + return Gate::check($method, $arguments); + } } diff --git a/tests/Feature/ParameterlessPolicyTest.php b/tests/Feature/ParameterlessPolicyTest.php new file mode 100644 index 00000000..e7e2fb10 --- /dev/null +++ b/tests/Feature/ParameterlessPolicyTest.php @@ -0,0 +1,80 @@ +logout(); + + $_SERVER['restify.parameterless.allowRestify'] = true; + + $this->getJson(PostRepository::route()) + ->assertOk(); + } + + public function test_unauthenticated_request_is_denied_when_parameterless_allow_restify_returns_false(): void + { + Gate::policy(Post::class, ParameterlessAllowPolicy::class); + + $this->logout(); + + $_SERVER['restify.parameterless.allowRestify'] = false; + + $this->getJson(PostRepository::route()) + ->assertForbidden(); + } + + public function test_authenticated_request_is_allowed_when_parameterless_allow_restify_returns_true(): void + { + Gate::policy(Post::class, ParameterlessAllowPolicy::class); + + $_SERVER['restify.parameterless.allowRestify'] = true; + + $this->getJson(PostRepository::route()) + ->assertOk(); + } +} + +/** + * A policy where allowRestify() has zero parameters — no $user argument at all. + * + * Laravel's Gate denies guest access when a policy method is missing a nullable + * $user parameter. The checkPolicyMethod() helper in AuthorizableModels detects + * this and calls the method directly, bypassing Gate's guest-check logic. + */ +class ParameterlessAllowPolicy +{ + public function allowRestify(): bool + { + return $_SERVER['restify.parameterless.allowRestify'] ?? true; + } + + public function show(): bool + { + return true; + } + + public function store(): bool + { + return true; + } +}