Policies¶
Las Policies son clases que organizan la lógica de autorización para modelos específicos. Proporcionan una forma limpia de determinar si un usuario puede realizar una acción determinada sobre un recurso.
¿Cuándo usar Policies vs Gates?¶
- Policies: Para autorización basada en recursos/modelos (ej.
User
,Role
,Post
) - Gates: Para acciones simples no relacionadas con modelos (ej.
view-admin-panel
)
Base Resource Policy¶
El sistema incluye una BaseResourcePolicy
abstracta que estandariza el mapeo de abilities de Laravel a permisos de Spatie, con soporte para prefijos por recurso y multi-guard.
Abilities Estándar¶
La BaseResourcePolicy
proporciona estas abilities estándar:
Ability | Propósito | Permiso Construido |
---|---|---|
viewAny |
Listar recursos | {prefix}.view |
view |
Ver recurso específico | {prefix}.view |
create |
Crear nuevo recurso | {prefix}.create |
update |
Actualizar recurso | {prefix}.update |
delete |
Eliminar recurso | {prefix}.delete |
restore |
Restaurar recurso eliminado | {prefix}.restore |
forceDelete |
Eliminar permanentemente | {prefix}.forceDelete |
export |
Exportar recursos | {prefix}.export |
Implementación Básica¶
<?php
namespace App\Policies;
use App\Models\User;
use Spatie\Permission\Models\Role;
class RolePolicy extends BaseResourcePolicy
{
protected function abilityPrefix(): string
{
return 'roles'; // roles.view, roles.create, etc.
}
}
Consideraciones Multi-Guard¶
Spatie Permission segmenta permisos por guard. La BaseResourcePolicy
soporta esto mediante el método guardName()
:
class ApiRolePolicy extends BaseResourcePolicy
{
protected function abilityPrefix(): string
{
return 'roles';
}
protected function guardName(): ?string
{
return 'api'; // Usa permisos del guard 'api'
}
}
Importante: Los permisos deben crearse con el guard_name
correcto:
// Permiso para guard 'web'
Permission::create(['name' => 'roles.view', 'guard_name' => 'web']);
// Permiso para guard 'api'
Permission::create(['name' => 'roles.view', 'guard_name' => 'api']);
Registro en AuthServiceProvider¶
Método 1: Array $policies¶
<?php
namespace App\Providers;
use App\Policies\RolePolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Spatie\Permission\Models\Role;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Role::class => RolePolicy::class,
];
public function boot(): void
{
$this->registerPolicies();
}
}
Método 2: Gate::policy()¶
public function boot(): void
{
Gate::policy(Role::class, RolePolicy::class);
Gate::policy(Permission::class, PermissionPolicy::class);
}
Gate::before para Super-Admin¶
Para permitir que un rol super-admin
bypasee todas las políticas:
public function boot(): void
{
$this->registerPolicies();
Gate::before(function (User $user, string $ability) {
return $user->hasRole('super-admin') ? true : null;
});
}
⚠️ Advertencias:
Gate::before
se ejecuta ANTES que cualquier policy- Retornar
true
= permitir,false
= denegar,null
= continuar con policy - Úsalo con cuidado - el super-admin tendrá acceso total
Personalización de Policies¶
Puedes override métodos de BaseResourcePolicy
para lógica contextual:
class RolePolicy extends BaseResourcePolicy
{
protected function abilityPrefix(): string
{
return 'roles';
}
public function delete(User $user, Role $role): bool
{
// Verificar permiso base
if (!parent::delete($user, $role)) {
return false;
}
// Regla de negocio: no eliminar roles con usuarios asignados
return $role->users()->count() === 0;
}
}
Integración en Controladores¶
Index (Listado)¶
public function index()
{
$this->authorize('viewAny', Role::class);
// Lógica del listado...
}
Show (Ver)¶
public function show(Role $role)
{
$this->authorize('view', $role);
return response()->json($role);
}
Store (Crear)¶
public function store(StoreRoleRequest $request)
{
$this->authorize('create', Role::class);
// Crear rol...
}
Update (Actualizar)¶
public function update(UpdateRoleRequest $request, Role $role)
{
$this->authorize('update', $role);
// Actualizar rol...
}
Destroy (Eliminar)¶
public function destroy(Role $role)
{
$this->authorize('delete', $role);
$role->delete();
}
Export (Exportar)¶
public function export()
{
$this->authorize('export', Role::class);
// Lógica de exportación...
}
Verificación Manual¶
También puedes verificar permisos manualmente:
// En controladores
if (Gate::allows('viewAny', Role::class)) {
// Usuario puede ver roles
}
// En vistas Blade
@can('create', App\Models\Role::class)
<a href="{{ route('roles.create') }}">Crear Rol</a>
@endcan
// En código general
if ($user->can('update', $role)) {
// Usuario puede actualizar este rol
}
Integración Frontend (Inertia.js)¶
Pasa información de permisos al frontend:
// En HandleInertiaRequests middleware
public function share(Request $request): array
{
return [
'auth' => [
'user' => $request->user(),
'permissions' => $request->user()?->getAllPermissions()->pluck('name'),
],
];
}
// En React/Vue
import { usePage } from '@inertiajs/react';
function RolesList() {
const { auth } = usePage().props;
const canCreate = auth.permissions.includes('roles.create');
return <div>{canCreate && <button onClick={createRole}>Crear Rol</button>}</div>;
}
Testing¶
Setup Base en Tests¶
beforeEach(function () {
// Crear permisos necesarios
Permission::create(['name' => 'roles.view', 'guard_name' => 'web']);
Permission::create(['name' => 'roles.create', 'guard_name' => 'web']);
// ...
});
Test de Autorización¶
it('authorizes user with correct permissions', function () {
$user = User::factory()->create();
$user->givePermissionTo('roles.view');
$this->actingAs($user);
expect(Gate::allows('viewAny', Role::class))->toBeTrue();
});
it('denies user without permissions', function () {
$user = User::factory()->create();
$this->actingAs($user);
expect(Gate::denies('viewAny', Role::class))->toBeTrue();
});
Test de HTTP¶
it('returns 403 for unauthorized access', function () {
$user = User::factory()->create(); // Sin permisos
$this->actingAs($user)
->get('/roles')
->assertForbidden();
});
Mejores Prácticas¶
✅ Recomendado¶
- Usar
BaseResourcePolicy
para consistencia - Definir permisos con prefijos claros (
roles.view
,users.create
) - Registrar policies en
AuthServiceProvider
- Verificar autorización en todos los endpoints
- Testear todos los casos de autorización
❌ Evitar¶
- Lógica de autorización directa en controladores
- Mezclar Gates y Policies para el mismo recurso
- Usar
Gate::before
sin considerar las implicaciones - Permisos sin prefijo o con nombres inconsistentes
- Autorización solo en frontend
Patrones Comunes¶
Policy con Scope¶
public function viewAny(User $user): bool
{
// Solo puede ver roles de su organización
return $this->can($user, 'view') && $user->organization_id !== null;
}
Policy Condicional¶
public function update(User $user, Role $role): bool
{
if (!parent::update($user, $role)) {
return false;
}
// Solo puede actualizar si es de su organización
return $user->organization_id === $role->organization_id;
}
Policy con Relaciones¶
public function assign(User $user, Role $role, User $targetUser): bool
{
return $this->can($user, 'assign') &&
$user->can('manage', $targetUser);
}