Weryfikacja uprawnień (Autoryzacja)

Autoryzacja sprawdza, czy użytkownik ma wystarczające uprawnienia, na przykład do dostępu do określonego zasobu lub do wykonania jakiejś akcji. Autoryzacja zakłada wcześniejszą pomyślną autentykację, tj. że użytkownik jest zalogowany.

Instalacja i wymagania

W przykładach będziemy używać obiektu klasy Nette\Security\User, który reprezentuje aktualnego użytkownika i do którego dostaniesz się, prosząc o jego przekazanie za pomocą wstrzykiwania zależności. W presenterach wystarczy tylko wywołać $user = $this->getUser().

W bardzo prostych stronach internetowych z administracją, gdzie nie rozróżnia się uprawnień użytkowników, można jako kryterium autoryzacji użyć już znanej metody isLoggedIn(). Innymi słowy: jak tylko użytkownik jest zalogowany, ma wszelkie uprawnienia i na odwrót.

if ($user->isLoggedIn()) { // czy użytkownik jest zalogowany?
	deleteItem(); // wtedy ma uprawnienia do operacji
}

Role

Celem ról jest zaoferowanie dokładniejszego zarządzania uprawnieniami i pozostanie niezależnym od nazwy użytkownika. Każdemu użytkownikowi zaraz po zalogowaniu przypisujemy jedną lub więcej ról, w których będzie występował. Role mogą być prostymi ciągami znaków, na przykład admin, member, guest, itp. Podaje się je jako drugi parametr konstruktora SimpleIdentity, albo jako ciąg znaków, albo tablicę ciągów – ról.

Jako kryterium autoryzacji teraz użyjemy metody isInRole(), która powie, czy użytkownik występuje w danej roli:

if ($user->isInRole('admin')) { // czy użytkownik jest w roli admina?
	deleteItem(); // wtedy ma uprawnienia do operacji
}

Jak już wiesz, po wylogowaniu użytkownika nie musi się skasować jego tożsamość. Czyli nadal metoda getIdentity() zwraca obiekt SimpleIdentity, włącznie ze wszystkimi przyznanymi rolami. Nette Framework wyznaje zasadę „less code, more security”, gdzie mniej pisania prowadzi do bardziej zabezpieczonego kodu, dlatego przy sprawdzaniu ról nie musisz jeszcze weryfikować, czy użytkownik jest zalogowany. Metoda isInRole() pracuje z efektywnymi rolami, tj. jeśli użytkownik jest zalogowany, opiera się na rolach podanych w tożsamości, jeśli nie jest zalogowany, ma automatycznie specjalną rolę guest.

Autoryzator

Oprócz ról wprowadzimy jeszcze pojęcia zasobu i operacji:

  • rola to właściwość użytkownika – np. moderator, redaktor, gość, zarejestrowany użytkownik, administrator…
  • zasób (resource) to jakiś logiczny element strony – artykuł, strona, użytkownik, pozycja w menu, ankieta, presenter, …
  • operacja (operation) to jakaś konkretna czynność, którą użytkownik może lub nie może wykonać na zasobie – na przykład usunąć, edytować, utworzyć, głosować, …

Autoryzator to obiekt, który decyduje, czy dana rola ma pozwolenie na wykonanie określonej operacji na określonym zasobie. Jest to obiekt implementujący interfejs Nette\Security\Authorizator z jedyną metodą isAllowed():

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;
	}
}

Autoryzator dodamy do konfiguracji jako usługę kontenera DI:

services:
	- MyAuthorizator

A następuje przykład użycia. Uwaga, tym razem wywołujemy metodę Nette\Security\User::isAllowed(), a nie autoryzator, więc nie ma tam pierwszego parametru $role. Ta metoda wywołuje MyAuthorizator::isAllowed() kolejno dla wszystkich ról użytkownika i zwraca true, jeśli przynajmniej jedna z nich ma pozwolenie.

if ($user->isAllowed('file')) { // czy użytkownik może robić cokolwiek z zasobem 'file'?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // czy może na zasobie 'file' wykonać 'delete'?
	deleteFile();
}

Oba parametry są opcjonalne, domyślna wartość null ma znaczenie cokolwiek.

Permission ACL

Nette dostarcza wbudowaną implementację autoryzatora, a mianowicie klasę Nette\Security\Permission zapewniającą programiście lekką i elastyczną warstwę ACL (Access Control List) do zarządzania uprawnieniami i dostępami. Praca z nią polega na definicji ról, zasobów i poszczególnych uprawnień. Przy czym role i zasoby umożliwiają tworzenie hierarchii. Dla wyjaśnienia pokażemy przykład aplikacji internetowej:

  • guest: niezalogowany odwiedzający, który może czytać i przeglądać publiczną część strony, tzn. czytać artykuły, komentarze i głosować w ankietach
  • registered: zalogowany zarejestrowany użytkownik, który dodatkowo może komentować
  • admin: może zarządzać artykułami, komentarzami i ankietami

Zdefiniowaliśmy więc pewne role (guest, registered i admin) i wspomnieliśmy zasoby (article, comment, poll), do których użytkownicy z jakąś rolą mogą uzyskiwać dostęp lub wykonywać określone operacje (view, vote, add, edit).

Stworzymy instancję klasy Permission i zdefiniujemy role. Można przy tym wykorzystać tzw. dziedziczenie ról, które zapewni, że np. użytkownik z rolą administratora (admin) może robić również to, co zwykły odwiedzający strony (i oczywiście więcej).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' dziedziczy po 'guest'
$acl->addRole('admin', 'registered'); // a po nim dziedziczy 'admin'

Teraz zdefiniujemy również listę zasobów, do których użytkownicy mogą uzyskiwać dostęp.

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

Również zasoby mogą używać dziedziczenia, można by było na przykład podać $acl->addResource('perex', 'article').

A teraz najważniejsze. Zdefiniujemy między nimi reguły określające, kto co może z czym robić:

// najpierw nikt nie może robić nic

// niech guest może przeglądać artykuły, komentarze i ankiety
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// a w ankietach dodatkowo i głosować
$acl->allow('guest', 'poll', 'vote');

// zarejestrowany dziedziczy prawa od guesta, damy mu dodatkowo prawo komentowania
$acl->allow('registered', 'comment', 'add');

// administrator może przeglądać i edytować cokolwiek
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

Co jeśli chcemy komuś zabronić dostępu do określonego zasobu?

// administrator nie może edytować ankiet, to byłoby niedemokratyczne
$acl->deny('admin', 'poll', 'edit');

Teraz, gdy mamy stworzoną listę reguł, możemy łatwo zadawać pytania autoryzacyjne:

// czy guest może przeglądać artykuły?
$acl->isAllowed('guest', 'article', 'view'); // true

// czy guest może edytować artykuły?
$acl->isAllowed('guest', 'article', 'edit'); // false

// czy guest może głosować w ankietach?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// czy guest może komentować?
$acl->isAllowed('guest', 'comment', 'add'); // false

To samo dotyczy zarejestrowanego użytkownika, ten jednak może również komentować:

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

Administrator może edytować wszystko, oprócz ankiet:

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

Uprawnienia mogą być również oceniane dynamicznie i możemy pozostawić decyzję własnemu callbackowi, któremu zostaną przekazane wszystkie parametry:

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

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

Jak jednak np. rozwiązać sytuację, gdy nie wystarczą tylko nazwy ról i zasobów, ale chcielibyśmy zdefiniować, że np. rola registered może edytować zasób article tylko jeśli jest jego autorem? Zamiast ciągów znaków użyjemy obiektów, rola będzie obiektem Nette\Security\Role, a zasób Nette\Security\Resource. Ich metody getRoleId() resp. getResourceId() będą zwracać pierwotne ciągi znaków:

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';
	}
}

A teraz stworzymy regułę:

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

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

A zapytanie do ACL zostanie wykonane przez przekazanie obiektów:

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

Rola może dziedziczyć po innej roli lub po wielu rolach. Co się jednak stanie, jeśli jeden przodek ma akcję zabronioną, a drugi dozwoloną? Jakie będą prawa potomka? Określa się to według wagi roli – ostatnia podana rola w liście przodków ma największą wagę, pierwsza podana rola tę najmniejszą. Bardziej obrazowe jest to z przykładu:

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

$acl->addResource('backend');

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

// przypadek A: rola admin ma mniejszą wagę niż rola guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// przypadek B: rola admin ma większą wagę niż guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Role i zasoby można również usuwać (removeRole(), removeResource()), można również odwracać reguły (removeAllow(), removeDeny()). Tablicę wszystkich bezpośrednich ról rodzicielskich zwraca getRoleParents(), czy dwie encje dziedziczą po sobie zwraca roleInheritsFrom() i resourceInheritsFrom().

Dodawanie jako usługi

Nasze stworzone ACL musimy przekazać do konfiguracji jako usługę, aby zaczął go używać obiekt $user, czyli aby było możliwe używanie w kodzie np. $user->isAllowed('article', 'view'). W tym celu napiszemy dla niego fabrykę:

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;
	}
}

I dodamy ją do konfiguracji:

services:
	- App\Model\AuthorizatorFactory::create

W presenterach następnie możesz weryfikować uprawnienia na przykład w metodzie startup():

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