Zugriffskontrolle (Autorisierung)
Die Autorisierung prüft, ob ein Benutzer ausreichende Berechtigungen hat, um zum Beispiel auf eine bestimmte Ressource zuzugreifen oder eine Aktion auszuführen. Die Autorisierung setzt eine zuvor erfolgreiche Authentifizierung voraus, also dass der Benutzer angemeldet ist.
→ Installation und Anforderungen
In den Beispielen verwenden wir ein Objekt der Klasse Nette\Security\User, das den aktuellen Benutzer
repräsentiert und das Sie sich per Dependency
Injection übergeben lassen. In Presentern genügt der Aufruf $user = $this->getUser().
Bei sehr einfachen Websites mit einer Administration, in denen die Berechtigungen der Benutzer nicht unterschieden werden,
lässt sich als Kriterium für die Autorisierung die bereits bekannte Methode isLoggedIn() verwenden. 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 Zweck von Rollen ist, eine genauere Kontrolle über die Berechtigungen zu bieten und dabei vom Benutzernamen unabhängig zu
bleiben. Sobald sich ein Benutzer anmeldet, werden ihm eine oder mehrere Rollen zugewiesen, in denen er handelt. Rollen können
einfache Strings sein, zum Beispiel admin, member, guest usw. Sie werden als zweites
Argument des Konstruktors von SimpleIdentity angegeben, entweder als String oder als Array von Strings – den
Rollen.
Als Kriterium für die Autorisierung verwenden wir nun die Methode isInRole(), die verrät, ob der Benutzer in der
angegebenen Rolle handelt:
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 das Abmelden des Benutzers seine Identity nicht löschen. Die Methode getIdentity()
gibt also weiterhin das Objekt SimpleIdentity samt allen gewährten Rollen zurück. Das Nette Framework bekennt sich
zum Grundsatz “weniger Code, mehr Sicherheit”, bei dem weniger Schreibarbeit zu sichererem Code führt. Deshalb müssen Sie
beim Prüfen von Rollen nicht zusätzlich prüfen, ob der Benutzer angemeldet ist. Die Methode isInRole() arbeitet
mit den effektiven Rollen: Ist der Benutzer angemeldet, stützt sie sich auf die in der Identity angegebenen Rollen; ist er
nicht angemeldet, hat er automatisch die besondere Rolle guest (oder die Rollen der Gast-Identity, falls eine bereitgestellt wird).
Autorisator
Zusätzlich zu den Rollen führen wir die Begriffe Ressource und Operation ein:
- Rolle ist eine Eigenschaft des Benutzers – z. B. Moderator, Redakteur, Besucher, registrierter Benutzer, Administrator …
- Ressource ist eine logische Einheit der Website – Artikel, Seite, Benutzer, Menüeintrag, Umfrage, Presenter, …
- Operation ist eine konkrete Tätigkeit, die der Benutzer mit der Ressource ausführen darf oder nicht – z. B. ansehen, bearbeiten, löschen, abstimmen, …
Ein Autorisator ist ein Objekt, das entscheidet, ob die gegebene Rolle die Berechtigung hat, eine bestimmte
Operation mit einer bestimmten Ressource auszuführen. Es ist ein Objekt, das das Interface Nette\Security\Authorizator mit der 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 der Konfiguration als Service des DI-Containers hinzu:
services:
- MyAuthorizator
Und hier ein Beispiel für die Verwendung. Beachten Sie: Diesmal rufen wir die Methode
Nette\Security\User::isAllowed() auf und nicht die des Autorisators, deshalb 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 von ihnen die Berechtigung hat.
if ($user->isAllowed('file')) { // darf der Benutzer irgendetwas mit der Ressource 'file' tun?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // darf der Benutzer 'delete' auf der Ressource 'file' ausführen?
deleteFile();
}
Beide Argumente sind optional; der Standardwert null bedeutet irgendetwas.
Permission ACL
Nette bringt eine eingebaute Implementierung des Autorisators mit, die Klasse Nette\Security\Permission, die dem Programmierer eine leichte und flexible Schicht für ACL (Access Control List) zur Verwaltung von Berechtigungen und Zugriffen bietet. Die Arbeit damit besteht darin, Rollen, Ressourcen und die einzelnen Berechtigungen zu definieren. Rollen und Ressourcen erlauben das Bilden von Hierarchien. Zur Erklärung zeigen wir ein Beispiel für eine Webanwendung:
guest: ein nicht registrierter Besucher, der den öffentlichen Teil der Website lesen und durchsehen kann, also Artikel und Kommentare lesen und in Umfragen abstimmen darf.registered: ein registrierter, angemeldeter Benutzer, der zusätzlich Kommentare schreiben darf.admin: darf Artikel, Kommentare und Umfragen verwalten.
Wir haben bestimmte Rollen definiert (guest, registered und admin) und Ressourcen
genannt (article, comment, poll), auf die Benutzer mit einer bestimmten Rolle zugreifen
oder mit denen sie bestimmte Operationen ausführen dürfen (view, vote, add,
edit).
Wir erzeugen eine Instanz der Klasse Permission und definieren die Rollen. Man kann die sogenannte Rollenvererbung
nutzen, die dafür sorgt, dass ein Benutzer mit der Rolle admin zum Beispiel auch das kann, was ein gewöhnlicher
Besucher der Website kann (und natürlich mehr).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' erbt von 'guest'
$acl->addRole('admin', 'registered'); // 'admin' erbt von 'registered'
Nun definieren wir die Liste der Ressourcen, auf die Benutzer zugreifen können.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Auch Ressourcen können Vererbung nutzen; es wäre zum Beispiel möglich, $acl->addResource('perex', 'article')
einzutragen.
Und nun der wichtigste Teil. Wir definieren die Regeln dazwischen, die bestimmen, wer was womit tun darf:
// zu Beginn darf niemand irgendetwas
// lassen wir den Gast Artikel, Kommentare und Umfragen ansehen
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// und außerdem in Umfragen abstimmen
$acl->allow('guest', 'poll', 'vote');
// registered erbt die Berechtigungen von guest, geben wir ihm zusätzlich das Recht zu kommentieren
$acl->allow('registered', 'comment', 'add');
// der Administrator darf alles ansehen und bearbeiten
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Was, wenn wir jemandem den Zugriff auf eine bestimmte Ressource verwehren wollen?
// der Administrator darf Umfragen nicht bearbeiten, das wäre undemokratisch
$acl->deny('admin', 'poll', 'edit');
Nachdem wir nun den Satz von Regeln erstellt haben, können wir einfach Autorisierungsanfragen stellen:
// darf guest Artikel ansehen?
$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, der aber außerdem kommentieren darf:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
Der Administrator darf alles bearbeiten, außer Umfragen:
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
Berechtigungen lassen sich auch dynamisch auswerten, 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);
Aber wie geht man mit einer Situation um, in der die bloßen Namen von Rollen und Ressourcen nicht genügen und wir zum
Beispiel festlegen wollen, dass die Rolle registered die Ressource article nur dann bearbeiten darf,
wenn sie deren Autor ist? Wir verwenden statt Strings Objekte; die Rolle wird ein Objekt Nette\Security\Role und die Ressource ein Objekt Nette\Security\Resource. Ihre Methoden
getRoleId() bzw. getResourceId() geben die ursprünglichen Strings 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 Anfrage an die ACL erfolgt durch die Übergabe der Objekte:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Eine Rolle kann von einer Rolle oder von mehreren Rollen erben. Aber was passiert, wenn einem Vorfahren die Aktion verboten und dem anderen erlaubt ist? Welche Rechte hat dann der Nachkomme? Das bestimmt das Gewicht der Rolle – die in der Liste der Vorfahren zuletzt genannte Rolle hat das höchste Gewicht, die erste das niedrigste. Am Beispiel ist das anschaulicher:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// Fall A: die Rolle admin hat ein geringeres Gewicht als die Rolle guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// Fall B: die Rolle admin hat ein größeres Gewicht als die Rolle guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Rollen und Ressourcen lassen sich auch entfernen (removeRole(), removeResource()), und Regeln lassen
sich zurücknehmen (removeAllow(), removeDeny()). Das Array aller direkten Elternrollen gibt
getRoleParents() zurück. Ob zwei Entities voneinander erben, geben roleInheritsFrom() und
resourceInheritsFrom() zurück. Die Existenz einer Rolle oder Ressource prüfen hasRole() und
hasResource(), die Listen aller registrierten Rollen und Ressourcen geben getRoles() und
getResources() zurück, und mit removeAllRoles() und removeAllResources() lässt sich alles
löschen.
Als Service hinzufügen
Die von uns erstellte ACL müssen wir der Konfiguration als Service übergeben, damit das Objekt $user sie
verwendet, also damit sich im Code $user->isAllowed('article', 'view') nutzen lässt. Dazu schreiben wir eine
Factory für sie:
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 ergänzen sie in der Konfiguration:
services:
- App\Model\AuthorizatorFactory::create
In Presentern können Sie die Berechtigungen dann zum Beispiel in der Methode startup() prüfen:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}