diff --git a/config/rest.php b/config/rest.php index db0ce45..f58b5de 100644 --- a/config/rest.php +++ b/config/rest.php @@ -14,6 +14,9 @@ 'gates' => [ 'enabled' => true, 'key' => 'gates', + 'message' => [ + 'enabled' => false, + ], // Here you can customize the keys for each gate 'names' => [ 'authorized_to_view' => 'authorized_to_view', diff --git a/src/Concerns/Authorizable.php b/src/Concerns/Authorizable.php index 07bad2b..76e0015 100644 --- a/src/Concerns/Authorizable.php +++ b/src/Concerns/Authorizable.php @@ -58,13 +58,13 @@ public function authorizeTo($ability, $model) * @param string $ability * @param Model|string $model * - * @return bool + * @return Response */ public function authorizedTo($ability, $model) { if ($this->isAuthorizingEnabled()) { $resolver = function () use ($ability, $model) { - return Gate::check($ability, $model); + return Gate::inspect($ability, $model); }; if ($this->isAuthorizationCacheEnabled()) { @@ -86,15 +86,15 @@ public function authorizedTo($ability, $model) return $resolver(); } - return true; + return Response::allow(); } /** * Determine if the current user has a given ability. * * @param string $ability - * * @param Model $model - * * @param string $toActionModel + * @param Model $model + * @param string $toActionModel * * @throws \Illuminate\Auth\Access\AuthorizationException * diff --git a/src/Http/Response.php b/src/Http/Response.php index 83428fd..97a19b6 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -30,15 +30,38 @@ public function resource(Resource $resource) }); } - protected function buildGatesForModel(Model $model, Resource $resource, array $gates) + protected function buildGatesForModel(Model|string $model, Resource $resource, array $gates) { - return array_merge( - in_array('view', $gates) ? [config('rest.gates.names.authorized_to_view') => $resource->authorizedTo('view', $model)] : [], - in_array('update', $gates) ? [config('rest.gates.names.authorized_to_update') => $resource->authorizedTo('update', $model)] : [], - in_array('delete', $gates) ? [config('rest.gates.names.authorized_to_delete') => $resource->authorizedTo('delete', $model)] : [], - in_array('restore', $gates) ? [config('rest.gates.names.authorized_to_restore') => $resource->authorizedTo('restore', $model)] : [], - in_array('forceDelete', $gates) ? [config('rest.gates.names.authorized_to_force_delete') => $resource->authorizedTo('forceDelete', $model)] : [], - ); + $nameMap = [ + 'create' => config('rest.gates.names.authorized_to_create'), + 'view' => config('rest.gates.names.authorized_to_view'), + 'update' => config('rest.gates.names.authorized_to_update'), + 'delete' => config('rest.gates.names.authorized_to_delete'), + 'restore' => config('rest.gates.names.authorized_to_restore'), + 'forceDelete' => config('rest.gates.names.authorized_to_force_delete'), + ]; + + $result = []; + + if (config('rest.gates.message.enabled', false)) { + foreach ($gates as $gate) { + if (isset($nameMap[$gate])) { + $authorizedTo = $resource->authorizedTo($gate, $model); + $result[$nameMap[$gate]]['allowed'] = $authorizedTo->allowed(); + $result[$nameMap[$gate]]['message'] = $authorizedTo->message(); + } + } + } else { + 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'); + foreach ($gates as $gate) { + if (isset($nameMap[$gate])) { + $authorizedTo = $resource->authorizedTo($gate, $model); + $result[$nameMap[$gate]] = $authorizedTo->allowed(); + } + } + } + + return $result; } /** @@ -77,9 +100,11 @@ public function modelToResponse(Model $model, Resource $resource, array $request ) ) ->when($resource->isGatingEnabled() && isset($currentRequestArray['gates']), function ($attributes) use ($currentRequestArray, $resource, $model) { + $currentRequestArrayWithoutCreate = collect($currentRequestArray['gates'])->reject(fn ($value) => $value === 'create')->toArray(); + return $attributes->put( config('rest.gates.key'), - $this->buildGatesForModel($model, $resource, $currentRequestArray['gates']) + $this->buildGatesForModel($model, $resource, $currentRequestArrayWithoutCreate) ); }) ->toArray(), @@ -133,9 +158,7 @@ public function toResponse($request) $this->responsable->currentPage(), $this->responsable->getOptions(), $this->resource->isGatingEnabled() && in_array('create', $request->input('search.gates', [])) ? [ - config('rest.gates.key') => [ - config('rest.gates.names.authorized_to_create') => $this->resource->authorizedTo('create', $this->resource::newModel()::class), - ], + config('rest.gates.key') => $this->buildGatesForModel($this->resource::newModel()::class, $this->resource, ['create']), ] : [] ); @@ -154,9 +177,7 @@ public function toResponse($request) 'data' => $data ?? $this->map($this->responsable, $this->modelToResponse($this->responsable, $this->resource, $request->input('search', []))), 'meta' => array_merge( $this->resource->isGatingEnabled() && in_array('create', $request->input('search.gates', [])) ? [ - config('rest.gates.key') => [ - config('rest.gates.names.authorized_to_create') => $this->resource->authorizedTo('create', $this->resource::newModel()::class), - ], + config('rest.gates.key') => $this->buildGatesForModel($this->resource::newModel()::class, $this->resource, ['create']), ] : [] ), ]; diff --git a/tests/Feature/Controllers/AutomaticGatingTest.php b/tests/Feature/Controllers/AutomaticGatingTest.php index 994598a..0d4ff30 100644 --- a/tests/Feature/Controllers/AutomaticGatingTest.php +++ b/tests/Feature/Controllers/AutomaticGatingTest.php @@ -15,6 +15,7 @@ use Lomkit\Rest\Tests\Support\Policies\DeletePolicy; use Lomkit\Rest\Tests\Support\Policies\ForceDeletePolicy; use Lomkit\Rest\Tests\Support\Policies\GreenPolicy; +use Lomkit\Rest\Tests\Support\Policies\RedPolicyWithMessage; use Lomkit\Rest\Tests\Support\Policies\RestorePolicy; use Lomkit\Rest\Tests\Support\Policies\UpdatePolicy; use Lomkit\Rest\Tests\Support\Policies\ViewPolicy; @@ -62,6 +63,124 @@ public function test_searching_automatic_gated_resource(): void ); } + public function test_searching_automatic_gated_resource_and_custom_message(): void + { + $model = ModelFactory::new() + ->create(); + + Gate::policy(Model::class, RedPolicyWithMessage::class); + + config(['rest.gates.message.enabled' => true]); + + $response = $this->post( + '/api/automatic-gating/search', + [ + 'search' => [ + 'gates' => ['create', 'view', 'update', 'delete', 'forceDelete', 'restore'], + ], + ], + ['Accept' => 'application/json'] + ); + + $this->assertResourcePaginated( + $response, + [$model], + new AutomaticGatingResource(), + [ + [ + 'gates' => [ + 'authorized_to_view' => [ + 'allowed' => false, + 'message' => 'You don\'t have permission to view user', + ], + 'authorized_to_update' => [ + 'allowed' => false, + 'message' => 'You don\'t have permission to update user', + ], + 'authorized_to_delete' => [ + 'allowed' => false, + 'message' => 'You don\'t have permission to delete user', + ], + 'authorized_to_restore' => [ + 'allowed' => false, + 'message' => 'You don\'t have permission to restore user', + ], + 'authorized_to_force_delete' => [ + 'allowed' => false, + 'message' => 'You don\'t have permission to force delete user', + ], + ], + ], + ] + ); + $response->assertJsonPath( + 'meta.gates.authorized_to_create', + [ + 'allowed' => false, + 'message' => 'You don\'t have permission to create user', + ] + ); + } + + public function test_searching_automatic_gated_resource_with_allowed_gates_and_custom_message(): void + { + $model = ModelFactory::new() + ->create(); + + Gate::policy(Model::class, GreenPolicy::class); + + config(['rest.gates.message.enabled' => true]); + + $response = $this->post( + '/api/automatic-gating/search', + [ + 'search' => [ + 'gates' => ['create', 'view', 'update', 'delete', 'forceDelete', 'restore'], + ], + ], + ['Accept' => 'application/json'] + ); + + $this->assertResourcePaginated( + $response, + [$model], + new AutomaticGatingResource(), + [ + [ + 'gates' => [ + 'authorized_to_view' => [ + 'allowed' => true, + 'message' => null, + ], + 'authorized_to_update' => [ + 'allowed' => true, + 'message' => null, + ], + 'authorized_to_delete' => [ + 'allowed' => true, + 'message' => null, + ], + 'authorized_to_restore' => [ + 'allowed' => true, + 'message' => null, + ], + 'authorized_to_force_delete' => [ + 'allowed' => true, + 'message' => null, + ], + ], + ], + ] + ); + $response->assertJsonPath( + 'meta.gates.authorized_to_create', + [ + 'allowed' => true, + 'message' => null, + ] + ); + } + public function test_searching_automatic_gated_resource_with_global_config_disabled(): void { $model = ModelFactory::new() diff --git a/tests/Support/Policies/RedPolicyWithMessage.php b/tests/Support/Policies/RedPolicyWithMessage.php new file mode 100644 index 0000000..d48a59b --- /dev/null +++ b/tests/Support/Policies/RedPolicyWithMessage.php @@ -0,0 +1,87 @@ +