diff --git a/src/Laravel/Security/ResourceAccessChecker.php b/src/Laravel/Security/ResourceAccessChecker.php index 5633ea1c808..e64e5808dda 100644 --- a/src/Laravel/Security/ResourceAccessChecker.php +++ b/src/Laravel/Security/ResourceAccessChecker.php @@ -21,11 +21,11 @@ class ResourceAccessChecker implements ResourceAccessCheckerInterface { public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool { + $object = $extraVariables['object'] ?? null; + return Gate::allows( $expression, - $extraVariables['object'] instanceof Paginator ? - $resourceClass : - $extraVariables['object'] + ($object instanceof Paginator || null === $object) ? $resourceClass : $object ); } } diff --git a/src/Laravel/Tests/Policy/Issue7945Test.php b/src/Laravel/Tests/Policy/Issue7945Test.php new file mode 100644 index 00000000000..4ca87165f96 --- /dev/null +++ b/src/Laravel/Tests/Policy/Issue7945Test.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\ApiResource\Issue7945; +use Workbench\App\Policies\Issue7945Policy; + +class Issue7945Test extends TestCase +{ + use ApiTestAssertionsTrait; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(static fn (string $modelClass) => Issue7945::class === $modelClass ? Issue7945Policy::class : null); + } + + public function testPolicyGrantsAccessWhenBodyIsNull(): void + { + $response = $this->postJson('/api/issue7945/import', [], ['accept' => 'application/ld+json']); + $response->assertStatus(202); + } +} diff --git a/src/Laravel/Tests/Unit/Security/ResourceAccessCheckerTest.php b/src/Laravel/Tests/Unit/Security/ResourceAccessCheckerTest.php new file mode 100644 index 00000000000..60781af913f --- /dev/null +++ b/src/Laravel/Tests/Unit/Security/ResourceAccessCheckerTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Unit\Security; + +use ApiPlatform\Laravel\Eloquent\Paginator; +use ApiPlatform\Laravel\Security\ResourceAccessChecker; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Book; + +class ResourceAccessCheckerTest extends TestCase +{ + use WithWorkbench; + + public function testNullObjectFallsBackToResourceClass(): void + { + Gate::shouldReceive('allows') + ->once() + ->with('import', Book::class) + ->andReturn(true); + + $checker = new ResourceAccessChecker(); + $this->assertTrue($checker->isGranted(Book::class, 'import', ['object' => null])); + } + + public function testPaginatorObjectFallsBackToResourceClass(): void + { + Gate::shouldReceive('allows') + ->once() + ->with('viewAny', Book::class) + ->andReturn(true); + + $paginator = new Paginator(new LengthAwarePaginator([], 0, 1)); + $checker = new ResourceAccessChecker(); + $this->assertTrue($checker->isGranted(Book::class, 'viewAny', ['object' => $paginator])); + } + + public function testActualObjectIsForwarded(): void + { + $book = new Book(); + + Gate::shouldReceive('allows') + ->once() + ->with('view', $book) + ->andReturn(true); + + $checker = new ResourceAccessChecker(); + $this->assertTrue($checker->isGranted(Book::class, 'view', ['object' => $book])); + } +} diff --git a/src/Laravel/workbench/app/ApiResource/Issue7945.php b/src/Laravel/workbench/app/ApiResource/Issue7945.php new file mode 100644 index 00000000000..395c8331b94 --- /dev/null +++ b/src/Laravel/workbench/app/ApiResource/Issue7945.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\App\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; + +#[ApiResource( + operations: [ + new Post( + uriTemplate: '/issue7945/import', + policy: 'import', + output: false, + deserialize: false, + status: 202, + processor: [self::class, 'process'], + ), + ] +)] +class Issue7945 +{ + public static function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + return null; + } +} diff --git a/src/Laravel/workbench/app/Policies/Issue7945Policy.php b/src/Laravel/workbench/app/Policies/Issue7945Policy.php new file mode 100644 index 00000000000..e5cb2a2e16e --- /dev/null +++ b/src/Laravel/workbench/app/Policies/Issue7945Policy.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\App\Policies; + +use Illuminate\Foundation\Auth\User; + +class Issue7945Policy +{ + public function import(?User $user): bool + { + return true; + } +}