From f49f0aa9b21573db044764000ed2174ac485a59b Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Thu, 14 Dec 2023 19:04:41 +0100 Subject: [PATCH] add user permissions and tests Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/UserController.php | 36 ++ app/Permissions/UsersPermissions.php | 29 + ...2023_12_13_214743_add_user_permissions.php | 71 +++ .../Http/Controllers/UserControllerTest.php | 505 +++++++++++++++++- 4 files changed, 625 insertions(+), 16 deletions(-) create mode 100644 app/Permissions/UsersPermissions.php create mode 100644 database/migrations/2023_12_13_214743_add_user_permissions.php diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d8522173..c0d4503c 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -2,13 +2,36 @@ namespace App\Http\Controllers; +use App\Http\Responses\ApiErrorResponse; use App\Http\Responses\ApiSuccessResponse; use App\Models\User; +use App\Permissions\UsersPermissions; use Illuminate\Http\Request; use Illuminate\Http\Response; class UserController extends Controller { + + /** + * UserController constructor. + */ + public function __construct() + { + /** + * Permissions: + * - index: list-users + * - store: create-users + * - show: show-users || list-users + * - update-users || update-users-self + * - destroy: delete-users + */ + $this->middleware('permission:'.UsersPermissions::CAN_LIST_USERS)->only('index'); + $this->middleware('permission:'.UsersPermissions::CAN_LIST_USERS.'|'.UsersPermissions::CAN_SHOW_USERS)->only('show'); + $this->middleware('permission:'.UsersPermissions::CAN_CREATE_USERS)->only('store'); + $this->middleware('permission:'.UsersPermissions::CAN_UPDATE_USERS.'|'.UsersPermissions::CAN_UPDATE_USERS_SELF)->only('update'); + $this->middleware('permission:'.UsersPermissions::CAN_DELETE_USERS)->only('destroy'); + } + /** * Display a listing of the resource. */ @@ -42,6 +65,12 @@ public function store(Request $request) */ public function show(User $user) { + /** @var User $authUser */ + $authUser = auth()->user(); + + if(!$authUser->checkPermissionTo(UsersPermissions::CAN_LIST_USERS) && !$authUser->is($user)) { + return new ApiErrorResponse("You can only view your own user.", status: Response::HTTP_FORBIDDEN); + } return new ApiSuccessResponse($user); } @@ -56,6 +85,13 @@ public function update(Request $request, User $user) 'password' => 'sometimes|required|min:8|confirmed', ]); + /** @var User $authUser */ + $authUser = auth()->user(); + + if($authUser->checkPermissionTo(UsersPermissions::CAN_UPDATE_USERS_SELF) && !$authUser->is($user)) { + return new ApiErrorResponse("You can only update your own user.", status: Response::HTTP_FORBIDDEN); + } + $user->update($validated); return new ApiSuccessResponse($user); diff --git a/app/Permissions/UsersPermissions.php b/app/Permissions/UsersPermissions.php new file mode 100644 index 00000000..7291619e --- /dev/null +++ b/app/Permissions/UsersPermissions.php @@ -0,0 +1,29 @@ +insert( + [ + [ + 'name' => UsersPermissions::CAN_LIST_USERS, + 'guard_name' => 'web', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ], + [ + 'name' => UsersPermissions::CAN_SHOW_USERS, + 'guard_name' => 'web', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ], + [ + 'name' => UsersPermissions::CAN_CREATE_USERS, + 'guard_name' => 'web', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ], + [ + 'name' => UsersPermissions::CAN_UPDATE_USERS, + 'guard_name' => 'web', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ], + [ + 'name' => UsersPermissions::CAN_UPDATE_USERS_SELF, + 'guard_name' => 'web', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ], + [ + 'name' => UsersPermissions::CAN_DELETE_USERS, + 'guard_name' => 'web', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ] + ] + ); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('permissions')->whereIn('name', [ + UsersPermissions::CAN_LIST_USERS, + UsersPermissions::CAN_SHOW_USERS, + UsersPermissions::CAN_CREATE_USERS, + UsersPermissions::CAN_UPDATE_USERS, + UsersPermissions::CAN_UPDATE_USERS_SELF, + UsersPermissions::CAN_DELETE_USERS, + ])->delete(); + } +}; diff --git a/tests/Feature/Http/Controllers/UserControllerTest.php b/tests/Feature/Http/Controllers/UserControllerTest.php index a706a7b4..5a35ec81 100644 --- a/tests/Feature/Http/Controllers/UserControllerTest.php +++ b/tests/Feature/Http/Controllers/UserControllerTest.php @@ -3,28 +3,37 @@ namespace Tests\Feature\Http\Controllers; use App\Models\User; +use App\Permissions\UsersPermissions; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; use Tests\TestCase; +/** + * @coversDefaultClass \App\Http\Controllers\UserController + */ class UserControllerTest extends TestCase { use RefreshDatabase; /** - * Test the index method. + * Test that the index method list users with minimal permission. + * + * @group UserController.Index + * @covers ::index */ - public function test_users_index(): void + public function test_index_users_with_minimal_permission(): void { // Create some dummy users User::factory()->count(10)->create(); Sanctum::actingAs( - User::factory()->create() + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_LIST_USERS + ]) ); // Send a GET request to the index endpoint - $response = $this->get('/api/v1/users'); + $response = $this->getJson('/api/v1/users'); // Assert that the response has a successful status code $response->assertStatus(200); @@ -34,14 +43,70 @@ public function test_users_index(): void } /** - * Test the store method. + * Test that the index method don't list users with unauthorized user permissions. + * + * @group UserController.Index + * @covers ::index + */ + public function test_not_index_users_with_unauthorized_user_permissions(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_SHOW_USERS, + UsersPermissions::CAN_CREATE_USERS, + UsersPermissions::CAN_UPDATE_USERS, + UsersPermissions::CAN_UPDATE_USERS_SELF, + UsersPermissions::CAN_DELETE_USERS, + ]) + ); + + // Send a GET request to the index endpoint + $response = $this->getJson('/api/v1/users'); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the index method without permission. + * + * @group UserController.Index + * @covers ::index */ - public function test_create_user(): void + public function test_not_index_users_without_any_permission(): void { Sanctum::actingAs( User::factory()->create() ); + // Send a GET request to the index endpoint + $response = $this->getJson('/api/v1/users'); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test that the store method creates users with minimal permission. + * + * @group UserController.Store + * @covers ::store + */ + public function test_store_user_with_minimal_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_CREATE_USERS, + ]) + ); + + // Array of user data to create $userData = [ 'name' => 'John Doe', 'email' => 'john.doe@example.com', @@ -50,9 +115,9 @@ public function test_create_user(): void ]; // Send a POST request to the store endpoint - $response = $this->post('/api/v1/users', $userData); + $response = $this->postJson('/api/v1/users', $userData); - // Assert that the response has a successful status code + // Assert that the response has a created status code $response->assertStatus(201); // Assert that the database has the user @@ -69,14 +134,96 @@ public function test_create_user(): void } /** - * Test the show method. + * Test the store method with unauthorized permission. + * + * @group UserController.Store + * @covers ::store + */ + + public function test_not_store_user_with_unauthorized_user_permissions(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_LIST_USERS, + UsersPermissions::CAN_SHOW_USERS, + UsersPermissions::CAN_UPDATE_USERS, + UsersPermissions::CAN_UPDATE_USERS_SELF, + UsersPermissions::CAN_DELETE_USERS, + ]) + ); + + $userData = [ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]; + + // Send a POST request to the store endpoint + $response = $this->postJson('/api/v1/users', $userData); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the database did not create the user + $this->assertDatabaseMissing('users', [ + 'name' => $userData['name'], + 'email' => $userData['email'], + ]); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the store method without permission. + * + * @group UserController.Store + * @covers ::store */ - public function test_users_show(): void + public function test_not_store_user_without_any_permission(): void { Sanctum::actingAs( User::factory()->create() ); + $userData = [ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]; + + // Send a POST request to the store endpoint + $response = $this->postJson('/api/v1/users', $userData); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the database did not create the user + $this->assertDatabaseMissing('users', [ + 'name' => $userData['name'], + 'email' => $userData['email'], + ]); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the show method for other users + * + * @group UserController.Show + * @covers ::show + */ + public function test_show_other_users_with_list_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_LIST_USERS, + ]) + ); + $user = User::factory()->create(); // Send a GET request to the show endpoint @@ -90,9 +237,100 @@ public function test_users_show(): void } /** - * Test the update method. + * Test the show method for self. + * + * @group UserController.Show + * @covers ::show */ - public function test_users_update(): void + public function test_show_users_self_with_show_permission(): void + { + $user = User::factory()->create(); + + Sanctum::actingAs($user->givePermissionTo([ + UsersPermissions::CAN_SHOW_USERS, + ])); + + $user = User::find($user->id); + + // Send a GET request to the show endpoint + $response = $this->getJson('/api/v1/users/' . $user->id); + + // Assert that the response has a successful status code + $response->assertStatus(200); + + // Assert that the response contains the user data + $response->assertJsonFragment($user->toArray()); + } + + + /** + * Test the show method for other users with self permission. + * + * @group UserController.Show + * @covers ::show + */ + public function test_not_show_other_users_with_self_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_SHOW_USERS, + ]) + ); + + $user = User::factory()->create(); + + // Send a GET request to the show endpoint + $response = $this->get('/api/v1/users/' . $user->id); + + // Assert that the response has a successful status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'You can only view your own user.']); + } + + /** + * Test the show method with unauthorized user permissions. + * + * @group UserController.Show + * @covers ::show + */ + public function test_not_show_other_users_with_unauthorized_user_permissions(): void + { + $unauthorizedPerms = [ + UsersPermissions::CAN_CREATE_USERS, + UsersPermissions::CAN_UPDATE_USERS, + UsersPermissions::CAN_UPDATE_USERS_SELF, + UsersPermissions::CAN_DELETE_USERS, + ]; + + foreach ($unauthorizedPerms as $perm) { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + $perm, + ]) + ); + + $user = User::factory()->create(); + + // Send a GET request to the show endpoint + $response = $this->getJson('/api/v1/users/' . $user->id); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + } + + /** + * Test the show method without permission. + * + * @group UserController.Show + * @covers ::show + */ + public function test_not_show_users_without_any_permission(): void { Sanctum::actingAs( User::factory()->create() @@ -100,18 +338,43 @@ public function test_users_update(): void $user = User::factory()->create(); + // Send a GET request to the show endpoint + $response = $this->getJson('/api/v1/users/' . $user->id); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the update method for self. + * + * @group UserController.Update + * @covers ::update + */ + public function test_update_users_self_with_self_permission(): void + { + $user = User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_UPDATE_USERS_SELF, + ]); + + Sanctum::actingAs($user); + $userData = [ 'name' => 'Updated Name', 'email' => 'updated.email@example.com', ]; - // Send a PATCH request to the update endpoint - $response = $this->put('/api/v1/users/' . $user->id, $userData); + // Send a PUT request to the update endpoint + $response = $this->putJson('/api/v1/users/' . $user->id, $userData); // Assert that the response has a successful status code $response->assertStatus(200); $this->assertDatabaseHas('users', [ + 'id' => $user->id, 'name' => $userData['name'], 'email' => $userData['email'], ]); @@ -121,9 +384,126 @@ public function test_users_update(): void } /** - * Test the destroy method. + * Test the update method for other users with self permission. + * + * @group UserController.Update + * @covers ::update + */ + public function test_update_other_users_with_update_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_UPDATE_USERS, + ]) + ); + + $user = User::factory()->create(); + + $userData = [ + 'name' => 'Updated Name', + 'email' => 'updated.email@example.com', + ]; + + // Send a PATCH request to the update endpoint + $response = $this->putJson('/api/v1/users/' . $user->id, $userData); + + // Assert that the response has a unauthorized status code + $response->assertStatus(200); + + // Assert that the database did not update the user + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'name' => $userData['name'], + 'email' => $userData['email'], + ]); + + // Assert that the response contains the error message + $response->assertJsonFragment($userData); + } + + /** + * Test the update method for other users with self permission. + * + * @group UserController.Update + * @covers ::update + */ + public function test_not_update_other_users_with_self_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_UPDATE_USERS_SELF + ]) + ); + + $user = User::factory()->create(); + + $userData = [ + 'name' => 'Updated Name', + 'email' => 'updated.email@example.com', + ]; + + // Send a PATCH request to the update endpoint + $response = $this->putJson('/api/v1/users/' . $user->id, $userData); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the database did not update the user + $this->assertDatabaseMissing('users', [ + 'id' => $user->id, + 'name' => $userData['name'], + 'email' => $userData['email'], + ]); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'You can only update your own user.']); + } + + /** + * Test the update method for other users with unauthorized user permissions. + */ + public function test_not_update_other_users_with_unauthorized_user_permissions(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_LIST_USERS, + UsersPermissions::CAN_SHOW_USERS, + UsersPermissions::CAN_CREATE_USERS, + UsersPermissions::CAN_DELETE_USERS, + ]) + ); + + $user = User::factory()->create(); + + $userData = [ + 'name' => 'Updated Name', + 'email' => 'updated.email@example.com', + ]; + + // Send a PATCH request to the update endpoint + $response = $this->putJson('/api/v1/users/' . $user->id, $userData); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the database did not update the user + $this->assertDatabaseMissing('users', [ + 'id' => $user->id, + 'name' => $userData['name'], + 'email' => $userData['email'], + ]); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the update method without any permission. + * + * @group UserController.Update + * @covers ::update */ - public function test_users_destroy(): void + public function test_not_update_users_without_any_permission(): void { Sanctum::actingAs( User::factory()->create() @@ -131,6 +511,44 @@ public function test_users_destroy(): void $user = User::factory()->create(); + $userData = [ + 'name' => 'Updated Name', + 'email' => 'updated.email@example.com', + ]; + + // Send a PATCH request to the update endpoint + $response = $this->putJson('/api/v1/users/' . $user->id, $userData); + + // Assert that the response has a unauthorized status code + $response->assertStatus(403); + + // Assert that the database did not update the user + $this->assertDatabaseMissing('users', [ + 'id' => $user->id, + 'name' => $userData['name'], + 'email' => $userData['email'], + ]); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the destroy method with minimal permission. + * + * @group UserController.Destroy + * @covers ::destroy + */ + public function test_destroy_users_with_minimal_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_DELETE_USERS, + ]) + ); + + $user = User::factory()->create(); + // Send a DELETE request to the destroy endpoint $response = $this->delete('/api/v1/users/' . $user->id); @@ -140,4 +558,59 @@ public function test_users_destroy(): void // Assert that the response contains the success message $response->assertJsonFragment(['data' => 'User deleted successfully.']); } + + /** + * Test the destroy method with unauthorized user permissions. + * + * @group UserController.Destroy + * @covers ::destroy + */ + public function test_not_destroy_users_with_unauthorized_user_permissions(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + UsersPermissions::CAN_LIST_USERS, + UsersPermissions::CAN_SHOW_USERS, + UsersPermissions::CAN_CREATE_USERS, + UsersPermissions::CAN_UPDATE_USERS, + UsersPermissions::CAN_UPDATE_USERS_SELF, + ]) + ); + + $user = User::factory()->create(); + + // Send a DELETE request to the destroy endpoint + $response = $this->deleteJson('/api/v1/users/' . $user->id); + + // Assert that the response has a successful status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + + /** + * Test the destroy method without permission. + * + * @group UserController.Destroy + * @covers ::destroy + */ + public function test_users_destroy_without_any_permission(): void + { + Sanctum::actingAs( + User::factory()->create() + ); + + $user = User::factory()->create(); + + // Send a DELETE request to the destroy endpoint + $response = $this->deleteJson('/api/v1/users/' . $user->id); + + // Assert that the response has a successful status code + $response->assertStatus(403); + + // Assert that the response contains the error message + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } }