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.
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 ankietachregistered
: 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);
}
}