En una aplicación web, es importante que cada usuario tenga un conjunto de permisos específicos que determinen qué acciones pueden realizar en la plataforma. Los roles son un conjunto de permisos que se asignan a un usuario específico. Por ejemplo:
La asignación de permisos a un rol permite controlar quién puede realizar ciertas acciones en la aplicación.
He preparado un pequeño proyecto base que contiene todo lo necesario para empezar el tutorial, lo puedes descargar aquí. Lo que contiene es:
Luego de clonar el repositorio, nos vamos al branch starter con el siguiente comando.
1git checkout starter
Luego creamos las tablas con información de prueba en la base de datos con el siguiente comando.
1php artisan migrate --seed
Laravel tiene una característica llamada Gates, el cual nos sirve para poder crear acciones o permisos y determinar qué usuario está autorizado para ejecutarlo.
En este caso vamos a crear 2 acciones:
Lo hacemos de la siguiente manera en el archivo app/Providers/AuthServiceProvider.php.
1<?php 2 3namespace App\Providers; 4 5use App\Models\User; 6use Illuminate\Support\Facades\Gate; 7use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; 8 9class AuthServiceProvider extends ServiceProvider10{11 /**12 * The model to policy mappings for the application.13 *14 * @var array<class-string, class-string>15 */16 protected $policies = [17 // 'App\Models\Model' => 'App\Policies\ModelPolicy',18 ];19 20 /**21 * Register any authentication / authorization services.22 */23 public function boot(): void24 {25 Gate::define('see-reports', fn(User $user) => 26 $user->role == User::ROLE_ADMINISTRATOR27 );28 29 Gate::define('register-attendance', fn(User $user) =>30 $user->role == User::ROLE_TEACHER31 );32 }33}
Procedemos a crear las rutas en el archivo routes/web.php.
1<?php 2 3use App\Http\Controllers\ProfileController; 4use Illuminate\Support\Facades\Route; 5 6/* 7|-------------------------------------------------------------------------- 8| Web Routes 9|--------------------------------------------------------------------------10|11| Here is where you can register web routes for your application. These12| routes are loaded by the RouteServiceProvider and all of them will13| be assigned to the "web" middleware group. Make something great!14|15*/16 17Route::get('/', function () {18 return view('welcome');19});20 21Route::get('/dashboard', function () {22 return view('dashboard');23})->middleware(['auth', 'verified'])->name('dashboard');24 25Route::middleware('auth')->group(function () {26 Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');27 Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');28 Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');29 30 Route::get('reports', function () { 31 return view('reports');32 })->name('reports');33 34 Route::get('register-attendance', function () {35 return view('register-attendance');36 })->name('register-attendance');37});38 39require __DIR__.'/auth.php';
Además también creamos las vistas, duplicaremos la vista del resources/views/dashboard.blade.php y crearemos dos vistas. Esta es la vista resources/views/reports.blade.php
1<x-app-layout> 2 <x-slot name="header"> 3 <h2 class="font-semibold text-xl text-gray-800 leading-tight"> 4 {{ __('Reportes') }} 5 </h2> 6 </x-slot> 7 8 <div class="py-12"> 9 <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">10 <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">11 <div class="p-6 text-gray-900">12 {{ __("You're logged in!") }}13 </div>14 </div>15 </div>16 </div>17</x-app-layout>
Esta es la vista resources/views/register-attendance.blade.php.
1<x-app-layout> 2 <x-slot name="header"> 3 <h2 class="font-semibold text-xl text-gray-800 leading-tight"> 4 {{ __('Registrar Asistencia') }} 5 </h2> 6 </x-slot> 7 8 <div class="py-12"> 9 <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">10 <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">11 <div class="p-6 text-gray-900">12 {{ __("You're logged in!") }}13 </div>14 </div>15 </div>16 </div>17</x-app-layout>
Ya tenemos las rutas y las vistas creadas, vamos a actualizar nuestro menú de navegación en el archivo resources/views/layouts/navigation.blade.php.
Por ello utilizaremos la directiva blade llamada @can/@endcan que nos permite usar las acciones o gates que hemos definido anteriormente y usarlas como condicional.
1<nav x-data="{ open: false }" class="bg-white border-b border-gray-100"> 2 <!-- Primary Navigation Menu --> 3 <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 4 <div class="flex justify-between h-16"> 5 <div class="flex"> 6 <!-- Logo --> 7 <div class="shrink-0 flex items-center"> 8 <a href="{{ route('dashboard') }}"> 9 <x-application-logo class="block h-9 w-auto fill-current text-gray-800" /> 10 </a> 11 </div> 12 13 <!-- Navigation Links --> 14 <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> 15 <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> 16 {{ __('Dashboard') }} 17 </x-nav-link> 18 @can('see-reports') 19 <x-nav-link :href="route('reports')" :active="request()->routeIs('reports')"> 20 {{ __('Reportes') }} 21 </x-nav-link> 22 @endcan 23 @can('register-attendance') 24 <x-nav-link :href="route('register-attendance')" :active="request()->routeIs('register-attendance')"> 25 {{ __('Registrar Asistencia') }} 26 </x-nav-link> 27 @endcan 28 </div> 29 </div> 30 31 <!-- Settings Dropdown --> 32 <div class="hidden sm:flex sm:items-center sm:ml-6"> 33 <x-dropdown align="right" width="48"> 34 <x-slot name="trigger"> 35 <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"> 36 <div>{{ Auth::user()->name }}</div> 37 38 <div class="ml-1"> 39 <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> 40 <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> 41 </svg> 42 </div> 43 </button> 44 </x-slot> 45 46 <x-slot name="content"> 47 <x-dropdown-link :href="route('profile.edit')"> 48 {{ __('Profile') }} 49 </x-dropdown-link> 50 51 <!-- Authentication --> 52 <form method="POST" action="{{ route('logout') }}"> 53 @csrf 54 55 <x-dropdown-link :href="route('logout')" 56 onclick="event.preventDefault(); 57 this.closest('form').submit();"> 58 {{ __('Log Out') }} 59 </x-dropdown-link> 60 </form> 61 </x-slot> 62 </x-dropdown> 63 </div> 64 65 <!-- Hamburger --> 66 <div class="-mr-2 flex items-center sm:hidden"> 67 <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"> 68 <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> 69 <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 70 <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 71 </svg> 72 </button> 73 </div> 74 </div> 75 </div> 76 77 <!-- Responsive Navigation Menu --> 78 <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> 79 <div class="pt-2 pb-3 space-y-1"> 80 <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> 81 {{ __('Dashboard') }} 82 </x-responsive-nav-link> 83 @can('see-reports') 84 <x-responsive-nav-link :href="route('reports')" :active="request()->routeIs('reports')"> 85 {{ __('Reportes') }} 86 </x-responsive-nav-link> 87 @endcan 88 @can('register-attendance') 89 <x-responsive-nav-link :href="route('register-attendance')" :active="request()->routeIs('register-attendance')"> 90 {{ __('Registrar Asistencia') }} 91 </x-responsive-nav-link> 92 @endcan 93 </div> 94 95 <!-- Responsive Settings Options --> 96 <div class="pt-4 pb-1 border-t border-gray-200"> 97 <div class="px-4"> 98 <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div> 99 <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>100 </div>101 102 <div class="mt-3 space-y-1">103 <x-responsive-nav-link :href="route('profile.edit')">104 {{ __('Profile') }}105 </x-responsive-nav-link>106 107 <!-- Authentication -->108 <form method="POST" action="{{ route('logout') }}">109 @csrf110 111 <x-responsive-nav-link :href="route('logout')"112 onclick="event.preventDefault();113 this.closest('form').submit();">114 {{ __('Log Out') }}115 </x-responsive-nav-link>116 </form>117 </div>118 </div>119 </div>120</nav>
Si entramos a la web con un usuario “Administrador” podemos ver el menú “Ver Reportes”. El usuario es “administrador@laraveleando.com” y la contraseña es “password”.
Y si ingresamos con un usuario “Profesor” podemos ver el menú “Registrar Asistencia”. El usuario es “profesor@laraveleando.com” y la contraseña es “password”.
Hasta ahora hemos validado nuestro frontend, pero nuestro backend está expuesto 😱 y te voy a mostrar por qué. Si como profesor queremos ver los reportes escribiendo la ruta en el navegador nos daremos cuenta que podemos ingresar y eso pasa porque no hemos protegido nuestras rutas, solo las vistas.
Gracias a los Gates, podemos usar la función “authorize” lo cual validará si el usuario en cuestión tiene el permiso a evaluar. Para ello validaremos en la ruta “/reports” si el usuario es un administrador y en la ruta “/register-attendance” si el usuario es profesor. Agregamos las validaciones en cada ruta de nuestro routes/web.php.
1<?php 2 3use App\Http\Controllers\ProfileController; 4use Illuminate\Support\Facades\Gate; 5use Illuminate\Support\Facades\Route; 6 7/* 8|-------------------------------------------------------------------------- 9| Web Routes10|--------------------------------------------------------------------------11|12| Here is where you can register web routes for your application. These13| routes are loaded by the RouteServiceProvider and all of them will14| be assigned to the "web" middleware group. Make something great!15|16*/17 18Route::get('/', function () {19 return view('welcome');20});21 22Route::get('/dashboard', function () {23 return view('dashboard');24})->middleware(['auth', 'verified'])->name('dashboard');25 26Route::middleware('auth')->group(function () {27 Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');28 Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');29 Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');30 31 Route::get('reports', function () {32 Gate::authorize('see-reports'); 33 return view('reports');34 })->name('reports');35 36 Route::get('register-attendance', function () {37 Gate::authorize('register-attendance'); 38 return view('register-attendance');39 })->name('register-attendance');40});41 42require __DIR__.'/auth.php';
Mensaje que nos aparece si ingresamos como profesor a ver los reportes o como administrador a registrar la asistencia.
Los tests nos ayudarán a poder darle más integridad a nuestra aplicación ya que nos da una mayor fiabilidad de que no rompamos algo cuando estemos añadiendo funcionalidades nuevas.
En este caso usaremos Pest, si quieres saber más sobre este framework de Testing para PHP, te dejaré los enlaces de los videos que he hecho al finalizar el blog.
Laravel nos trae tests ya hechos para que podamos usarlo como referencia, si en el terminal ejecutamos
1./vendor/bin/pest
o
1php artisan test
De esa manera veremos los tests que tenemos por el momento.
El test #1 será que el usuario administrador pueda ver los reportes. Para este tutorial los tests los crearemos en el archivo tests/Feature/ExampleTest.php.
1<?php 2 3use App\Models\User; 4 5it('returns a successful response', function () { 6 $response = $this->get('/'); 7 8 $response->assertStatus(200); 9});10 11test('admin can see reports', function () { 12 // Creamos un usuario administrador13 $adminUser = User::factory()14 ->administrator()15 ->create();16 17 // Nos logueamos con el usuario administrador18 $response = $this->actingAs($adminUser);19 20 // Nos vamos a la ruta "/reports" y esperamos que21 // la petición sea correcta22 $response->get('/reports')23 ->assertOk();24});
De la misma manera crearemos los siguientes 3 tests:
Entonces nuestro archivo de tests quedaría así.
1<?php 2 3use App\Models\User; 4 5it('returns a successful response', function () { 6 $response = $this->get('/'); 7 8 $response->assertStatus(200); 9});10 11test('admin can see reports', function () {12 // Creamos un usuario administrador13 $adminUser = User::factory()14 ->administrator()15 ->create();16 17 // Nos logueamos con el usuario administrador18 $response = $this->actingAs($adminUser);19 20 // Nos vamos a la ruta "/reports" y esperamos que21 // la petición sea correcta22 $response->get('/reports')23 ->assertOk();24});25 26test('teacher cannot see reports', function () { 27 $teacherUser = User::factory()28 ->teacher()29 ->create();30 31 $this->actingAs($teacherUser)32 ->get('/reports')33 ->assertForbidden();34});35 36test('teacher can register attendance', function () {37 $teacherUser = User::factory()38 ->teacher()39 ->create();40 41 $this->actingAs($teacherUser)42 ->get('/register-attendance')43 ->assertOk();44});45 46test('admin cannot register attendance', function () {47 $adminUser = User::factory()48 ->administrator()49 ->create();50 51 $this->actingAs($adminUser)52 ->get('/register-attendance')53 ->assertForbidden();54});
Vamos a ejecutar nuestros tests con el siguiente comando:
1php artisan test --filter ExampleTest
Esta es tan sola una manera de poder manejar los roles y permisos en nuestras aplicaciones Laravel, para algunos proyectos sencillos puede funcionar, para otros quizá se necesite del uso de Policies o paquetes como Laravel Permissions.
No olvides suscribirte a mis canales de Youtube, TikTok y también a mi Newsletter para que te enteres del próximo tutorial donde aprenderás sobre cómo usar los Policies y seguir dándole seguridad a tus aplicaciones Laravel.
Tendrás tutoriales, tips, conceptos y puedas convertirte en un artesano de todo el ecosistema Laravel.
Revisa los detalles del nuevo curso en desarrollo