Courses

Vue.js 3 + Laravel 11 API + Vite: SPA CRUD

Permissions, Roles, Gates: Back-End

Summary of this lesson:
- Setting up roles and permissions models
- Creating permission migrations and seeders
- Implementing Laravel Gates
- Securing API endpoints with permissions

Finally, in this course, we will take care of permission. In this lesson, we will take care of the back-end part. We will create Role and Permission models. Then we will add two roles: admin and editor. The admin role will be able to do everything and the editor will not be able to delete the posts.

network tab


First, we will create the models with migrations for roles and permissions.

php artisan make:model Role -m
php artisan make:model Permission -m

database/migrations/xxxx_create_roles_table.php:

public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

app/Models/Role.php:

class Role extends Model
{
protected $fillable = ['name'];
}

database/migrations/xxxx_create_permissions_table.php:

public function up(): void
{
Schema::create('permissions', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

app/Models/Permission.php:

class permissions extends Model
{
protected $fillable = ['name'];
}

Next, we need to create a pivot table for the many-to-many relationship and add relations to the models.

php artisan make:migration "create permission role table"

database/migrations/xxxx_create_permission_role_table.php:

public function up(): void
{
Schema::create('permission_role', function (Blueprint $table) {
$table->foreignId('permission_id')->constrained();
$table->foreignId('role_id')->constrained();
});
}

app/Models/Role.php:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
protected $fillable = ['name'];
 
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class);
}
}
php artisan make:migration "create role user table"

database/migrations/xxxx_create_role_user_table.php:

public function up(): void
{
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('role_id')->constrained();
$table->foreignId('user_id')->constrained();
});
}

app/Models/User.php:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class User extends Authenticatable
{
// ...
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}

Next, we will create seeders for roles with permissions and attach them.

php artisan make:seeder PermissionSeeder
php artisan make:seeder RoleSeeder
php artisan make:seeder UserSeeder

database/seeders/PermissionSeeder.php:

class PermissionSeeder extends Seeder
{
public function run(): void
{
Permission::create(['name' => 'posts.create']);
Permission::create(['name' => 'posts.update']);
Permission::create(['name' => 'posts.delete']);
}
}

database/seeders/RoleSeeder.php:

class RoleSeeder extends Seeder
{
public function run(): void
{
$admin = Role::create(['name' => 'Administrator']);
$admin->permissions()->attach(Permission::pluck('id'));
 
$editor = Role::create(['name' => 'Editor']);
$editor->permissions()->attach(
Permission::where('name', '!=', 'posts.delete')->pluck('id')
);
}
}

database/seeders/UserSeeder.php:

class UserSeeder extends Seeder
{
public function run(): void
{
$admin = User::factory()->create(['email' => '[email protected]']);
$admin->roles()->attach(Role::where('name', 'Administrator')->value('id'));
 
$editor = User::factory()->create(['email' => '[email protected]']);
$editor->roles()->attach(Role::where('name', 'Editor')->value('id'));
}
}

And lastly, we need to call them in the main DatabaseSeeder.

database/seeders/DatabaseSeeder.php:

class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PermissionSeeder::class,
RoleSeeder::class,
UserSeeder::class,
]);
}
}

Run the migrations and seed the DB.

php artisan migrate --seed

Next, we need to register every permission to the Gate. This can be done in the AppServiceProvider.

app/Providers/AppServiceProvider.php:

use App\Models\Permission;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\QueryException;
use Illuminate\Database\Eloquent\Builder;
 
class AuthServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
try {
foreach (Permission::pluck('name') as $permission) {
Gate::define($permission, function ($user) use ($permission) {
return $user->roles()->whereHas('permissions', function (Builder $q) use ($permission) {
$q->where('name', $permission);
})->exists();
});
}
} catch (QueryException $e) {
 
}
}
}

We define all of the permissions for the gate. The Gate::define accepts closure where we define what needs to be true. In our case, we check if any of the user's roles have the permissions.

The try/catch is needed because at first we don't have a permissions table and even running migrations will result in error message:

Illuminate\Database\QueryException
 
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'project.permissions' doesn't exist (Connection: mysql, SQL: select `name` from `permissions`)
 
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:793
789▕ // If an exception occurs when attempting to run a query, we'll format the error
790▕ // message to include the bindings with SQL, which will make this exception a
791▕ // lot more helpful to the developer instead of just the database's errors.
792▕ catch (Exception $e) {
➜ 793▕ throw new QueryException(
794▕ $this->getName(), $query, $this->prepareBindings($bindings), $e
795▕ );
796▕ }
797▕ }
 
i A table was not found: You might have forgotten to run your database migrations.
https://laravel.com/docs/master/migrations#running-migrations
 
1 [internal]:0
Illuminate\Foundation\Application::Illuminate\Foundation\{closure}()
+13 vendor frames
 
15 app/Providers/AuthServiceProvider.php:29

Now that we have defined gates we can use them in the Controller.

app/Http/Controllers/Api/PostController.php:

use Illuminate\Support\Facades\Gate;
 
class PostController extends Controller
{
// ...
 
public function store(StorePostRequest $request)
{
Gate::authorize('posts.create');
 
if ($request->hasFile('thumbnail')) {
$filename = $request->file('thumbnail')->getClientOriginalName();
info($filename);
}
$post = Post::create($request->validated());
 
return new PostResource($post);
}
 
public function show(Post $post)
{
Gate::authorize('posts.update');
 
return new PostResource($post);
}
 
public function update(Post $post, StorePostRequest $request)
{
Gate::authorize('posts.update');
 
$post->update($request->validated());
 
return new PostResource($post);
}
 
public function destroy(Post $post)
{
Gate::authorize('posts.delete');
 
$post->delete();
 
return response()->noContent();
}
}

Now if you try to delete a post with the user which has editor role, you would get an error alert.

sweetalert error

And in the developer console, in the network tab, you would see the expected result from the response This action is unauthorized.

network tab

Previous: Show User Data and Logout
avatar
Luis Antonio Parrado

According to this tutorial we are defining the Gates but we are not using them in the controllers (or routes) to allow/prohibit the actions.

👍 2
avatar
Luis Antonio Parrado

My solution in app/Http/Controllers/Api/PostController.php:

    // ...
		
    public function store(StorePostRequest $request)
    {
        $this->authorize('posts.create');

        // ...
    }
		
	//...
	
	public function update(Post $post, StorePostRequest $request)
    {
        $this->authorize('posts.update');

        // ...
    }

    public function destroy(Post $post)
    {
        $this->authorize('posts.delete');

        // ...
    }
		
	// ...
avatar

You are right. Database tables permissions, permission_role and role_user are correctly populated. But without these lines all users can delete posts.

avatar
You can use Markdown
avatar
Luis Antonio Parrado

foreach Closing bracket } is missing in app/Providers/AuthServiceProvider.php into boot method.

public function boot(): void
    {
        try {
            foreach (Permission::pluck('name') as $permission) {
                Gate::define($permission, function ($user) use ($permission) {
                    return $user->roles()->whereHas('permissions', function (Builder $q) use ($permission) {
                        $q->where('name', $permission);
                    })->exists();
                });
            } // **MISSING**
        } catch (QueryException $e) {
            //
        }
    }
avatar
You can use Markdown
avatar

As a respected ambassador of programming principles in general and KISS/YAGNI in particular, I must say that this is too complicated for this limited functionality :)

I would limit myself to the role column in the users table. Casting it to App\Enums\Role

namespace App\Enums;

enum Role: int
{
	case ADMINISTRATOR = 1;
	case EDITOR = 2;
}

And the policy App\Policies\PostPolicy

class PostPolicy
{
	// ...
	public function delete(User $user, Post $post): bool
	{
			return $user->role === Role::ADMINISTRATOR;
	}
	// ...
}
avatar

But that is a completely different approach from our example.

In your code, you are simply saying that user can have only one role, while in our example - we allow multiple roles. Then we are looking at your "KISS/YAGNI" example of a policy and it's hard-coded per role. What if our user has role Admin, but they are just learning and should not have ability to delete the user? I can't handle that case with your example.

Or to shorten what I just said - there are millions of ways to build the same thing. It just boils down to the project needs and future expansions

avatar

So, it all boils down to necessity. In this case, the functionality is excessive for CRUD operations for individual roles. The pinciples and approaches to software development advocate for keeping code simple and avoiding unnecessary functionality that is redundant.

avatar

In a way, you are right. This indeed goes deeper than it should. But one thing we learned over the years of building roles/permissions systems - they change rapidly. And preparing a system like this (or just installing spatie permissions package) is a great way to have the basics covered :) It is not that complex to understand and work with, yet provides you with a good base for future changes.

avatar
You can use Markdown
avatar
You can use Markdown