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 kann
  • registered: angemeldeter registrierter Benutzer, der zusätzlich kommentieren kann
  • admin: 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);
	}
}
Version: 4.0