Skip to content

Commit de01aae

Browse files
committed
preferencable authorization
1 parent 66c61ba commit de01aae

File tree

16 files changed

+141
-72
lines changed

16 files changed

+141
-72
lines changed

.github/workflows/tests.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,4 @@ jobs:
3939
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}"
4040
composer update
4141
- name: Execute tests
42-
run: vendor/bin/phpunit ./tests
43-
- name: Upload coverage reports to Codecov
44-
uses: codecov/[email protected]
45-
with:
46-
token: ${{ secrets.CODECOV_TOKEN }}
47-
slug: matteoc99/laravel-preference
42+
run: vendor/bin/phpunit ./tests

routes/api.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
->name($name . ".get");
2525
Route::match(['PUT', 'PATCH'], "{scope_id}/$group/{preference}", [PreferenceController::class, 'update'])
2626
->name($name . ".update");
27-
Route::delete("{scope_id}/$group/{preference}", [PreferenceController::class, 'destroy'])
28-
->name($name . ".destroy");
27+
Route::delete("{scope_id}/$group/{preference}", [PreferenceController::class, 'delete'])
28+
->name($name . ".delete");
2929
}
3030
});
3131
}

src/Contracts/PreferenceableModel.php

+13
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace Matteoc99\LaravelPreference\Contracts;
44

5+
use Illuminate\Contracts\Auth\Authenticatable;
56
use Illuminate\Support\Collection;
67
use Illuminate\Validation\ValidationException;
8+
use Matteoc99\LaravelPreference\Enums\PolicyAction;
79
use Matteoc99\LaravelPreference\Exceptions\PreferenceNotFoundException;
810

911
interface PreferenceableModel
@@ -49,4 +51,15 @@ public function setPreference(PreferenceGroup $name, mixed $value): void;
4951
*/
5052
public function getPreference(PreferenceGroup $name, mixed $default = null): mixed;
5153

54+
55+
/**
56+
* check if the user is authorized
57+
*
58+
* @param Authenticatable|null $user
59+
* @param PolicyAction $action
60+
*
61+
* @return bool
62+
*/
63+
public function isUserAuthorized(?Authenticatable $user, PolicyAction $action): bool;
64+
5265
}

src/Enums/PolicyAction.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Matteoc99\LaravelPreference\Enums;
4+
5+
enum PolicyAction
6+
{
7+
case INDEX;
8+
case GET;
9+
case UPDATE;
10+
case DELETE;
11+
}

src/Http/Controllers/PreferenceController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function update(PreferenceUpdateRequest $request): JsonResponse
5555
);
5656
}
5757

58-
public function destroy(Request $request): JsonResponse
58+
public function delete(Request $request): JsonResponse
5959
{
6060
$this->scope->removePreference($this->group);
6161

src/Http/Middlewares/PreferenceMiddleware.php

+8-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
namespace Matteoc99\LaravelPreference\Http\Middlewares;
44

55
use Closure;
6+
use Illuminate\Auth\Access\AuthorizationException;
67
use Illuminate\Http\Request;
78
use Illuminate\Http\Response;
89
use Matteoc99\LaravelPreference\Exceptions\PreferenceNotFoundException;
10+
use Symfony\Component\HttpKernel\Exception\HttpException;
11+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
912

1013
class PreferenceMiddleware
1114
{
@@ -14,11 +17,11 @@ public function handle(Request $request, Closure $next)
1417

1518
/**@var Response $response * */
1619
$response = $next($request);
17-
if ($response->exception) {
18-
return match ($response?->exception::class) {
19-
PreferenceNotFoundException::class => response()->json([
20-
'error' => $response->exception->getMessage()
21-
], 404),
20+
$e = $response?->exception;
21+
if ($e) {
22+
return match ($e::class) {
23+
PreferenceNotFoundException::class => throw new NotFoundHttpException($e->getMessage(), $e),
24+
AuthorizationException::class => throw new HttpException(403, $e->getMessage(), $e),
2225
default => $response,
2326
};
2427
}

src/Traits/HasPreferences.php

+22-13
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace Matteoc99\LaravelPreference\Traits;
44

5+
use Illuminate\Auth\Access\AuthorizationException;
56
use Illuminate\Database\Eloquent\Relations\MorphMany;
67
use Illuminate\Support\Collection;
8+
use Illuminate\Support\Facades\Auth;
79
use Illuminate\Support\Facades\Validator;
810
use Illuminate\Validation\ValidationException;
911
use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;
12+
use Matteoc99\LaravelPreference\Enums\PolicyAction;
1013
use Matteoc99\LaravelPreference\Exceptions\PreferenceNotFoundException;
1114
use Matteoc99\LaravelPreference\Models\Preference;
1215
use Matteoc99\LaravelPreference\Models\UserPreference;
@@ -32,6 +35,7 @@ private function userPreferences(): MorphMany
3235
*/
3336
public function getPreference(PreferenceGroup $name, mixed $default = null): mixed
3437
{
38+
$this->authorize(PolicyAction::GET);
3539
SerializeHelper::conformNameAndGroup($name, $group);
3640
/**@var string $name * */
3741
$preference = $this->validateAndRetrievePreference($name, $group);
@@ -43,19 +47,6 @@ public function getPreference(PreferenceGroup $name, mixed $default = null): mix
4347
return $userPreference?->value ?? $this->getDefaultPreferenceValue($name, $group) ?? $default;
4448
}
4549

46-
/**
47-
* Retrieve the default value for a preference from its configuration.
48-
*
49-
* @param string $name
50-
* @param string $group
51-
*
52-
* @return mixed
53-
*/
54-
private function getDefaultPreferenceValue(string $name, string $group): mixed
55-
{
56-
return Preference::where('group', $group)->where('name', $name)->first()?->default_value ?? null;
57-
}
58-
5950
/**
6051
* Set a preference value, handling validation and persistence.
6152
*
@@ -67,6 +58,7 @@ private function getDefaultPreferenceValue(string $name, string $group): mixed
6758
*/
6859
public function setPreference(PreferenceGroup $name, mixed $value): void
6960
{
61+
$this->authorize(PolicyAction::UPDATE);
7062

7163
SerializeHelper::conformNameAndGroup($name, $group);
7264
/**@var string $name * */
@@ -93,6 +85,8 @@ public function setPreference(PreferenceGroup $name, mixed $value): void
9385
*/
9486
public function removePreference(PreferenceGroup $name): int
9587
{
88+
$this->authorize(PolicyAction::DELETE);
89+
9690
SerializeHelper::conformNameAndGroup($name, $group);
9791
/**@var string $name * */
9892
$preference = $this->validateAndRetrievePreference($name, $group);
@@ -109,6 +103,8 @@ public function removePreference(PreferenceGroup $name): int
109103
*/
110104
public function getPreferences(string $group = null): Collection
111105
{
106+
$this->authorize(PolicyAction::INDEX);
107+
112108
$query = $this->userPreferences()->with('preference');
113109

114110
if ($group) {
@@ -130,4 +126,17 @@ private function validateAndRetrievePreference(string $name, string $group): Pre
130126
}
131127
return $preference;
132128
}
129+
130+
131+
private function getDefaultPreferenceValue(string $name, string $group): mixed
132+
{
133+
return Preference::where('group', $group)->where('name', $name)->first()?->default_value ?? null;
134+
}
135+
136+
private function authorize(PolicyAction $action): void
137+
{
138+
if (!$this->isUserAuthorized(Auth::user(), $action)) {
139+
throw new AuthorizationException("the user is not authorized to perform the action: " . $action->name);
140+
}
141+
}
133142
}

tests/ApiTest/ApiTestCase.php

+1-5
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,12 @@
1616
class ApiTestCase extends TestCase
1717
{
1818

19-
protected Preference $dummyPref;
20-
2119
public function setUp(): void
2220
{
2321
parent::setUp();
2422

25-
$this->dummyPref = PreferenceBuilder::init(General::LANGUAGE)->withRule(new InRule('it', 'en', 'de'))->create();
26-
PreferenceBuilder::init(VideoPreferences::QUALITY, Cast::INT)->withDefaultValue(2)->withRule(new LowerThanRule(5))->create();
23+
PreferenceBuilder::init(General::LANGUAGE)->withRule(new InRule('it', 'en', 'de'))->create();
2724
}
28-
2925
/**
3026
* Define environment setup.
3127
*

tests/ApiTest/DeleteTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Matteoc99\LaravelPreference\Tests\ApiTest;
4+
5+
class DeleteTest extends ApiTestCase
6+
{
7+
/** @test */
8+
public function test_delete_action()
9+
{
10+
11+
$response = $this->delete(route('preferences.user.general.delete', ['scope_id' => 1,'preference' => 'language']));
12+
13+
$response->assertStatus(200);
14+
}
15+
16+
public function test_delete_invalid_scope()
17+
{
18+
$response = $this->delete(route('preferences.user.general.delete', ['scope_id' => 200, 'preference' => 'language']));
19+
20+
$response->assertNotFound();
21+
}
22+
public function test_delete_invalid_permission()
23+
{
24+
$response = $this->delete(route('preferences.user.general.delete', ['scope_id' => 2, 'preference' => 'language']));
25+
26+
$response->assertForbidden();
27+
}
28+
29+
/** @test */
30+
31+
public function test_delete_invalid_pref()
32+
{
33+
$response = $this->delete(route('preferences.user.general.delete', ['scope_id' => 1, 'preference' => 'languageee']));
34+
35+
$response->assertNotFound();
36+
}
37+
}

tests/ApiTest/DestroyTest.php

-31
This file was deleted.

tests/ApiTest/GetTest.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@ public function test_get_action()
1717

1818
public function test_get_invalid_scope()
1919
{
20-
$response = $this->get(route('preferences.user.general.get', ['scope_id' => 2, 'preference' => 'language']));
20+
$response = $this->get(route('preferences.user.general.get', ['scope_id' => 200, 'preference' => 'language']));
2121

2222
$response->assertNotFound();
2323
}
24+
/** @test */
25+
26+
public function test_get_invalid_permission()
27+
{
28+
$response = $this->get(route('preferences.user.general.get', ['scope_id' => 2, 'preference' => 'language']));
29+
30+
$response->assertForbidden();
31+
}
2432

2533
/** @test */
2634
public function test_get_invalid_pref()

tests/ApiTest/IndexTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function test_index_action()
1717

1818
public function test_index_invalid_scope()
1919
{
20-
$response = $this->get(route('preferences.user.general.index', ['scope_id' => 2]));
20+
$response = $this->get(route('preferences.user.general.index', ['scope_id' => 200]));
2121

2222
$response->assertNotFound();
2323
}

tests/ApiTest/UpdateTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function test_update_success_action()
4141

4242
public function test_update_invalid_scope()
4343
{
44-
$response = $this->patch(route('preferences.user.general.update', ['scope_id' => 2, 'preference' => 'language']));
44+
$response = $this->patch(route('preferences.user.general.update', ['scope_id' => 200, 'preference' => 'language']));
4545

4646
$response->assertNotFound();
4747
}

tests/ApiTest/WorkflowTest.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22

33
namespace Matteoc99\LaravelPreference\Tests\ApiTest;
44

5+
use Matteoc99\LaravelPreference\Enums\Cast;
6+
use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
7+
use Matteoc99\LaravelPreference\Rules\InRule;
8+
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
9+
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences;
10+
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\LowerThanRule;
11+
512
class WorkflowTest extends ApiTestCase
613
{
714

815

16+
17+
918
/** @test */
1019
public function test_workflow()
1120
{
@@ -19,8 +28,9 @@ public function test_workflow()
1928
}
2029

2130
/** @test */
22-
public function test_get_and_set()
31+
public function test_int_workflow()
2332
{
33+
PreferenceBuilder::init(VideoPreferences::QUALITY, Cast::INT)->withDefaultValue(2)->withRule(new LowerThanRule(5))->create();
2434

2535
$video = $this->get(route('preferences.user.video.get', ['scope_id' => 1, 'preference' => 'quality']));
2636
$video->assertSuccessful();
@@ -32,6 +42,9 @@ public function test_get_and_set()
3242
]);
3343
$video->assertJson(['value'=>4]);
3444

45+
$video = $this->delete(route('preferences.user.video.delete', ['scope_id' => 1, 'preference' => 'quality']));
46+
47+
$video->assertJson(['value'=>2]);
3548

3649
$video = $this->patch(route('preferences.user.video.update', ['scope_id' => 1, 'preference' => 'quality']),[
3750
'value'=>40

0 commit comments

Comments
 (0)