Weryfikacja uprawnień (Authorization)
Autoryzacja określa, czy użytkownik ma wystarczające uprawnienia do np. dostępu do zasobu lub wykonania akcji. Autoryzacja zakłada wcześniejsze udane uwierzytelnienie, czyli to, że użytkownik jest zalogowany.
W przykładach użyjemy obiektu klasy Nette\Security\User, który reprezentuje aktualnego
użytkownika i do którego można uzyskać dostęp, zlecając jego przekazanie przez dependency injection. W prezenterze wystarczy
wywołać $user = $this->getUser()
.
Dla bardzo prostych witryn z administracją, gdzie uprawnienia użytkowników nie są rozróżniane, jako kryterium
autoryzacji można zastosować znaną metodę isLoggedIn()
Innymi słowy, gdy użytkownik jest zalogowany, ma
wszystkie uprawnienia i odwrotnie.
if ($user->isLoggedIn()) { // czy użytkownik jest zalogowany?
deleteItem(); // wtedy ma uprawnienia do wykonania operacji
}
Rola
Celem ról jest zaoferowanie bardziej precyzyjnej kontroli uprawnień i zachowanie niezależności od nazwy użytkownika.
Każdemu użytkownikowi zaraz po zalogowaniu zostanie przypisana jedna lub więcej ról, w których będzie działał. Role mogą
być prostymi ciągami znaków, takimi jak admin
, member
, guest
, itd. Jest on podawany jako
drugi parametr konstruktora SimpleIdentity
, jako łańcuch lub tablica łańcuchów – ról.
Jako kryterium autoryzacji używamy teraz metody isInRole()
, która ujawnia, czy użytkownik jest w
danej roli:
if ($user->isInRole('admin')) { // czy użytkownik jest w roli administratora?
deleteItem(); // wtedy ma uprawnienia do wykonania operacji
}
Jak już wiesz, kiedy użytkownik się wylogowuje, jego tożsamość nie musi być usunięta. Tak więc metoda
getIdentity()
nadal zwraca obiekt SimpleIdentity
, łącznie z przyznanymi rolami. Nette Framework
przestrzega zasady “mniej kodu, więcej bezpieczeństwa”, gdzie mniej wpisywania prowadzi do bardziej bezpiecznego kodu, więc
nie trzeba sprawdzać, czy użytkownik jest nadal zalogowany podczas odkrywania ról. Metoda isInRole()
działa
z efektywnymi rolami, czyli jeśli użytkownik jest zalogowany, to bazuje na rolach określonych w tożsamości, jeśli
nie jest zalogowany, to automatycznie ma specjalną rolę guest
.
Autor
Oprócz ról wprowadzamy pojęcia zasobu i działania:
- role to właściwość użytkownika – np. moderator, redaktor, gość, zarejestrowany użytkownik, administrator…
- zasób (zasób) to logiczny element serwisu – artykuł, strona, użytkownik, pozycja menu, ankieta, prezenter, …
- operacja (operacja) to pewne konkretne działanie, które użytkownik może lub nie może wykonać z zasobem – na przykład usunąć, edytować, stworzyć, zagłosować, …
Autoryzator to obiekt, który decyduje, czy dana role ma uprawnienia do wykonania określonej operacji
z określonym zasobem. Jest to obiekt implementujący interfejs Nette\Security\Authorizator z jedną 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;
}
}
Dodaj authorizer do konfiguracji jako usługę kontenera DI:
services:
- MyAuthorizator
Poniżej przedstawiono przykładowy przypadek użycia. Zauważ, że tym razem wywołujemy metodę
Nette\Security\User::isAllowed()
, a nie Authorizer, więc pierwszego parametru $role
nie ma. Metoda ta
wywołuje MyAuthorizator::isAllowed()
sekwencyjnie dla wszystkich ról użytkownika i zwraca true, jeśli
przynajmniej jedna z nich ma uprawnienia.
if ($user->isAllowed('file')) { // czy użytkownik może coś zrobić z zasobem 'plik'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // może zrobić 'delete' na zasobie 'plik'?
deleteFile();
}
Oba parametry są opcjonalne, domyślna wartość null
ma znaczenie anything.
Pozwolenie ACL
Nette posiada wbudowaną implementację autoryzatora, klasę Nette\Security\Permission, dostarczającą programiście lekką i elastyczną warstwę ACL (Access Control List) dla uprawnień i kontroli dostępu. Praca z nim polega na definiowaniu ról, zasobów i indywidualnych uprawnień. Role i zasoby pozwalają na tworzenie hierarchii. Aby to wyjaśnić, pokażemy przykład aplikacji internetowej:
guest
: niezalogowany gość, który może czytać i przeglądać publiczną część strony, tj. czytać artykuły, komentarze i głosować w ankietachregistered
: zalogowany zarejestrowany użytkownik, który może również komentowaćadmin
: może zarządzać artykułami, komentarzami i ankietami
Mamy więc zdefiniowane pewne role (guest
, registered
i admin
) i wymienione zasoby
(article
, comment
, poll
), do których użytkownicy z daną rolą mają dostęp lub
wykonują pewne operacje (view
, vote
, add
, edit
).
Utwórz instancję klasy Permission i zdefiniuj role. Możemy wykorzystać tzw. dziedziczenie ról, które zapewnia,
że np. użytkownik z rolą administratora (admin
) może robić to, co zwykły odwiedzający stronę (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'); // i 'admin' po nim dziedziczy
Teraz zdefiniujemy również listę zasobów, do których użytkownicy mogą mieć dostęp.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Nawet zasoby mogą używać dziedziczenia, możliwe byłoby na przykład wpisanie
$acl->addResource('perex', 'article')
.
A teraz najważniejsza rzecz. Zdefiniujmy zasady między nimi, aby określić, kto może zrobić co z czym:
//po pierwsze, nikt nie może nic zrobić
// Pozwól gościom przeglądać artykuły, komentarze i ankiety
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// i głosować w ankietach
$acl->allow('guest', 'poll', 'vote');
// osoba zameldowana dziedziczy prawa po gościu, dajemy jej prawo do komentowania
$acl->allow('registered', 'comment', 'add');
// administrator może przeglądać i edytować wszystko
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
A co jeśli chcemy ograniczać komuś dostęp do określonego zasobu?
// Administrator nie może edytować ankiet, to byłoby niedemokratyczne
$acl->deny('admin', 'poll', 'edit');
Teraz, gdy mamy już listę zasad, możemy po prostu zadać pytania autoryzacyjne:
// czy goście mogą oglądać artykuły?
$acl->isAllowed('guest', 'article', 'view'); // true
// czy goście mogą edytować artykuły?
$acl->isAllowed('guest', 'article', 'edit'); // false
// czy gość może głosować w ankietach?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// czy gość może komentować?
$acl->isAllowed('guest', 'comment', 'add'); // false
To samo dotyczy zarejestrowanego użytkownika, ale on również może 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
Komunikaty mogą być również oceniane dynamicznie, a decyzję możemy pozostawić własnemu callbackowi, do którego przekazywane są wszystkie parametry:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Ale jak poradzić sobie z sytuacją, w której np. nie wystarczą tylko nazwy ról i zasobów, ale chcielibyśmy
zdefiniować, że np. rola registered
może edytować zasób article
tylko wtedy, gdy jest jego autorem?
Zamiast używać ciągów znaków będziemy używać obiektów, rolą będzie obiekt Nette\Security\Role a zasobem Nette\Security\Resource. Ich metody
getRoleId()
i getResourceId()
zwrócą odpowiednio oryginalne 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 tworzymy regułę:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // objekt Registered
$resource = $acl->getQueriedResource(); // objekt Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
A zapytanie ACL odbywa się poprzez 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. Ale co się stanie, jeśli jeden przodek ma wyłączone działanie, a drugi włączone? Jakie będą prawa zstępnego? Jest to określone przez wagę roli – ostatnia wymieniona rola na liście przodków ma największą wagę, pierwsza wymieniona rola najmniejszą. To jest bardziej obrazowe 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 administratora ma mniejszą wagę niż rola gościa
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// przypadek B: rola admin ma większą wagę niż gość
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Role i zasoby mogą być również usuwane (removeRole()
, removeResource()
), reguły mogą być
odwracane (removeAllow()
, removeDeny()
). Tablica wszystkich ról bezpośrednio rodzicielskich zwraca
getRoleParents()
, to czy dwie encje dziedziczą po sobie zwraca roleInheritsFrom()
i
resourceInheritsFrom()
.
Dodawanie jako usługa
Musimy przekazać ACL, który stworzyliśmy jako usługę do konfiguracji, aby mógł być używany przez obiekt
$user
, czyli aby mógł być używany w kodzie takim jak $user->isAllowed('article', 'view')
. Aby to
zrobić, napiszemy na nim 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 dodaj go do konfiguracji:
services:
- App\Model\AuthorizatorFactory::create
W presenterech można wtedy zweryfikować uprawnienia, na przykład w metodzie startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}