Policy e Gate: autorização granular

1. Fundamentos da Autorização no Laravel

1.1 Autorização vs Autenticação

Autenticação responde "quem é você?", enquanto autorização responde "o que você pode fazer?". No Laravel, Gates e Policies são os mecanismos principais para implementar autorização granular, permitindo controlar acesso a recursos específicos com base em regras de negócio.

1.2 Visão Geral do Sistema

O sistema de autorização do Laravel é centrado no serviço Gate, que gerencia todas as verificações de permissão. Diferente do Auth::check() (que apenas verifica se o usuário está logado), Gates e Policies permitem definir regras complexas.

// Verificação básica vs autorização granular
Auth::check(); // apenas autenticação
Gate::allows('update-post', $post); // autorização específica

1.3 Registro no AuthServiceProvider

Tanto Gates quanto Policies são registrados no AuthServiceProvider:

// app/Providers/AuthServiceProvider.php
public function boot(): void
{
    // Registro de Gate
    Gate::define('edit-settings', function (User $user) {
        return $user->is_admin;
    });

    // Registro de Policy
    Gate::policy(Post::class, PostPolicy::class);
}

2. Gates: Autorizações Simples

2.1 Criando Gates com Closures

Gates são ideais para autorizações simples que não justificam uma classe Policy inteira:

// No AuthServiceProvider
Gate::define('view-reports', function (User $user) {
    return $user->hasRole('manager');
});

Gate::define('export-data', function (User $user, string $format) {
    return $user->canExportTo($format);
});

2.2 Verificações com Gates

// No controller
if (Gate::allows('view-reports')) {
    // Usuário pode ver relatórios
}

if (Gate::denies('export-data', 'pdf')) {
    abort(403, 'Exportação PDF não permitida');
}

// Lança exceção automaticamente
Gate::authorize('view-reports');

2.3 Gates com Múltiplos Parâmetros

// Definição
Gate::define('manage-category', function (User $user, $category, $action) {
    return $user->canManage($category, $action);
});

// Uso
Gate::authorize('manage-category', [$category, 'delete']);

3. Policies: Classes de Autorização para Modelos

3.1 Gerando Policies

php artisan make:policy PostPolicy --model=Post

3.2 Métodos Padrão

// app/Policies/PostPolicy.php
class PostPolicy
{
    public function viewAny(User $user): bool
    {
        return true; // Todos podem listar
    }

    public function view(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $post->is_public;
    }

    public function create(User $user): bool
    {
        return $user->hasVerifiedEmail();
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id && !$post->is_pinned;
    }

    public function restore(User $user, Post $post): bool
    {
        return $user->hasRole('admin');
    }

    public function forceDelete(User $user, Post $post): bool
    {
        return $user->hasRole('super-admin');
    }
}

3.3 Injeção de Dependências

public function update(User $user, Post $post, Logger $logger): bool
{
    $logger->info("Verificando permissão de atualização");
    return $user->id === $post->user_id;
}

4. Integração com Controllers e Views

4.1 Autorização no Controller

// app/Http/Controllers/PostController.php
public function update(Request $request, Post $post)
{
    // Autorização inline
    $this->authorize('update', $post);

    // Ou com modelo explícito
    $this->authorize('update', Post::class);

    // Lógica de atualização
    $post->update($request->validated());
}

4.2 Diretivas Blade

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Editar</a>
@elsecan('view', $post)
    <span>Visualizar apenas</span>
@endcan

@cannot('delete', $post)
    <span>Exclusão não permitida</span>
@endcannot

4.3 Middleware em Rotas

// Rotas protegidas
Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

Route::delete('/posts/{post}', [PostController::class, 'destroy'])
    ->middleware('can:delete,post');

5. Autorização Granular com Condições Complexas

5.1 Verificações Baseadas em Relacionamentos

// Policy para Team
class TeamPolicy
{
    public function addMember(User $user, Team $team): bool
    {
        return $user->id === $team->owner_id || 
               $team->members()->where('user_id', $user->id)
                    ->where('role', 'admin')->exists();
    }

    public function removeMember(User $user, Team $team, User $member): bool
    {
        return ($user->id === $team->owner_id) || 
               ($user->id !== $member->id && 
                $team->isAdmin($user) && 
                !$team->isOwner($member));
    }
}

5.2 Políticas com Papéis e Permissões

public function publish(User $user, Post $post): bool
{
    if ($user->hasPermissionTo('publish-posts')) {
        return true;
    }

    if ($post->status === 'draft' && $user->id === $post->user_id) {
        return $user->hasAnyRole(['editor', 'admin']);
    }

    return false;
}

5.3 Regras Globais com before() e after()

class PostPolicy
{
    // Executa antes de qualquer método
    public function before(User $user, string $ability): ?bool
    {
        if ($user->hasRole('super-admin')) {
            return true; // Super admin tem acesso total
        }

        return null; // Continua para o método específico
    }

    // Executa depois de qualquer método
    public function after(User $user, string $ability, bool $result): bool
    {
        if ($user->hasRole('auditor') && $ability === 'view') {
            return true; // Auditores sempre podem visualizar
        }

        return $result;
    }
}

6. Respostas e Tratamento de Exceções

6.1 Personalizando Respostas de Erro

// No Policy
use Illuminate\Auth\Access\Response;

public function delete(User $user, Post $post): Response
{
    if ($user->id !== $post->user_id) {
        return Response::deny('Você não é o autor deste post.');
    }

    if ($post->is_pinned) {
        return Response::deny('Posts fixados não podem ser excluídos.');
    }

    return Response::allow();
}

6.2 Inspeção de Autorização

// Gate::inspect() retorna resposta detalhada
$response = Gate::inspect('update', $post);

if ($response->allowed()) {
    // Prosseguir
} else {
    $message = $response->message(); // Mensagem personalizada
    $code = $response->code(); // Código de erro
}

// Gate::raw() retorna boolean puro
$canUpdate = Gate::raw('update', $post);

7. Testes de Autorização

7.1 Testando Gates com Fake

public function test_gate_authorization()
{
    Gate::fake();

    // Simular autorização
    Gate::define('edit-post', function () {
        return true;
    });

    $response = $this->actingAs(User::factory()->create())
                     ->put('/posts/1', ['title' => 'Novo']);

    Gate::assertAuthorized('edit-post');
}

7.2 Testando Policies

public function test_post_policy()
{
    $user = User::factory()->create();
    $post = Post::factory()->create(['user_id' => $user->id]);

    $this->assertTrue($user->can('update', $post));
    $this->assertTrue($user->can('delete', $post));

    $otherUser = User::factory()->create();
    $this->assertFalse($otherUser->can('update', $post));
}

7.3 Simulação Completa

public function test_authorization_in_feature_test()
{
    $admin = User::factory()->create(['role' => 'admin']);
    $post = Post::factory()->create();

    $response = $this->actingAs($admin)
                     ->delete("/posts/{$post->id}");

    $response->assertStatus(200);

    $regularUser = User::factory()->create(['role' => 'user']);

    $response = $this->actingAs($regularUser)
                     ->delete("/posts/{$post->id}");

    $response->assertStatus(403);
}

8. Boas Práticas e Performance

8.1 Organização por Domínio

// app/Policies/Admin/
// app/Policies/Content/
// app/Policies/Finance/

// Reutilização de lógica
trait AdminAuthorization
{
    public function before(User $user): ?bool
    {
        return $user->hasRole('admin') ? true : null;
    }
}

class ContentPolicy
{
    use AdminAuthorization;

    public function create(User $user): bool
    {
        return $user->hasVerifiedEmail();
    }
}

8.2 Cache de Autorização

// Cache para evitar consultas repetidas
public function view(User $user, Post $post): bool
{
    return Cache::remember(
        "user.{$user->id}.post.{$post->id}.view",
        3600,
        fn() => $user->id === $post->user_id || $post->is_public
    );
}

// Uso de before() para cache global
public function before(User $user): ?bool
{
    if ($cached = Cache::get("user.{$user->id}.permissions")) {
        return $cached['super_admin'] ? true : null;
    }
    return null;
}

8.3 Evitando Redundância

// Form Request com autorização embutida
class UpdatePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return Gate::allows('update', $this->route('post'));
    }

    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
            'content' => 'required',
        ];
    }
}

// Controller limpo
public function update(UpdatePostRequest $request, Post $post)
{
    $post->update($request->validated());
    return redirect()->route('posts.show', $post);
}

Referências