Jogosultságok ellenőrzése (Autorizáció)

Az autorizáció azt vizsgálja, hogy a felhasználónak elegendő jogosultsága van-e például egy adott erőforráshoz való hozzáféréshez vagy egy adott művelet végrehajtásához. Az autorizáció feltételezi az előző sikeres authentikációt, azaz hogy a felhasználó be van jelentkezve.

Telepítés és követelmények

A példákban a Nette\Security\User osztály objektumát fogjuk használni, amely az aktuális felhasználót képviseli, és amelyhez úgy juthat hozzá, hogy dependency injection segítségével kéri át. A presenterekben elegendő csak a $user = $this->getUser() hívása.

Nagyon egyszerű, adminisztrációval rendelkező weboldalaknál, ahol nem különböztetik meg a felhasználói jogosultságokat, autorizációs kritériumként használható a már ismert isLoggedIn() metódus. Más szóval: amint a felhasználó be van jelentkezve, minden jogosultsággal rendelkezik, és fordítva.

if ($user->isLoggedIn()) { // be van jelentkezve a felhasználó?
	deleteItem(); // akkor van jogosultsága a művelethez
}

Szerepkörök

A szerepkörök célja, hogy pontosabb jogosultságkezelést kínáljanak, és függetlenek maradjanak a felhasználónévtől. Minden felhasználónak a bejelentkezéskor egy vagy több szerepkört rendelünk, amelyekben fellép. A szerepkörök lehetnek egyszerű stringek, például admin, member, guest, stb. A SimpleIdentity konstruktorának második paramétereként adjuk meg őket, vagy stringként, vagy stringek tömbjeként – szerepkörökként.

Autorizációs kritériumként most az isInRole() metódust használjuk, amely megmondja, hogy a felhasználó az adott szerepkörben lép-e fel:

if ($user->isInRole('admin')) { // a felhasználó admin szerepkörben van?
	deleteItem(); // akkor van jogosultsága a művelethez
}

Ahogy már tudja, a felhasználó kijelentkezése után nem feltétlenül törlődik az identitása. Tehát a getIdentity() metódus továbbra is visszaadja a SimpleIdentity objektumot, beleértve az összes megadott szerepkört. A Nette Framework a „kevesebb kód, több biztonság” elvét vallja, ahol a kevesebb írás biztonságosabb kódhoz vezet, ezért a szerepkörök ellenőrzésekor nem kell még azt is ellenőriznie, hogy a felhasználó be van-e jelentkezve. Az isInRole() metódus az effektív szerepkörökkel dolgozik, azaz ha a felhasználó be van jelentkezve, az identitásban megadott szerepkörökből indul ki, ha nincs bejelentkezve, automatikusan a speciális guest szerepkört kapja.

Autorizátor

A szerepkörökön kívül bevezetjük még az erőforrás és a művelet fogalmát:

  • szerepkör a felhasználó tulajdonsága – pl. moderátor, szerkesztő, látogató, regisztrált felhasználó, adminisztrátor…
  • erőforrás (resource) a weboldal valamilyen logikai eleme – cikk, oldal, felhasználó, menüpont, szavazás, presenter, …
  • művelet (operation) valamilyen konkrét tevékenység, amelyet a felhasználó megtehet vagy nem tehet meg az erőforrással – például törlés, szerkesztés, létrehozás, szavazás, …

Az autorizátor egy objektum, amely eldönti, hogy az adott szerepkörnek van-e engedélye egy bizonyos művelet végrehajtására egy bizonyos erőforrással. Ez egy objektum, amely implementálja a Nette\Security\Authorizator interfészt egyetlen isAllowed() metódussal:

class MyAuthorizator implements Nette\Security\Authorizator
{
	public function isAllowed($role, $resource, $operation): bool
	{
		if ($role === 'admin') {
			return true;
		}
		if ($role === 'user' && $resource === 'article') {
			return true;
		}

		// ...

		return false;
	}
}

Az autorizátort hozzáadjuk a DI konténer konfigurációjához szolgáltatásként:

services:
	- MyAuthorizator

És következik a használat példája. Figyelem, ezúttal a Nette\Security\User::isAllowed() metódust hívjuk, nem az autorizátort, tehát ott nincs az első $role paraméter. Ez a metódus a MyAuthorizator::isAllowed()-t hívja meg sorban a felhasználó összes szerepkörére, és true-t ad vissza, ha legalább egyiküknek van engedélye.

if ($user->isAllowed('file')) { // a felhasználó bármit megtehet a 'file' erőforrással?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // végrehajthatja a 'delete' műveletet a 'file' erőforráson?
	deleteFile();
}

Mindkét paraméter opcionális, az alapértelmezett null érték jelentése bármi.

Permission ACL

A Nette egy beépített autorizátor implementációval érkezik, ez a Nette\Security\Permission osztály, amely a programozónak egy könnyű és rugalmas ACL (Access Control List) réteget biztosít a jogosultságok és hozzáférések kezelésére. A vele való munka a szerepkörök, erőforrások és egyes jogosultságok definiálásából áll. A szerepkörök és erőforrások lehetővé teszik hierarchiák létrehozását. Magyarázatként megmutatunk egy példát egy webalkalmazásra:

  • guest: nem bejelentkezett látogató, aki olvashatja és böngészheti a weboldal nyilvános részét, azaz olvashat cikkeket, kommenteket és szavazhat a szavazásokon
  • registered: bejelentkezett regisztrált felhasználó, aki ráadásul kommentelhet is
  • admin: kezelheti a cikkeket, kommenteket és szavazásokat

Tehát definiáltunk bizonyos szerepköröket (guest, registered és admin), és említettünk erőforrásokat (article, comment, poll), amelyekhez a felhasználók valamilyen szerepkörrel hozzáférhetnek vagy bizonyos műveleteket végezhetnek (view, vote, add, edit).

Létrehozzuk a Permission osztály példányát, és definiáljuk a szerepköröket. Használhatjuk az ún. szerepkör öröklődést, amely biztosítja, hogy pl. egy adminisztrátor szerepkörrel (admin) rendelkező felhasználó megteheti azt is, amit egy átlagos weboldal látogató (és természetesen többet is).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // a 'registered' örököl a 'guest'-től
$acl->addRole('admin', 'registered'); // és tőle örököl az 'admin'

Most definiáljuk az erőforrások listáját is, amelyekhez a felhasználók hozzáférhetnek.

$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');

Az erőforrások is használhatnak öröklődést, lehetne például megadni $acl->addResource('perex', 'article').

És most a legfontosabb. Definiálunk közöttük szabályokat, amelyek meghatározzák, ki mit tehet mivel:

// először senki sem tehet semmit

// a guest nézhesse a cikkeket, kommenteket és szavazásokat
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// és a szavazásokon ráadásul szavazhasson is
$acl->allow('guest', 'poll', 'vote');

// a regisztrált örökli a guest jogait, adjunk neki ráadásul kommentelési jogot
$acl->allow('registered', 'comment', 'add');

// az adminisztrátor bármit megtekinthet és szerkeszthet
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

Mi van, ha valakinek meg akarjuk tiltani a hozzáférést egy bizonyos erőforráshoz?

// az adminisztrátor nem szerkesztheti a szavazásokat, az nem lenne demokratikus
$acl->deny('admin', 'poll', 'edit');

Most, hogy létrehoztuk a szabályok listáját, egyszerűen feltehetünk autorizációs kérdéseket:

// a guest megtekintheti a cikkeket?
$acl->isAllowed('guest', 'article', 'view'); // true

// a guest szerkesztheti a cikkeket?
$acl->isAllowed('guest', 'article', 'edit'); // false

// a guest szavazhat a szavazásokon?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// a guest kommentelhet?
$acl->isAllowed('guest', 'comment', 'add'); // false

Ugyanez érvényes a regisztrált felhasználóra is, ő azonban kommentelhet is:

$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false

Az adminisztrátor mindent szerkeszthet, kivéve a szavazásokat:

$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true

A jogosultságok dinamikusan is kiértékelhetők, és a döntést saját callbackre bízhatjuk, amelynek átadjuk az összes paramétert:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	return /* ... */;
};

$acl->allow('registered', 'comment', null, $assertion);

De hogyan kezeljük például azt a helyzetet, amikor nem elegendőek csak a szerepkörök és erőforrások nevei, hanem definiálni szeretnénk, hogy például a registered szerepkör csak akkor szerkesztheti az article erőforrást, ha ő a szerzője? Stringek helyett objektumokat használunk, a szerepkör egy Nette\Security\Role objektum lesz, az erőforrás pedig egy Nette\Security\Resource objektum. A getRoleId() ill. getResourceId() metódusaik visszaadják az eredeti stringeket:

class Registered implements Nette\Security\Role
{
	public $id;

	public function getRoleId(): string
	{
		return 'registered';
	}
}


class Article implements Nette\Security\Resource
{
	public $authorId;

	public function getResourceId(): string
	{
		return 'article';
	}
}

És most létrehozunk egy szabályt:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	$role = $acl->getQueriedRole(); // Registered objektum
	$resource = $acl->getQueriedResource(); // Article objektum
	return $role->id === $resource->authorId;
};

$acl->allow('registered', 'article', 'edit', $assertion);

És az ACL lekérdezése objektumok átadásával történik:

$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');

Egy szerepkör örökölhet egy másik szerepkörtől vagy több szerepkörtől. De mi történik, ha az egyik ősnek tiltva van egy művelet, a másiknak pedig engedélyezve? Milyenek lesznek az utód jogai? Ezt a szerepkör súlya határozza meg – az ősök listájában utoljára említett szerepkörnek van a legnagyobb súlya, az elsőként említett szerepkörnek a legkisebb. Ez a példából jobban látszik:

$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');

$acl->addResource('backend');

$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');

// A eset: az admin szerepkörnek kisebb a súlya, mint a guest szerepkörnek
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// B eset: az admin szerepkörnek nagyobb a súlya, mint a guestnek
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

A szerepköröket és erőforrásokat el is lehet távolítani (removeRole(), removeResource()), a szabályokat is vissza lehet vonni (removeAllow(), removeDeny()). Az összes közvetlen szülő szerepkör tömbjét a getRoleParents() adja vissza, azt, hogy két entitás örököl-e egymástól, a roleInheritsFrom() és a resourceInheritsFrom() adja vissza.

Hozzáadás szolgáltatásként

Az általunk létrehozott ACL-t át kell adnunk a konfigurációnak szolgáltatásként, hogy a $user objektum használni kezdje, tehát hogy a kódban használható legyen pl. $user->isAllowed('article', 'view'). Ehhez írunk rá egy factory-t:

namespace App\Model;

class AuthorizatorFactory
{
	public static function create(): Nette\Security\Permission
	{
		$acl = new Nette\Security\Permission;
		$acl->addRole(/* ... */);
		$acl->addResource(/* ... */);
		$acl->allow(/* ... */);
		return $acl;
	}
}

És hozzáadjuk a konfigurációhoz:

services:
	- App\Model\AuthorizatorFactory::create

A presenterekben ezután ellenőrizheti a jogosultságokat például a startup() metódusban:

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
verzió: 4.0