Berechtigungsprüfung (Autorisierung)
Autorisierung stellt fest, ob ein Benutzer ausreichende Berechtigungen hat, beispielsweise um auf eine bestimmte Ressource zuzugreifen oder eine bestimmte Aktion auszuführen. Autorisierung setzt eine vorherige erfolgreiche Authentifizierung voraus, d.h., dass der Benutzer angemeldet ist.
→ Installation und Anforderungen
In den Beispielen verwenden wir das Objekt der Klasse Nette\Security\User, das den aktuellen Benutzer
repräsentiert und auf das Sie zugreifen können, indem Sie es sich mittels Dependency Injection übergeben lassen. In
Presentern genügt es, $user = $this->getUser()
aufzurufen.
Bei sehr einfachen Websites mit Administration, bei denen keine Benutzerberechtigungen unterschieden werden, kann als
Autorisierungskriterium die bereits bekannte Methode isLoggedIn()
verwendet werden. Mit anderen Worten: Sobald ein
Benutzer angemeldet ist, hat er alle Berechtigungen und umgekehrt.
if ($user->isLoggedIn()) { // ist der Benutzer angemeldet?
deleteItem(); // dann hat er die Berechtigung für die Operation
}
Rollen
Der Sinn von Rollen besteht darin, eine präzisere Steuerung der Berechtigungen zu ermöglichen und unabhängig vom
Benutzernamen zu bleiben. Jedem Benutzer weisen wir gleich bei der Anmeldung eine oder mehrere Rollen zu, in denen er auftreten
wird. Rollen können einfache Zeichenketten sein, wie z. B. admin
, member
, guest
usw. Sie
werden als zweiter Parameter des Konstruktors SimpleIdentity
angegeben, entweder als Zeichenkette oder als Array von
Zeichenketten – Rollen.
Als Autorisierungskriterium verwenden wir nun die Methode isInRole()
, die angibt, ob der Benutzer in der gegebenen
Rolle auftritt:
if ($user->isInRole('admin')) { // ist der Benutzer in der Rolle admin?
deleteItem(); // dann hat er die Berechtigung für die Operation
}
Wie Sie bereits wissen, muss nach der Abmeldung des Benutzers seine Identität nicht gelöscht werden. Das heißt, die Methode
getIdentity()
gibt weiterhin das Objekt SimpleIdentity
zurück, einschließlich aller gewährten Rollen.
Das Nette Framework folgt dem Prinzip „weniger Code, mehr Sicherheit“, bei dem weniger Schreiben zu sicherem Code führt.
Daher müssen Sie bei der Überprüfung von Rollen nicht zusätzlich prüfen, ob der Benutzer angemeldet ist. Die Methode
isInRole()
arbeitet mit effektiven Rollen, d. h., wenn der Benutzer angemeldet ist, basiert sie auf den in der
Identität angegebenen Rollen; wenn er nicht angemeldet ist, hat er automatisch die spezielle Rolle guest
.
Autorisator
Neben Rollen führen wir noch die Begriffe Ressource und Operation ein:
- Rolle ist eine Eigenschaft des Benutzers – z. B. Moderator, Redakteur, Besucher, registrierter Benutzer, Administrator…
- Ressource (resource) ist ein logisches Element der Website – Artikel, Seite, Benutzer, Menüpunkt, Umfrage, Presenter, …
- Operation (operation) ist eine bestimmte Tätigkeit, die der Benutzer mit der Ressource tun oder nicht tun darf – z. B. löschen, bearbeiten, erstellen, abstimmen, …
Ein Autorisator ist ein Objekt, das entscheidet, ob eine gegebene Rolle die Berechtigung hat, eine bestimmte
Operation mit einer bestimmten Ressource durchzuführen. Es handelt sich um ein Objekt, das das Interface Nette\Security\Authorizator mit einer einzigen
Methode isAllowed()
implementiert:
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;
}
}
Den Autorisator fügen wir zur Konfiguration als Dienst des DI-Containers hinzu:
services:
- MyAuthorizator
Und hier ist ein Anwendungsbeispiel. Achtung, diesmal rufen wir die Methode Nette\Security\User::isAllowed()
auf,
nicht den Autorisator, daher fehlt der erste Parameter $role
. Diese Methode ruft
MyAuthorizator::isAllowed()
nacheinander für alle Rollen des Benutzers auf und gibt true zurück, wenn mindestens
eine davon die Berechtigung hat.
if ($user->isAllowed('file')) { // darf der Benutzer irgendetwas mit der Ressource 'file' tun?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // darf er über die Ressource 'file' die Operation 'delete' ausführen?
deleteFile();
}
Beide Parameter sind optional, der Standardwert null
bedeutet alles.
Permission ACL
Nette kommt mit einer eingebauten Implementierung eines Autorisators, der Klasse Nette\Security\Permission, die dem Programmierer eine leichte und flexible ACL (Access Control List) Schicht zur Verwaltung von Berechtigungen und Zugriffen bietet. Die Arbeit damit besteht darin, Rollen, Ressourcen und einzelne Berechtigungen zu definieren. Dabei ermöglichen Rollen und Ressourcen die Erstellung von Hierarchien. Zur Erklärung zeigen wir ein Beispiel einer Webanwendung:
guest
: nicht angemeldeter Besucher, der den öffentlichen Teil der Website lesen und durchsuchen kann, d.h. Artikel, Kommentare lesen und in Umfragen abstimmen kannregistered
: angemeldeter registrierter Benutzer, der zusätzlich kommentieren kannadmin
: kann Artikel, Kommentare und Umfragen verwalten
Wir haben also bestimmte Rollen (guest
, registered
und admin
) definiert und Ressourcen
(article
, comment
, poll
) erwähnt, auf die Benutzer mit einer bestimmten Rolle zugreifen
oder bestimmte Operationen (view
, vote
, add
, edit
) ausführen können.
Wir erstellen eine Instanz der Klasse Permission und definieren die Rollen. Dabei kann die sogenannte Rollenvererbung
genutzt werden, die sicherstellt, dass z. B. ein Benutzer mit der Rolle Administrator (admin
) auch das tun kann, was
ein normaler Website-Besucher tun kann (und natürlich noch mehr).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' erbt von 'guest'
$acl->addRole('admin', 'registered'); // und davon erbt 'admin'
Nun definieren wir auch die Liste der Ressourcen, auf die Benutzer zugreifen können.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Auch Ressourcen können Vererbung verwenden, es wäre beispielsweise möglich,
$acl->addResource('perex', 'article')
anzugeben.
Und jetzt das Wichtigste. Wir definieren zwischen ihnen Regeln, die festlegen, wer was mit was tun darf:
// zuerst darf niemand etwas tun
// lasse guest Artikel, Kommentare und Umfragen anzeigen
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// und in Umfragen zusätzlich abstimmen
$acl->allow('guest', 'poll', 'vote');
// registrierter erbt Rechte von guest, geben wir ihm zusätzlich das Recht zu kommentieren
$acl->allow('registered', 'comment', 'add');
// Administrator kann alles anzeigen und bearbeiten
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Was ist, wenn wir jemandem den Zugriff auf eine bestimmte Ressource verweigern wollen?
// Administrator kann Umfragen nicht bearbeiten, das wäre undemokratisch
$acl->deny('admin', 'poll', 'edit');
Nun, da wir die Liste der Regeln erstellt haben, können wir einfach Autorisierungsabfragen stellen:
// darf guest Artikel anzeigen?
$acl->isAllowed('guest', 'article', 'view'); // true
// darf guest Artikel bearbeiten?
$acl->isAllowed('guest', 'article', 'edit'); // false
// darf guest in Umfragen abstimmen?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// darf guest kommentieren?
$acl->isAllowed('guest', 'comment', 'add'); // false
Dasselbe gilt für einen registrierten Benutzer, dieser kann jedoch auch kommentieren:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
Der Administrator kann alles bearbeiten, außer Umfragen:
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
Berechtigungen können auch dynamisch ausgewertet werden, und wir können die Entscheidung einem eigenen Callback überlassen, dem alle Parameter übergeben werden:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Wie löst man aber beispielsweise eine Situation, in der die Namen von Rollen und Ressourcen nicht ausreichen, sondern wir
definieren möchten, dass beispielsweise die Rolle registered
die Ressource article
nur bearbeiten darf,
wenn sie ihr Autor ist? Anstelle von Zeichenketten verwenden wir Objekte, die Rolle ist ein Objekt Nette\Security\Role und die Ressource Nette\Security\Resource. Ihre Methoden
getRoleId()
bzw. getResourceId()
geben die ursprünglichen Zeichenketten zurück:
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';
}
}
Und nun erstellen wir die Regel:
$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);
Und die Abfrage an die ACL erfolgt durch Übergabe der Objekte:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Eine Rolle kann von einer anderen Rolle oder von mehreren Rollen erben. Was passiert aber, wenn ein Vorfahre die Aktion verboten und ein anderer erlaubt hat? Welche Rechte hat der Nachkomme? Dies wird durch das Gewicht der Rolle bestimmt – die zuletzt in der Liste der Vorfahren angegebene Rolle hat das höchste Gewicht, die zuerst angegebene Rolle das niedrigste. Dies wird anhand eines Beispiels deutlicher:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// Fall A: Rolle admin hat geringeres Gewicht als Rolle guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// Fall B: Rolle admin hat höheres Gewicht als guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Rollen und Ressourcen können auch entfernt werden (removeRole()
, removeResource()
), Regeln können
ebenfalls rückgängig gemacht werden (removeAllow()
, removeDeny()
). Das Array aller direkten
Elternrollen gibt getRoleParents()
zurück, ob zwei Entitäten voneinander erben, geben
roleInheritsFrom()
und resourceInheritsFrom()
zurück.
Hinzufügen als Dienste
Wir müssen unsere erstellte ACL als Dienst zur Konfiguration hinzufügen, damit das Objekt $user
sie verwenden
kann, d.h., damit wir im Code z. B. $user->isAllowed('article', 'view')
verwenden können. Zu diesem Zweck
schreiben wir eine Factory dafür:
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;
}
}
Und fügen sie zur Konfiguration hinzu:
services:
- App\Model\AuthorizatorFactory::create
In Presentern können Sie dann die Berechtigungen beispielsweise in der Methode startup()
überprüfen:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}