Skip to content

Commit ad613d1

Browse files
authored
Feature/gates with message (#195)
* Support custom gate denial messages in authorization Refactored authorization logic to use Gate::inspect and return Response objects, allowing custom denial messages from policies. Updated Response handling to extract messages or allowed status for gates, and added a test and policy to verify custom messages are returned for denied actions.
1 parent d52b512 commit ad613d1

File tree

5 files changed

+250
-20
lines changed

5 files changed

+250
-20
lines changed

config/rest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
'gates' => [
1515
'enabled' => true,
1616
'key' => 'gates',
17+
'message' => [
18+
'enabled' => false,
19+
],
1720
// Here you can customize the keys for each gate
1821
'names' => [
1922
'authorized_to_view' => 'authorized_to_view',

src/Concerns/Authorizable.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ public function authorizeTo($ability, $model)
5858
* @param string $ability
5959
* @param Model|string $model
6060
*
61-
* @return bool
61+
* @return Response
6262
*/
6363
public function authorizedTo($ability, $model)
6464
{
6565
if ($this->isAuthorizingEnabled()) {
6666
$resolver = function () use ($ability, $model) {
67-
return Gate::check($ability, $model);
67+
return Gate::inspect($ability, $model);
6868
};
6969

7070
if ($this->isAuthorizationCacheEnabled()) {
@@ -86,15 +86,15 @@ public function authorizedTo($ability, $model)
8686
return $resolver();
8787
}
8888

89-
return true;
89+
return Response::allow();
9090
}
9191

9292
/**
9393
* Determine if the current user has a given ability.
9494
*
9595
* @param string $ability
96-
* * @param Model $model
97-
* * @param string $toActionModel
96+
* @param Model $model
97+
* @param string $toActionModel
9898
*
9999
* @throws \Illuminate\Auth\Access\AuthorizationException
100100
*

src/Http/Response.php

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,38 @@ public function resource(Resource $resource)
3030
});
3131
}
3232

33-
protected function buildGatesForModel(Model $model, Resource $resource, array $gates)
33+
protected function buildGatesForModel(Model|string $model, Resource $resource, array $gates)
3434
{
35-
return array_merge(
36-
in_array('view', $gates) ? [config('rest.gates.names.authorized_to_view') => $resource->authorizedTo('view', $model)] : [],
37-
in_array('update', $gates) ? [config('rest.gates.names.authorized_to_update') => $resource->authorizedTo('update', $model)] : [],
38-
in_array('delete', $gates) ? [config('rest.gates.names.authorized_to_delete') => $resource->authorizedTo('delete', $model)] : [],
39-
in_array('restore', $gates) ? [config('rest.gates.names.authorized_to_restore') => $resource->authorizedTo('restore', $model)] : [],
40-
in_array('forceDelete', $gates) ? [config('rest.gates.names.authorized_to_force_delete') => $resource->authorizedTo('forceDelete', $model)] : [],
41-
);
35+
$nameMap = [
36+
'create' => config('rest.gates.names.authorized_to_create'),
37+
'view' => config('rest.gates.names.authorized_to_view'),
38+
'update' => config('rest.gates.names.authorized_to_update'),
39+
'delete' => config('rest.gates.names.authorized_to_delete'),
40+
'restore' => config('rest.gates.names.authorized_to_restore'),
41+
'forceDelete' => config('rest.gates.names.authorized_to_force_delete'),
42+
];
43+
44+
$result = [];
45+
46+
if (config('rest.gates.message.enabled', false)) {
47+
foreach ($gates as $gate) {
48+
if (isset($nameMap[$gate])) {
49+
$authorizedTo = $resource->authorizedTo($gate, $model);
50+
$result[$nameMap[$gate]]['allowed'] = $authorizedTo->allowed();
51+
$result[$nameMap[$gate]]['message'] = $authorizedTo->message();
52+
}
53+
}
54+
} else {
55+
trigger_deprecation('lomkit/laravel-rest-api', '2.17.0', 'In Laravel Rest Api 3 it won\'t be possible to use the old gate schema, please upgrade as quickly as possible. See: https://laravel-rest-api.lomkit.com/digging-deeper/gates#policy-message-in-gates');
56+
foreach ($gates as $gate) {
57+
if (isset($nameMap[$gate])) {
58+
$authorizedTo = $resource->authorizedTo($gate, $model);
59+
$result[$nameMap[$gate]] = $authorizedTo->allowed();
60+
}
61+
}
62+
}
63+
64+
return $result;
4265
}
4366

4467
/**
@@ -77,9 +100,11 @@ public function modelToResponse(Model $model, Resource $resource, array $request
77100
)
78101
)
79102
->when($resource->isGatingEnabled() && isset($currentRequestArray['gates']), function ($attributes) use ($currentRequestArray, $resource, $model) {
103+
$currentRequestArrayWithoutCreate = collect($currentRequestArray['gates'])->reject(fn ($value) => $value === 'create')->toArray();
104+
80105
return $attributes->put(
81106
config('rest.gates.key'),
82-
$this->buildGatesForModel($model, $resource, $currentRequestArray['gates'])
107+
$this->buildGatesForModel($model, $resource, $currentRequestArrayWithoutCreate)
83108
);
84109
})
85110
->toArray(),
@@ -133,9 +158,7 @@ public function toResponse($request)
133158
$this->responsable->currentPage(),
134159
$this->responsable->getOptions(),
135160
$this->resource->isGatingEnabled() && in_array('create', $request->input('search.gates', [])) ? [
136-
config('rest.gates.key') => [
137-
config('rest.gates.names.authorized_to_create') => $this->resource->authorizedTo('create', $this->resource::newModel()::class),
138-
],
161+
config('rest.gates.key') => $this->buildGatesForModel($this->resource::newModel()::class, $this->resource, ['create']),
139162
] : []
140163
);
141164

@@ -154,9 +177,7 @@ public function toResponse($request)
154177
'data' => $data ?? $this->map($this->responsable, $this->modelToResponse($this->responsable, $this->resource, $request->input('search', []))),
155178
'meta' => array_merge(
156179
$this->resource->isGatingEnabled() && in_array('create', $request->input('search.gates', [])) ? [
157-
config('rest.gates.key') => [
158-
config('rest.gates.names.authorized_to_create') => $this->resource->authorizedTo('create', $this->resource::newModel()::class),
159-
],
180+
config('rest.gates.key') => $this->buildGatesForModel($this->resource::newModel()::class, $this->resource, ['create']),
160181
] : []
161182
),
162183
];

tests/Feature/Controllers/AutomaticGatingTest.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Lomkit\Rest\Tests\Support\Policies\DeletePolicy;
1616
use Lomkit\Rest\Tests\Support\Policies\ForceDeletePolicy;
1717
use Lomkit\Rest\Tests\Support\Policies\GreenPolicy;
18+
use Lomkit\Rest\Tests\Support\Policies\RedPolicyWithMessage;
1819
use Lomkit\Rest\Tests\Support\Policies\RestorePolicy;
1920
use Lomkit\Rest\Tests\Support\Policies\UpdatePolicy;
2021
use Lomkit\Rest\Tests\Support\Policies\ViewPolicy;
@@ -62,6 +63,124 @@ public function test_searching_automatic_gated_resource(): void
6263
);
6364
}
6465

66+
public function test_searching_automatic_gated_resource_and_custom_message(): void
67+
{
68+
$model = ModelFactory::new()
69+
->create();
70+
71+
Gate::policy(Model::class, RedPolicyWithMessage::class);
72+
73+
config(['rest.gates.message.enabled' => true]);
74+
75+
$response = $this->post(
76+
'/api/automatic-gating/search',
77+
[
78+
'search' => [
79+
'gates' => ['create', 'view', 'update', 'delete', 'forceDelete', 'restore'],
80+
],
81+
],
82+
['Accept' => 'application/json']
83+
);
84+
85+
$this->assertResourcePaginated(
86+
$response,
87+
[$model],
88+
new AutomaticGatingResource(),
89+
[
90+
[
91+
'gates' => [
92+
'authorized_to_view' => [
93+
'allowed' => false,
94+
'message' => 'You don\'t have permission to view user',
95+
],
96+
'authorized_to_update' => [
97+
'allowed' => false,
98+
'message' => 'You don\'t have permission to update user',
99+
],
100+
'authorized_to_delete' => [
101+
'allowed' => false,
102+
'message' => 'You don\'t have permission to delete user',
103+
],
104+
'authorized_to_restore' => [
105+
'allowed' => false,
106+
'message' => 'You don\'t have permission to restore user',
107+
],
108+
'authorized_to_force_delete' => [
109+
'allowed' => false,
110+
'message' => 'You don\'t have permission to force delete user',
111+
],
112+
],
113+
],
114+
]
115+
);
116+
$response->assertJsonPath(
117+
'meta.gates.authorized_to_create',
118+
[
119+
'allowed' => false,
120+
'message' => 'You don\'t have permission to create user',
121+
]
122+
);
123+
}
124+
125+
public function test_searching_automatic_gated_resource_with_allowed_gates_and_custom_message(): void
126+
{
127+
$model = ModelFactory::new()
128+
->create();
129+
130+
Gate::policy(Model::class, GreenPolicy::class);
131+
132+
config(['rest.gates.message.enabled' => true]);
133+
134+
$response = $this->post(
135+
'/api/automatic-gating/search',
136+
[
137+
'search' => [
138+
'gates' => ['create', 'view', 'update', 'delete', 'forceDelete', 'restore'],
139+
],
140+
],
141+
['Accept' => 'application/json']
142+
);
143+
144+
$this->assertResourcePaginated(
145+
$response,
146+
[$model],
147+
new AutomaticGatingResource(),
148+
[
149+
[
150+
'gates' => [
151+
'authorized_to_view' => [
152+
'allowed' => true,
153+
'message' => null,
154+
],
155+
'authorized_to_update' => [
156+
'allowed' => true,
157+
'message' => null,
158+
],
159+
'authorized_to_delete' => [
160+
'allowed' => true,
161+
'message' => null,
162+
],
163+
'authorized_to_restore' => [
164+
'allowed' => true,
165+
'message' => null,
166+
],
167+
'authorized_to_force_delete' => [
168+
'allowed' => true,
169+
'message' => null,
170+
],
171+
],
172+
],
173+
]
174+
);
175+
$response->assertJsonPath(
176+
'meta.gates.authorized_to_create',
177+
[
178+
'allowed' => true,
179+
'message' => null,
180+
]
181+
);
182+
}
183+
65184
public function test_searching_automatic_gated_resource_with_global_config_disabled(): void
66185
{
67186
$model = ModelFactory::new()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace Lomkit\Rest\Tests\Support\Policies;
4+
5+
use Illuminate\Auth\Access\HandlesAuthorization;
6+
use Illuminate\Auth\Access\Response;
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class RedPolicyWithMessage
10+
{
11+
use HandlesAuthorization;
12+
13+
/**
14+
* Determine whether the user can view the list of models.
15+
*
16+
* @param $user
17+
*/
18+
public function viewAny($user)
19+
{
20+
return true;
21+
}
22+
23+
/**
24+
* Determine whether the user can view the model.
25+
*
26+
* @param $user
27+
* @param Model $model
28+
*/
29+
public function view($user, Model $model)
30+
{
31+
return Response::deny('You don\'t have permission to view user');
32+
}
33+
34+
/**
35+
* Determine whether the user can create models.
36+
*
37+
* @param $user
38+
*/
39+
public function create($user)
40+
{
41+
return Response::deny('You don\'t have permission to create user');
42+
}
43+
44+
/**
45+
* Determine whether the user can update the model.
46+
*
47+
* @param $user
48+
* @param Model $model
49+
*/
50+
public function update($user, Model $model)
51+
{
52+
return Response::deny('You don\'t have permission to update user');
53+
}
54+
55+
/**
56+
* Determine whether the user can delete the model.
57+
*
58+
* @param $user
59+
* @param Model $model
60+
*/
61+
public function delete($user, Model $model)
62+
{
63+
return Response::deny('You don\'t have permission to delete user');
64+
}
65+
66+
/**
67+
* Determine whether the user can restore the model.
68+
*
69+
* @param $user
70+
* @param Model $model
71+
*/
72+
public function restore($user, Model $model)
73+
{
74+
return Response::deny('You don\'t have permission to restore user');
75+
}
76+
77+
/**
78+
* Determine whether the user can permanently delete the model.
79+
*
80+
* @param $user
81+
* @param Model $model
82+
*/
83+
public function forceDelete($user, Model $model)
84+
{
85+
return Response::deny('You don\'t have permission to force delete user');
86+
}
87+
}

0 commit comments

Comments
 (0)