Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions src/Traits/AuthorizableModels.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
};

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(),
);
}
Expand All @@ -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);
}
}
80 changes: 80 additions & 0 deletions tests/Feature/ParameterlessPolicyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Binaryk\LaravelRestify\Tests\Feature;

use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post;
use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository;
use Binaryk\LaravelRestify\Tests\IntegrationTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;

class ParameterlessPolicyTest extends IntegrationTestCase
{
use RefreshDatabase;

protected function setUp(): void
{
parent::setUp();

$_SERVER['restify.parameterless.allowRestify'] = true;
}

public function test_unauthenticated_request_is_allowed_when_parameterless_allow_restify_returns_true(): void
{
Gate::policy(Post::class, ParameterlessAllowPolicy::class);

$this->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;
}
}