Skip to content

Commit 00787f3

Browse files
authored
feat(laravel): automatically register policies (#6623)
* feat: if a policy matches the name of a model we automatically register it, no need to do it manually anymore * fix: optimising the way we handle collection * fix: phpstan * fix: cs-fixer
1 parent 7c56896 commit 00787f3

File tree

6 files changed

+438
-1
lines changed

6 files changed

+438
-1
lines changed

src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,29 @@
1818
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
1919
use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor;
2020
use ApiPlatform\Metadata\CollectionOperationInterface;
21+
use ApiPlatform\Metadata\Delete;
2122
use ApiPlatform\Metadata\DeleteOperationInterface;
23+
use ApiPlatform\Metadata\Get;
24+
use ApiPlatform\Metadata\GetCollection;
25+
use ApiPlatform\Metadata\Patch;
26+
use ApiPlatform\Metadata\Post;
27+
use ApiPlatform\Metadata\Put;
2228
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2329
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2430
use Illuminate\Database\Eloquent\Model;
31+
use Illuminate\Support\Facades\Gate;
2532

2633
final class EloquentResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
2734
{
35+
private const POLICY_METHODS = [
36+
Put::class => 'update',
37+
Post::class => 'create',
38+
Get::class => 'view',
39+
GetCollection::class => 'viewAny',
40+
Delete::class => 'delete',
41+
Patch::class => 'update',
42+
];
43+
2844
public function __construct(
2945
private readonly ResourceMetadataCollectionFactoryInterface $decorated,
3046
) {
@@ -55,6 +71,17 @@ public function create(string $resourceClass): ResourceMetadataCollection
5571
$operation = $operation->withProvider($operation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class);
5672
}
5773

74+
if (!$operation->getPolicy() && ($policy = Gate::getPolicyFor($model))) {
75+
$policyMethod = self::POLICY_METHODS[$operation::class] ?? null;
76+
if ($operation instanceof Put && $operation->getAllowCreate()) {
77+
$policyMethod = self::POLICY_METHODS[Post::class];
78+
}
79+
80+
if ($policyMethod && method_exists($policy, $policyMethod)) {
81+
$operation = $operation->withPolicy($policyMethod);
82+
}
83+
}
84+
5885
if (!$operation->getProcessor()) {
5986
$operation = $operation->withProcessor($operation instanceof DeleteOperationInterface ? RemoveProcessor::class : PersistProcessor::class);
6087
}

src/Laravel/Security/ResourceAccessChecker.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@
1313

1414
namespace ApiPlatform\Laravel\Security;
1515

16+
use ApiPlatform\Laravel\Eloquent\Paginator;
1617
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
1718
use Illuminate\Support\Facades\Gate;
1819

1920
class ResourceAccessChecker implements ResourceAccessCheckerInterface
2021
{
2122
public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool
2223
{
23-
return Gate::allows($expression, $extraVariables['object']);
24+
return Gate::allows(
25+
$expression,
26+
$extraVariables['object'] instanceof Paginator ?
27+
$resourceClass :
28+
$extraVariables['object']
29+
);
2430
}
2531
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests\Policy;
15+
16+
use Illuminate\Foundation\Auth\User;
17+
use Workbench\App\Models\Book;
18+
19+
class BookAllowPolicy
20+
{
21+
/**
22+
* Determine whether the user can view any models.
23+
*/
24+
public function viewAny(?User $user): bool
25+
{
26+
return true;
27+
}
28+
29+
/**
30+
* Determine whether the user can view the model.
31+
*/
32+
public function view(?User $user, Book $book): bool
33+
{
34+
return true;
35+
}
36+
37+
/**
38+
* Determine whether the user can create models.
39+
*/
40+
public function create(?User $user): bool
41+
{
42+
return true;
43+
}
44+
45+
/**
46+
* Determine whether the user can update the model.
47+
*/
48+
public function update(?User $user, Book $book): bool
49+
{
50+
return true;
51+
}
52+
53+
/**
54+
* Determine whether the user can delete the model.
55+
*/
56+
public function delete(?User $user, Book $book): bool
57+
{
58+
return true;
59+
}
60+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests\Policy;
15+
16+
use Illuminate\Foundation\Auth\User;
17+
use Workbench\App\Models\Book;
18+
19+
class BookDenyPolicy
20+
{
21+
/**
22+
* Determine whether the user can view any models.
23+
*/
24+
public function viewAny(?User $user): bool
25+
{
26+
return false;
27+
}
28+
29+
/**
30+
* Determine whether the user can view the model.
31+
*/
32+
public function view(?User $user, Book $book): bool
33+
{
34+
return false;
35+
}
36+
37+
/**
38+
* Determine whether the user can create models.
39+
*/
40+
public function create(?User $user): bool
41+
{
42+
return false;
43+
}
44+
45+
/**
46+
* Determine whether the user can update the model.
47+
*/
48+
public function update(?User $user, Book $book): bool
49+
{
50+
return false;
51+
}
52+
53+
/**
54+
* Determine whether the user can delete the model.
55+
*/
56+
public function delete(?User $user, Book $book): bool
57+
{
58+
return false;
59+
}
60+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests\Policy;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Illuminate\Contracts\Config\Repository;
18+
use Illuminate\Foundation\Application;
19+
use Illuminate\Foundation\Testing\RefreshDatabase;
20+
use Illuminate\Support\Facades\Gate;
21+
use Orchestra\Testbench\Concerns\WithWorkbench;
22+
use Orchestra\Testbench\TestCase;
23+
use Workbench\App\Models\Author;
24+
use Workbench\App\Models\Book;
25+
26+
class PolicyAllowTest extends TestCase
27+
{
28+
use ApiTestAssertionsTrait;
29+
use RefreshDatabase;
30+
use WithWorkbench;
31+
32+
/**
33+
* @param Application $app
34+
*/
35+
protected function defineEnvironment($app): void
36+
{
37+
Gate::guessPolicyNamesUsing(function (string $modelClass) {
38+
return Book::class === $modelClass ?
39+
BookAllowPolicy::class :
40+
null;
41+
});
42+
43+
tap($app['config'], function (Repository $config): void {
44+
$config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]);
45+
$config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]);
46+
});
47+
}
48+
49+
public function testGetCollection(): void
50+
{
51+
$response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]);
52+
$response->assertStatus(200);
53+
}
54+
55+
public function testGetEmptyColelction(): void
56+
{
57+
$response = $this->get('/api/books?publicationDate[gt]=9999-12-31', ['accept' => ['application/vnd.api+json']]);
58+
$response->assertStatus(200);
59+
$response->assertJsonFragment([
60+
'meta' => [
61+
'totalItems' => 0,
62+
'itemsPerPage' => 5,
63+
'currentPage' => 1,
64+
],
65+
]);
66+
}
67+
68+
public function testGetBook(): void
69+
{
70+
$book = Book::first();
71+
$iri = $this->getIriFromResource($book);
72+
$response = $this->get($iri, ['accept' => ['application/vnd.api+json']]);
73+
$response->assertStatus(200);
74+
}
75+
76+
public function testCreateBook(): void
77+
{
78+
$author = Author::find(1);
79+
$response = $this->postJson(
80+
'/api/books',
81+
[
82+
'data' => [
83+
'attributes' => [
84+
'name' => 'Don Quichotte',
85+
'isbn' => fake()->isbn13(),
86+
'publicationDate' => fake()->optional()->date(),
87+
],
88+
'relationships' => [
89+
'author' => [
90+
'data' => [
91+
'id' => $this->getIriFromResource($author),
92+
'type' => 'Author',
93+
],
94+
],
95+
],
96+
],
97+
],
98+
[
99+
'accept' => 'application/vnd.api+json',
100+
'content_type' => 'application/vnd.api+json',
101+
]
102+
);
103+
104+
$response->assertStatus(201);
105+
}
106+
107+
public function testUpdateBook(): void
108+
{
109+
$book = Book::first();
110+
$iri = $this->getIriFromResource($book);
111+
$response = $this->putJson(
112+
$iri,
113+
[
114+
'data' => ['attributes' => ['name' => 'updated title']],
115+
],
116+
[
117+
'accept' => 'application/vnd.api+json',
118+
'content_type' => 'application/vnd.api+json',
119+
]
120+
);
121+
$response->assertStatus(200);
122+
}
123+
124+
public function testPatchBook(): void
125+
{
126+
$book = Book::first();
127+
$iri = $this->getIriFromResource($book);
128+
$response = $this->patchJson(
129+
$iri,
130+
[
131+
'name' => 'updated title',
132+
],
133+
[
134+
'accept' => 'application/vnd.api+json',
135+
'content_type' => 'application/merge-patch+json',
136+
]
137+
);
138+
$response->assertStatus(200);
139+
}
140+
141+
public function testDeleteBook(): void
142+
{
143+
$book = Book::first();
144+
$iri = $this->getIriFromResource($book);
145+
$response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']);
146+
$response->assertStatus(204);
147+
$this->assertNull(Book::find($book->id));
148+
}
149+
}

0 commit comments

Comments
 (0)