Skip to content

Implementação de Permissões no Laravel

Introdução

O Laravel fornece políticas para definir a lógica de autorização para sua aplicação. Essas políticas estabelecem as regras que governam quais usuários estão autorizados a realizar ações específicas nos modelos da sua aplicação. Neste guia, discutiremos os passos necessários para implementar permissões granulares em uma aplicação Laravel usando um esquema de banco de dados, uma trait e uma política.

Pré-requisitos

Antes de prosseguir, certifique-se de que você tenha uma aplicação Laravel configurada e em execução. Você também deve estar familiarizado com migrações, modelos e políticas do Laravel.

Esquema de Banco de Dados

Para implementar permissões em uma aplicação Laravel, crie um esquema de banco de dados que inclua tabelas para funções, permissões e seus relacionamentos. O exemplo a seguir demonstra como criar essas tabelas usando migrações do Laravel:

bash
php artisan make:model Role -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Execute as migrações.
     */
    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverta as migrações.
     */
    public function down(): void
    {
        Schema::dropIfExists('roles');
    }
};
bash
php artisan make:model Permission -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Execute as migrações.
     */
    public function up(): void
    {
        Schema::create('permissions', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverta as migrações.
     */
    public function down(): void
    {
        Schema::dropIfExists('permissions');
    }
};
bash
php artisan make:model RolePermission -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Execute as migrações.
     */
    public function up(): void
    {
        Schema::create('role_permissions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
            $table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade');
            $table->softDeletes();
            $table->timestamps();
        });
    }

    /**
     * Reverta as migrações.
     */
    public function down(): void
    {
        Schema::dropIfExists('role_permissions');
    }
};
bash
php artisan make:model UserRole -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Execute as migrações.
     */
    public function up(): void
    {
        Schema::create('user_roles', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
            $table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverta as migrações.
     */
    public function down(): void
    {
        Schema::dropIfExists('user_roles');
    }
};

Neste exemplo, criamos tabelas para funções, permissões e seus relacionamentos. A tabela roles inclui um ID, um nome, uma descrição e timestamps. A tabela permissions consiste em um ID, um nome, uma descrição e timestamps. A tabela role_permissions contém um ID, um role_id, um permission_id e timestamps. As colunas role_id e permission_id são chaves estrangeiras que fazem referência às colunas id nas tabelas roles e permissions, respectivamente.

Esquemas de Modelo

Para fornecer dicas de tipo e melhorar a legibilidade do código ao interagir com os modelos Role, Permission e User, criamos classes <Model>Schema correspondentes. Essas classes definem constantes que representam os nomes das colunas nas tabelas de banco de dados correspondentes.

Por exemplo, aqui está a classe PermissionSchema:

php
<?php

namespace App\Schemas;

class PermissionSchema
{
    const id = 'id';
    const name = 'name';
    const description = 'description';
    const created_at = 'created_at';
    const updated_at = 'updated_at';
    const deleted_at = 'deleted_at';
}

No exemplo acima, estamos definindo constantes para cada uma das colunas na tabela de permissões. Podemos então usar essas constantes para fornecer dicas de tipo e melhorar a legibilidade do código ao interagir com o modelo Permission e seus dados associados.

Usaremos esses esquemas posteriormente para definir o nome das colunas ao interagir com os modelos. Por exemplo, na trait HasPermissions que criaremos mais tarde, usamos a constante PermissionSchema::name para fazer referência à coluna name na tabela de permissões:

php
use App\Schemas\PermissionSchema;

// ...

public function hasPermission(string $permission): bool
{
    return $this->roles->some(function ($role) use ($permission) {
        return $role->permissions->contains(PermissionSchema::name, $permission);
    });
}

Ao usar PermissionSchema::name em vez da string bruta "name", estamos tornando nosso código mais legível e menos propenso a erros. Além disso, se alguma vez mudarmos o nome da coluna name na tabela de permissões, poderíamos simplesmente atualizar a constante PermissionSchema::name para refletir o novo nome, e todas as referências a essa constante em nosso código seriam automaticamente atualizadas.

Ao usar esquemas de modelo dessa maneira, podemos tornar nosso código mais fácil de manter e trabalhar ao longo do tempo.

Modelos

Para implementar permissões granulares em uma aplicação Laravel, crie quatro modelos: Permission, Role, RolePermission e UserRole. Esses modelos interagem com as tabelas do banco de dados que armazenam informações sobre funções, permissões e relacionamentos entre usuários e funções.

Aqui está o modelo Role:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property string $name
 * @property string|null $description
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class Role extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'user_roles')->withTimestamps();
    }

    public function permissions(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, 'role_permissions')->withTimestamps();
    }
}

No exemplo do modelo Role acima, usamos o relacionamento BelongsToMany para definir a relação entre os modelos Role e User, bem como a relação entre os modelos Role e Permission.

Aqui está o modelo Permission:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property string $name
 * @property string|null $description
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class Permission extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'role_permissions')->withTimestamps();
    }
}

No exemplo do modelo Permission acima, usamos o relacionamento BelongsToMany para definir a relação entre os modelos Permission e Role.

Aqui está o modelo RolePermission:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property int $role_id
 * @property int $permission_id
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class RolePermission extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }

    public function permission(): BelongsTo
    {
        return $this->belongsTo(Permission::class);
    }
}

No exemplo do modelo RolePermission acima, usamos o relacionamento BelongsTo para definir a relação entre os modelos RolePermission e Role, bem como a relação entre os modelos RolePermission e Permission.

Aqui está o modelo UserRole:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property int $user_id
 * @property int $role_id
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon $deleted_at
 */
class UserRole extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }
}

No exemplo do modelo UserRole acima, usamos o relacionamento BelongsTo para definir a relação entre os modelos UserRole e User, bem como a relação entre os modelos UserRole e Role.

Finalmente, aqui está o modelo User:

php
<?php

namespace App\Models;

use App\Traits\HasPermissions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

/**
 * Modelo User representando um usuário no sistema.
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property \DateTime|null $email_verified_at
 * @property string $password
 * @property string|null $remember_token
 * @property int|null $contact_id
 * @property \DateTime $created_at
 * @property \DateTime $updated_at
 */
class User extends Authenticatable
{
    use HasFactory, HasPermissions, Notifiable;

    /**
     * Os atributos que são atribuíveis em massa.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * Os atributos que devem ser ocultados para serialização.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Os atributos que devem ser convertidos.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'user_roles')->withTimestamps();
    }
}

No exemplo do modelo User acima, usamos o relacionamento BelongsToMany para definir a relação entre os modelos User e Role. Além disso, usamos várias traits e propriedades para lidar com autenticação, notificações e permissões.

Trait de Verificação de Permissão

Para simplificar as verificações de permissão em uma política, crie uma trait que adicione um método hasPermission() à classe que a utiliza. O exemplo a seguir demonstra como criar essa trait:

php
<?php

namespace App\Traits;

use App\Schemas\PermissionSchema;

trait HasPermissions
{
    public function hasPermission(string $permission): bool
    {
        return $this->roles->some(function ($role) use ($permission) {
            return $role->permissions->contains(PermissionSchema::name, $permission);
        });
    }
}

Este método simplifica o processo de verificar se um usuário tem uma permissão específica.

O método hasPermission recebe um parâmetro string, $permission, e retorna um valor booleano. Ele verifica se o usuário atual tem a permissão especificada examinando todas as funções associadas ao usuário.

Se nenhuma das funções possuir a $permission necessária, a função some retorna false, indicando que o usuário não tem a permissão necessária.

Ao incorporar essa trait em seu modelo User, você pode facilmente verificar se um usuário tem uma permissão específica.

Implementando Permissões Granulares com Políticas

Para aplicar permissões granulares usando o modelo User, crie uma classe de política para cada modelo que você deseja controlar o acesso. Neste exemplo, criaremos uma PostPolicy para gerenciar o acesso ao modelo Post.

Primeiro, crie a classe PostPolicy:

php
namespace App\Policies;

use App\Models\User;
use App\Models\Post;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    public function viewAny(User $user)
    {
        return $user->hasPermission('viewAny-posts');
    }

    public function view(User $user, Post $post)
    {
        return $user->hasPermission('view-posts') && $post->user_id === $user->id;
    }

    public function create(User $user)
    {
        return $user->hasPermission('create-posts');
    }

    public function update(User $user, Post $post)
    {
        return $user->hasPermission('update-posts') && $post->user_id === $user->id;
    }

    public function delete(User $user, Post $post)
    {
        return $user->hasPermission('delete-posts') && $post->user_id === $user->id;
    }
}

Observe que utilizamos a trait hasPermission adicionada ao modelo User.

Em seguida, registre a política no AuthServiceProvider. Aqui está um exemplo de registro da PostPolicy:

php
namespace App\Providers;

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    public function boot()
    {
        $this->registerPolicies();
    }
}

Neste exemplo, registramos a PostPolicy para o modelo Post. Isso instrui o Laravel a usar a PostPolicy para controlar o acesso às instâncias do modelo Post.

Agora, você pode usar essa política em seus controladores para autorizar ações. Aqui está um exemplo de uso da PostPolicy em um controlador:

php
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function edit(Post $post)
    {
        $this->authorize('update', $post);

        return view('posts.edit', compact('post'));
    }

    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        // Atualiza o post...
    }
}

Neste exemplo, usamos o método authorize() para verificar se o usuário autenticado tem permissão para atualizar o post fornecido, empregando o método update da PostPolicy. Se o usuário estiver autorizado, permitimos que ele edite ou atualize o post. Caso contrário, retornamos um erro HTTP 403 Forbidden.

Ao usar políticas para controlar o acesso aos seus modelos, você garante que apenas usuários autorizados possam realizar ações sensíveis em sua aplicação.