RBAC kialakítása Laravel keretrendszerrel

A szerepkör alapú jogosultságkezelés a legtöbb esetben elvárás az alkalmazással szemben, ezért a cikk folyamán bemutatom ennek megvalósítását Laravel keretrendszerrel.


A Laravel keretrendszer nem tartalmaz szerepkör alapú jogosultságkezelést, ezért több harmadik féltől származó csomag is rendelkezésre áll. Ennek ellenére a saját implementáció mellett döntöttem, amely az alábbi elemekből épül fel:

  1. Engedély (permission)
  2. Szerepkör (role)

A következő lépésben azt is el kell döntetnünk, hogy a szükséges elemeket konfigurációs fájlban vagy adatbázisban szeretnénk tárolni. Az előbbi gyorsabb elérést, az utóbbi viszont kényelmesebb konfigurációt eredményez. Ezeket figyelembe véve az adatbázis alapú tárolást választottam.

Adatbázis migrációk létrehozása

Az adatbázis verziókezelését a migrációk segítségével valósítjuk meg, amelyekhez a Laravel biztosít egy Schema Builder komponenst is. Új migrációt az alábbi paranccsal tudunk létrehozni:

php artisan migrate:make <migracio_neve>

Tipp! Az Artisan a Laravel beépített konzolos fejlesztői eszköze, aminek további lehetőségeit a php artisan parancs futtatásával érhetjük el.

A migrációt reprezentáló osztály az <alkalmazas_helye>/app/database/migrations/ mappában az aktuális dátumból és megadott névből összeállított fájlnévvel jön létre. Ebben két metódust kell implementálnunk, amelyek közül az egyik végrehajtja, a másik visszavonja a migrációt.

Tipp! A felhasználók táblához tartozó migrációt nem mutatom be, de első lépésben ajánlott azt létrehozni.

Szerepkörök tábla

Hozzuk létre a migrációt az alábbi parancs segítségével:

php artisan migrate:make create_roles_table --create=roles

Tipp! A --create opcióval megadhatjuk az új tábla nevét.

A tábla a szerepkör azonosítóját, nevét és leírását tartalmazza. A későbbi jogosultság ellenőrzések során a name mező alapján hivatkozunk a szerepkörre, ezért unique megszorítást alkalmazunk.

// ...
public function up()
{
    Schema::create('roles', function(Blueprint $table)
    {
        $table->increments('id');
        $table->string('name', 64)->unique();
        $table->string('description', 128);
    });
}
// ...

Engedélyek tábla

Hozzuk létre a migrációt az alábbi parancs segítségével:

php artisan migrate:make create_permissions_table --create=permissions

A tábla az engedély azonosítóját, nevét és leírását tartalmazza. A későbbi jogosultság ellenőrzések során a name mező alapján hivatkozunk az engedélyre, ezért unique megszorítást alkalmazunk.

// ...
public function up()
{
    Schema::create('permissions', function(Blueprint $table)
    {
        $table->increments('id');
        $table->string('name', 64)->unique();
        $table->string('description', 128);
    });
}
// ...

Szerepkör - engedélyek kapcsolótábla

Hozzuk létre a migrációt az alábbi parancs segítségével:

php artisan migrate:make create_role_permissions_table --create=role_permissions

A tábla a szerepkör és a hozzárendelt engedély azonosítóját tartalmazza. Idegen kulcsok segítségével hivatkozunk az eredeti táblákra.

Tipp! Külső táblák azonosítójának tárolásához használjuk az unsignedInteger() metódust.

// ...
public function up()
{
    Schema::create('role_permissions', function(Blueprint $table)
    {
        $table->unsignedInteger('role_id');
        $table->foreign('role_id')
            ->references('id')->on('roles')
            ->onDelete('cascade')->onUpdate('cascade');
        $table->unsignedInteger('permission_id');
        $table->foreign('permission_id')
            ->references('id')->on('permissions')
            ->onDelete('cascade')->onUpdate('cascade');
        $table->primary(array('role_id', 'permission_id'));
    });
}
// ...

Felhasználó - szerepkörök kapcsolótábla

Hozzuk létre a migrációt az alábbi parancs segítségével:

php artisan migrate:make create_user_roles_table --create=user_roles

A tábla a felhasználó és a hozzárendelt szerepkör azonosítóját tartalmazza. Idegen kulcsok segítségével hivatkozunk az eredeti táblákra.

// ...
public function up()
{
    Schema::create('user_roles', function(Blueprint $table)
    {
        $table->unsignedInteger('user_id');
        $table->foreign('user_id')
            ->references('id')->on('users')
            ->onDelete('cascade')->onUpdate('cascade');
        $table->unsignedInteger('role_id');
        $table->foreign('role_id')
            ->references('id')->on('roles')
            ->onDelete('cascade')->onUpdate('cascade');
        $table->primary(array('user_id', 'role_id'));
    });
}
// ...

Migrációk futtatása

Ha elkészültünk a migrációk megírásával, futtassuk le a következő parancsot:

php artisan migrate

Modellek testreszabása

A modell osztályokat a konvenció szerint az <alkalmazas_helye>/app/models/ mappába helyezzük el, így az automatikus osztálybetöltő megtalálja azokat.

Engedély modell

Hozzuk létre a Permission nevű modell osztályt, és származtassuk a Eloquent ősosztályból.

class Permission extends Eloquent {}

Ezután állítsuk be a modellhez tartozó adattáblát, és kapcsoljuk ki az időbélyegek támogatását.

// Permission.php
// ...
protected $table = 'permissions';

public $timestamps = false;
// ...

Majd készítsük el a roles() relációt a korábban létrehozott role_permissions kapcsolótábla segítségével.

// Permission.php
// ...
public function roles()
{
    return $this->belongsToMany('Role', 'role_permissions');
}
// ...

Szerepkör modell

Hozzuk létre a Role nevű modell osztályt, és származtassuk a Eloquent ősosztályból.

class Role extends Eloquent {}

Ezután állítsuk be a modellhez tartozó adattáblát, és kapcsoljuk ki az időbélyegek támogatását.

// Role.php
// ...
protected $table = 'roles';

public $timestamps = false;
// ...

Adjuk hozzá a users() és a permissions() relációt a korábban létrehozott user_roles és role_permissions kapcsolótáblák segítségével.

// Role.php
// ...
public function users()
{
    return $this->belongsToMany('User', 'user_roles');
}

public function permissions()
{
    return $this->belongsToMany('Permission', 'role_permissions');
}
// ...

Majd készítsük el a hasPermission() engedélyt ellenőrző metódust.

Tipp! Ha a relációkhoz az Eloquent ORM által támogatott virtuális adattagokat használjuk, akkor csak első alkalommal kéri le a hozzárendelt rekordokat.

// Role.php
// ...
public function hasPermission($name)
{
    return in_array($name, $this->permissions->lists('name'));
}
// ..

Felhasználó modell

A felhasználó modell már létrejött az alkalmazás generálásakor, így csak az említett migrációt kellett hozzá létrehozni. Első lépésként hozzáadjuk a roles() relációt a user_roles kapcsolótáblán keresztül.

// User.php
// ...
public function roles()
{
    return $this->belongsToMany('Role', 'user_roles');
}
// ...

Ezután készítsük el a hasRole() szerepkört és a hasPermission() engedélyt ellenőrző metódust.

// User.php
// ...
public function hasRole($name)
{
    return in_array($name, $this->roles->lists('name'));
}

public function hasPermission($name)
{
    foreach ($this->roles as $role)
    {
        if ($role->hasPermission($name))
        {
            return true;
        }
    }
    return false;
}
// ...

Szűrők létrehozása

Mint a legtöbb mai keretrendszer, a Laravel is támogatja az útvonalaknál használható szűrőket, amelyek az útvonalhoz rendelt metódus lefuttatása előtt vagy után hajtódnak végre. Az alapértelmezett szűrőket az <alkalmazas_helye>/app/filters.php fájlban találjuk névtelen függvények formájában. Bővítsük ki egy role és egy permission szűrővel, amelyek ellenőrzik a paraméterben átadott szerepköröket vagy engedélyeket. Ha a felhasználó nem rendelkezik ezekkel, az alkalmazás kilép HTTP 403 hibakóddal.

Tipp! A két szűrő csak a bejelentkezett felhasználók szerepköreit vagy jogosultságait ellenőrzi, ezért használjuk együtt az alapértelmezett auth szűrővel.

Szerepkör ellenőrzése

// filters.php
// ...
Route::filter('role', function()
{
    if (($user = Auth::user()))
    {
        $roles = array_slice(func_get_args(), 2);

        foreach ($roles as $role)
        {
            if ( ! $user->hasRole($role))
            {
                App::abort(403);
            }
        }
    }
});
// ...

Engedély ellenőrzése

// filters.php
// ...
Route::filter('permission', function()
{
    if (($user = Auth::user()))
    {
        $permissions = array_slice(func_get_args(), 2);

        foreach ($permissions as $permission)
        {
            if ( ! $user->hasPermission($permission))
            {
                App::abort(403);
            }
        }
    }
});
// ...