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.
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 direkt, 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
// Gast darf Artikel, Kommentare und Umfragen anzeigen
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// und in Umfragen zusätzlich abstimmen
$acl->allow('guest', 'poll', 'vote');
// Registrierter Benutzer erbt Rechte vom Gast, 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, das Nette\Security\Role implementiert, und die Ressource ein
Objekt, das Nette\Security\Resource
implementiert. 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);
}
}