Curso de Laravel
Autorización y permisos en Laravel: Gates y Policies
Por Víctor Peña · Publicado el
Hola, ¿cómo están? En la lección anterior resolvimos quién entra al sistema. Hoy resolvemos qué puede hacer cada uno.
Un recepcionista debería poder agendar citas pero no borrar historiales. Un doctor debería ver solo a sus pacientes. Eso es autorización, y Laravel tiene dos herramientas para ello.
¡Empecemos!
Gates y Policies: cuál usar
La diferencia es simple:
- Un Gate es un permiso suelto, que no gira alrededor de un modelo. «¿Puede ver el panel de administración?»
- Una Policy agrupa los permisos de un modelo. Quién puede ver, crear, editar y borrar pacientes.
En la práctica, casi todo son Policies. Los Gates quedan para permisos generales que no encajan en ningún modelo.
Gates
Se definen en AppServiceProvider:
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('ver-reportes', function (User $usuario) {
return in_array($usuario->rol, ['admin', 'gerente']);
});
Gate::define('acceder-configuracion', function (User $usuario) {
return $usuario->rol === 'admin';
});
}
Y se usan así:
if (Gate::allows('ver-reportes')) {
// ...
}
if (Gate::denies('ver-reportes')) {
abort(403);
}
Gate::authorize('ver-reportes'); // lanza el 403 directamente
El usuario autenticado se pasa solo; no hay que indicarlo.
Policies: lo que vas a usar de verdad
php artisan make:policy PacientePolicy --model=Paciente
Eso genera app/Policies/PacientePolicy.php con un método por cada acción del CRUD:
<?php
namespace App\Policies;
use App\Models\Paciente;
use App\Models\User;
class PacientePolicy
{
public function viewAny(User $usuario): bool
{
return true;
}
public function view(User $usuario, Paciente $paciente): bool
{
return true;
}
public function create(User $usuario): bool
{
return in_array($usuario->rol, ['admin', 'recepcion']);
}
public function update(User $usuario, Paciente $paciente): bool
{
return in_array($usuario->rol, ['admin', 'recepcion']);
}
public function delete(User $usuario, Paciente $paciente): bool
{
return $usuario->rol === 'admin';
}
public function restore(User $usuario, Paciente $paciente): bool
{
return $usuario->rol === 'admin';
}
}
Los nombres de los métodos coinciden con los del controlador de recursos. Eso no es casualidad: es lo que permite conectarlos automáticamente.
Fíjate en la diferencia entre viewAny y view: el primero no recibe modelo porque se refiere al listado; el segundo recibe el registro concreto.
El registro es automático
En las versiones actuales de Laravel no hace falta declarar nada: si el modelo es Paciente y la policy se llama PacientePolicy en app/Policies/, se encuentra sola.
Si necesitas otra convención:
use Illuminate\Support\Facades\Gate;
Gate::policy(Paciente::class, GestionPacientesPolicy::class);
Usar la policy en el controlador
Tres formas, de más explícita a más automática.
Método por método:
public function edit(Paciente $paciente)
{
$this->authorize('update', $paciente);
return view('pacientes.edit', compact('paciente'));
}
public function create()
{
$this->authorize('create', Paciente::class);
return view('pacientes.create');
}
Cuando no hay instancia —como en create— se pasa el nombre de la clase.
Todo el controlador de una vez:
public function __construct()
{
$this->authorizeResource(Paciente::class, 'paciente');
}
Esa línea conecta cada método del controlador con el método correspondiente de la policy. Es la que uso en controladores de recursos: una línea y todo queda protegido, sin posibilidad de olvidar uno.
Desde el Form Request, en el método authorize() que vimos en la lección de validación:
public function authorize(): bool
{
return $this->user()->can('update', $this->paciente);
}
Autorización en Blade
Aquí está el uso más visible: ocultar lo que el usuario no puede hacer.
@can('create', App\Models\Paciente::class)
<a href="{{ route('pacientes.create') }}" class="boton">Nuevo paciente</a>
@endcan
@can('update', $paciente)
<a href="{{ route('pacientes.edit', $paciente) }}">Editar</a>
@endcan
@can('delete', $paciente)
<form method="POST" action="{{ route('pacientes.destroy', $paciente) }}">
@csrf
@method('DELETE')
<button type="submit">Eliminar</button>
</form>
@endcan
También existen @cannot y @canany:
@canany(['update', 'delete'], $paciente)
<div class="acciones">...</div>
@endcanany
Ocultar el botón no es proteger la acción. El @can mejora la experiencia; la seguridad la da el authorize() del controlador. Alguien puede escribir la URL a mano, y si no está protegida, entra.
Las dos cosas van juntas: @can en la vista, authorize() en el controlador.
Roles: lo mínimo que funciona
Para la mayoría de sistemas, una columna basta:
// migración
$tabla->string('rol')->default('usuario');
Y en el modelo:
public function esAdmin(): bool
{
return $this->rol === 'admin';
}
public function tieneRol(string ...$roles): bool
{
return in_array($this->rol, $roles);
}
public function delete(User $usuario, Paciente $paciente): bool
{
return $usuario->tieneRol('admin');
}
Si los roles son un conjunto cerrado, un enum lo deja más claro:
enum Rol: string
{
case Admin = 'admin';
case Doctor = 'doctor';
case Recepcion = 'recepcion';
public function etiqueta(): string
{
return match ($this) {
self::Admin => 'Administrador',
self::Doctor => 'Doctor',
self::Recepcion => 'Recepción',
};
}
}
protected function casts(): array
{
return ['rol' => Rol::class];
}
if ($usuario->rol === Rol::Admin) { }
Con eso, un rol mal escrito da error en lugar de fallar en silencio, en lugar de quedar como un texto suelto repartido por el proyecto. Es la misma idea de agrupar valores fijos que vimos con las constantes en PHP, pero con verificación de tipos.
Cuándo necesitas permisos, no roles
Un rol es un conjunto fijo de permisos. Funciona hasta que aparece la petición de siempre: «este doctor sí puede ver los reportes, pero solo él».
Ahí conviene pasar a permisos:
usuarios ─── rol_usuario ─── roles ─── permiso_rol ─── permisos
public function tienePermiso(string $permiso): bool
{
return $this->roles()
->whereHas('permisos', fn ($q) => $q->where('nombre', $permiso))
->exists();
}
Y en la policy:
public function delete(User $usuario, Paciente $paciente): bool
{
return $usuario->tienePermiso('pacientes.eliminar');
}
Consejo práctico: no empieces por aquí. Un sistema con tres tipos de usuario no necesita una tabla de permisos. Empieza con la columna rol, y migra a permisos cuando el negocio realmente lo pida. Es mucho más fácil añadirlo después que mantener una estructura que nadie usa.
Si llegas a necesitarlo, hay paquetes muy establecidos que resuelven roles y permisos completos sin escribirlo desde cero.
Permisos sobre el propio registro
Este es el caso donde las policies brillan, porque el permiso depende del registro concreto:
public function view(User $usuario, Cita $cita): bool
{
if ($usuario->rol === Rol::Admin) {
return true;
}
if ($usuario->rol === Rol::Doctor) {
return $cita->doctor_id === $usuario->doctor_id;
}
return $usuario->rol === Rol::Recepcion;
}
Un doctor ve sus citas; el administrador, todas. Con un simple if ($usuario->rol === 'doctor') en el controlador esto sería imposible de expresar limpiamente.
Y en el listado, la restricción también debe aplicarse a la consulta:
$citas = Cita::query()
->when($usuario->rol === Rol::Doctor,
fn ($q) => $q->where('doctor_id', $usuario->doctor_id))
->with('paciente')
->paginate(15);
Las policies protegen registro a registro; la consulta protege el listado. Son dos cosas distintas y hacen falta las dos.
El superusuario
Para que el administrador no tenga que aparecer en cada método:
Gate::before(function (User $usuario, string $habilidad) {
return $usuario->rol === Rol::Admin ? true : null;
});
Ese null es importante: significa «no opino, sigue evaluando». Devolver false bloquearía a todos los demás.
Úsalo con cuidado. Un Gate::before demasiado amplio hace que las policies dejen de aplicarse sin que se note, y depurar eso lleva tiempo.
Respuestas con explicación
Un 403 sin motivo frustra al usuario:
use Illuminate\Auth\Access\Response;
public function update(User $usuario, Cita $cita): Response
{
if ($cita->estado === 'atendida') {
return Response::deny('No se puede modificar una cita ya atendida.');
}
return $usuario->tieneRol('admin', 'recepcion')
? Response::allow()
: Response::deny('No tienes permiso para modificar citas.');
}
Ese mensaje llega a la página de error, y el usuario entiende qué pasó.
Middleware por rol
Para zonas completas del sistema:
Route::middleware(['auth', 'can:acceder-configuracion'])->group(function () {
Route::resource('usuarios', UsuarioController::class);
});
O con un middleware propio, registrado en bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'rol' => \App\Http\Middleware\VerificarRol::class,
]);
})
Route::middleware('rol:admin')->group(function () {
// ...
});
Comprobar sin lanzar excepción
$usuario->can('update', $paciente);
$usuario->cannot('delete', $paciente);
Gate::forUser($otroUsuario)->allows('update', $paciente);
Útil cuando quieres mostrar un mensaje en lugar de un 403.
Personalizar la página 403
En resources/views/errors/403.blade.php:
@extends('layouts.app')
@section('contenido')
<h1>No tienes acceso a esta sección</h1>
<p>{{ $exception->getMessage() ?: 'Si crees que es un error, consulta con el administrador.' }}</p>
<a href="{{ route('pacientes.index') }}">Volver al inicio</a>
@endsection
Ese $exception->getMessage() muestra el texto del Response::deny().
Errores comunes
- Proteger solo la vista con
@cany dejar el controlador abierto. - Lógica de permisos repartida en
ifpor todos los controladores. - No filtrar el listado, protegiendo el detalle pero mostrando todo en la tabla.
Gate::beforeque devuelvefalseen vez denull.- Montar tablas de permisos para un sistema con tres roles.
- Olvidar
authorizeResource()y proteger cinco de siete métodos.
Para cerrar
La autorización mal hecha se dispersa en condicionales por todo el proyecto y se vuelve imposible de auditar. Las policies la concentran en un archivo por modelo, donde se puede leer de un vistazo.
Dos hábitos: authorizeResource() en los controladores de recursos, y @can en las vistas para no mostrar lo que no se puede hacer.
En la siguiente lección veremos la subida de archivos.
Saludos y éxitos.
Norvic Software
Desarrollamos el software que tu empresa necesita
Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp