Επαλήθευση αδειών (Authorization)

Η authorization διαπιστώνει εάν ο χρήστης έχει επαρκή δικαιώματα, για παράδειγμα, για πρόσβαση σε έναν συγκεκριμένο πόρο ή για την εκτέλεση κάποιας ενέργειας. Η authorization προϋποθέτει προηγούμενη επιτυχή authentication, δηλαδή ότι ο χρήστης είναι συνδεδεμένος.

Εγκατάσταση και απαιτήσεις

Στα παραδείγματα θα χρησιμοποιούμε το αντικείμενο της κλάσης Nette\Security\User, το οποίο αντιπροσωπεύει τον τρέχοντα χρήστη και στο οποίο μπορείτε να αποκτήσετε πρόσβαση ζητώντας να σας περαστεί μέσω dependency injection. Στους presenters αρκεί απλώς να καλέσετε $user = $this->getUser().

Σε πολύ απλούς ιστότοπους με διαχείριση, όπου δεν διακρίνονται τα δικαιώματα των χρηστών, είναι δυνατόν να χρησιμοποιηθεί ως κριτήριο authorization η ήδη γνωστή μέθοδος isLoggedIn(). Με άλλα λόγια: μόλις ο χρήστης συνδεθεί, έχει όλα τα δικαιώματα και αντίστροφα.

if ($user->isLoggedIn()) { // είναι ο χρήστης συνδεδεμένος;
	deleteItem(); // τότε έχει δικαίωμα για την ενέργεια
}

Ρόλοι

Ο σκοπός των ρόλων είναι να προσφέρουν ακριβέστερο έλεγχο των δικαιωμάτων και να παραμείνουν ανεξάρτητοι από το όνομα χρήστη. Σε κάθε χρήστη, αμέσως κατά τη σύνδεση, αναθέτουμε έναν ή περισσότερους ρόλους, με τους οποίους θα λειτουργεί. Οι ρόλοι μπορεί να είναι απλές συμβολοσειρές όπως admin, member, guest, κ.λπ. Αναφέρονται ως δεύτερη παράμετρος του constructor SimpleIdentity, είτε ως συμβολοσειρά είτε ως πίνακας συμβολοσειρών – ρόλων.

Ως κριτήριο authorization τώρα θα χρησιμοποιήσουμε τη μέθοδο isInRole(), η οποία αποκαλύπτει αν ο χρήστης λειτουργεί με τον δεδομένο ρόλο:

if ($user->isInRole('admin')) { // είναι ο χρήστης στον ρόλο του admin;
	deleteItem(); // τότε έχει δικαίωμα για την ενέργεια
}

Όπως ήδη γνωρίζετε, μετά την αποσύνδεση του χρήστη, η identity του δεν χρειάζεται να διαγραφεί. Δηλαδή, η μέθοδος getIdentity() εξακολουθεί να επιστρέφει το αντικείμενο SimpleIdentity, συμπεριλαμβανομένων όλων των εκχωρημένων ρόλων. Το Nette Framework ακολουθεί την αρχή «less code, more security», όπου λιγότερη γραφή οδηγεί σε πιο ασφαλή κώδικα, γι' αυτό κατά τον έλεγχο των ρόλων δεν χρειάζεται να επαληθεύσετε επιπλέον αν ο χρήστης είναι συνδεδεμένος. Η μέθοδος isInRole() λειτουργεί με ενεργούς ρόλους, δηλαδή, αν ο χρήστης είναι συνδεδεμένος, βασίζεται στους ρόλους που αναφέρονται στην identity, αν δεν είναι συνδεδεμένος, έχει αυτόματα τον ειδικό ρόλο guest.

Authorizator

Εκτός από τους ρόλους, θα εισαγάγουμε επίσης τις έννοιες του πόρου και της λειτουργίας:

  • role είναι μια ιδιότητα του χρήστη – π.χ. συντονιστής, συντάκτης, επισκέπτης, εγγεγραμμένος χρήστης, διαχειριστής…
  • resource (resource) είναι κάποιο λογικό στοιχείο του ιστότοπου – άρθρο, σελίδα, χρήστης, στοιχείο μενού, δημοσκόπηση, presenter, …
  • operation (operation) είναι κάποια συγκεκριμένη δραστηριότητα που ο χρήστης μπορεί ή δεν μπορεί να κάνει με τον πόρο – για παράδειγμα διαγραφή, επεξεργασία, δημιουργία, ψήφος, …

Ο authorizator είναι ένα αντικείμενο που αποφασίζει αν ο δεδομένος role έχει άδεια να εκτελέσει μια συγκεκριμένη operation σε έναν συγκεκριμένο resource. Πρόκειται για ένα αντικείμενο που υλοποιεί το interface Nette\Security\Authorizator με μία μόνο μέθοδο 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;
	}
}

Θα προσθέσουμε τον authorizator στη διαμόρφωση ως service του DI container:

services:
	- MyAuthorizator

Και ακολουθεί ένα παράδειγμα χρήσης. Προσοχή, αυτή τη φορά καλούμε τη μέθοδο Nette\Security\User::isAllowed(), όχι τον authorizator, οπότε δεν υπάρχει η πρώτη παράμετρος $role. Αυτή η μέθοδος καλεί την MyAuthorizator::isAllowed() διαδοχικά για όλους τους ρόλους του χρήστη και επιστρέφει true, αν τουλάχιστον ένας από αυτούς έχει άδεια.

if ($user->isAllowed('file')) { // μπορεί ο χρήστης να κάνει οτιδήποτε με τον πόρο 'file';
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // μπορεί πάνω στον πόρο 'file' να εκτελέσει 'delete';
	deleteFile();
}

Και οι δύο παράμετροι είναι προαιρετικές, η προεπιλεγμένη τιμή null σημαίνει οτιδήποτε.

Permission ACL

Το Nette έρχεται με μια ενσωματωμένη υλοποίηση του authorizator, την κλάση Nette\Security\Permission που παρέχει στον προγραμματιστή ένα ελαφρύ και ευέλικτο επίπεδο ACL (Access Control List) για τον έλεγχο των αδειών και των προσβάσεων. Η εργασία με αυτήν συνίσταται στον ορισμό ρόλων, πόρων και μεμονωμένων αδειών. Οι ρόλοι και οι πόροι επιτρέπουν τη δημιουργία ιεραρχιών. Για να το εξηγήσουμε, θα δείξουμε ένα παράδειγμα web εφαρμογής:

  • guest: μη συνδεδεμένος επισκέπτης, ο οποίος μπορεί να διαβάζει και να περιηγείται στο δημόσιο τμήμα του ιστότοπου, δηλ. να διαβάζει άρθρα, σχόλια και να ψηφίζει σε δημοσκοπήσεις
  • registered: συνδεδεμένος εγγεγραμμένος χρήστης, ο οποίος επιπλέον μπορεί να σχολιάζει
  • admin: μπορεί να διαχειρίζεται άρθρα, σχόλια και δημοσκοπήσεις

Ορίσαμε λοιπόν ορισμένους ρόλους (guest, registered και admin) και αναφέραμε πόρους (article, comment, poll), στους οποίους οι χρήστες με κάποιο ρόλο μπορούν να έχουν πρόσβαση ή να εκτελούν ορισμένες λειτουργίες (view, vote, add, edit).

Θα δημιουργήσουμε μια παρουσία της κλάσης Permission και θα ορίσουμε τους ρόλους. Μπορούμε να χρησιμοποιήσουμε την λεγόμενη κληρονομικότητα ρόλων, η οποία εξασφαλίζει ότι π.χ. ένας χρήστης με ρόλο διαχειριστή (admin) μπορεί να κάνει και ό,τι ένας απλός επισκέπτης του ιστότοπου (και φυσικά περισσότερα).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // ο 'registered' κληρονομεί από τον 'guest'
$acl->addRole('admin', 'registered'); // και από αυτόν κληρονομεί ο 'admin'

Τώρα θα ορίσουμε και τη λίστα των πόρων, στους οποίους οι χρήστες μπορούν να έχουν πρόσβαση.

$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');

Και οι πόροι μπορούν να χρησιμοποιούν κληρονομικότητα, θα ήταν δυνατό για παράδειγμα να ορίσουμε $acl->addResource('perex', 'article').

Και τώρα το πιο σημαντικό. Θα ορίσουμε μεταξύ τους κανόνες που καθορίζουν ποιος μπορεί να κάνει τι με τι:

// αρχικά κανείς δεν μπορεί να κάνει τίποτα

// ας μπορεί ο guest να βλέπει άρθρα, σχόλια και δημοσκοπήσεις
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// και στις δημοσκοπήσεις επιπλέον να ψηφίζει
$acl->allow('guest', 'poll', 'vote');

// ο εγγεγραμμένος κληρονομεί δικαιώματα από τον guest, θα του δώσουμε επιπλέον το δικαίωμα να σχολιάζει
$acl->allow('registered', 'comment', 'add');

// ο διαχειριστής μπορεί να βλέπει και να επεξεργάζεται οτιδήποτε
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

Τι γίνεται αν θέλουμε να εμποδίσουμε κάποιον από την πρόσβαση σε έναν συγκεκριμένο πόρο;

// ο διαχειριστής δεν μπορεί να επεξεργάζεται δημοσκοπήσεις, αυτό θα ήταν αντιδημοκρατικό
$acl->deny('admin', 'poll', 'edit');

Τώρα, που έχουμε δημιουργήσει τη λίστα των κανόνων, μπορούμε απλά να θέσουμε ερωτήματα authorization:

// μπορεί ο guest να βλέπει άρθρα;
$acl->isAllowed('guest', 'article', 'view'); // true

// μπορεί ο guest να επεξεργάζεται άρθρα;
$acl->isAllowed('guest', 'article', 'edit'); // false

// μπορεί ο guest να ψηφίζει σε δημοσκοπήσεις;
$acl->isAllowed('guest', 'poll', 'vote'); // true

// μπορεί ο guest να σχολιάζει;
$acl->isAllowed('guest', 'comment', 'add'); // false

Το ίδιο ισχύει και για τον εγγεγραμμένο χρήστη, ο οποίος όμως μπορεί και να σχολιάζει:

$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false

Ο διαχειριστής μπορεί να επεξεργάζεται τα πάντα, εκτός από τις δημοσκοπήσεις:

$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true

Οι άδειες μπορούν επίσης να αξιολογούνται δυναμικά και μπορούμε να αφήσουμε την απόφαση σε ένα δικό μας callback, στο οποίο θα περαστούν όλες οι παράμετροι:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	return /* ... */;
};

$acl->allow('registered', 'comment', null, $assertion);

Αλλά πώς να αντιμετωπίσουμε, για παράδειγμα, μια κατάσταση όπου δεν αρκούν μόνο τα ονόματα των ρόλων και των πόρων, αλλά θα θέλαμε να ορίσουμε ότι, για παράδειγμα, ο ρόλος registered μπορεί να επεξεργαστεί τον πόρο article μόνο αν είναι ο συγγραφέας του; Αντί για συμβολοσειρές θα χρησιμοποιήσουμε αντικείμενα, ο ρόλος θα είναι αντικείμενο Nette\Security\Role και ο πόρος Nette\Security\Resource. Οι μέθοδοί τους getRoleId() αντίστοιχα getResourceId() θα επιστρέφουν τις αρχικές συμβολοσειρές:

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';
	}
}

Και τώρα θα δημιουργήσουμε τον κανόνα:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	$role = $acl->getQueriedRole(); // αντικείμενο Registered
	$resource = $acl->getQueriedResource(); // αντικείμενο Article
	return $role->id === $resource->authorId;
};

$acl->allow('registered', 'article', 'edit', $assertion);

Και το ερώτημα στο ACL θα γίνει περνώντας τα αντικείμενα:

$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');

Ένας ρόλος μπορεί να κληρονομεί από έναν άλλο ρόλο ή από πολλούς ρόλους. Τι συμβαίνει όμως αν ένας πρόγονος έχει απαγορευμένη την ενέργεια και ο άλλος επιτρεπόμενη; Ποια θα είναι τα δικαιώματα του απογόνου; Καθορίζεται από το βάρος του ρόλου – ο τελευταίος αναφερόμενος ρόλος στη λίστα των προγόνων έχει το μεγαλύτερο βάρος, ο πρώτος αναφερόμενος ρόλος το μικρότερο. Είναι πιο κατανοητό από το παράδειγμα:

$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');

$acl->addResource('backend');

$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');

// περίπτωση Α: ο ρόλος admin έχει μικρότερο βάρος από τον ρόλο guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// περίπτωση Β: ο ρόλος admin έχει μεγαλύτερο βάρος από τον guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Οι ρόλοι και οι πόροι μπορούν επίσης να αφαιρεθούν (removeRole(), removeResource()), μπορούν να αναιρεθούν και οι κανόνες (removeAllow(), removeDeny()). Ο πίνακας όλων των άμεσων γονικών ρόλων επιστρέφεται από την getRoleParents(), το αν δύο οντότητες κληρονομούν η μία από την άλλη επιστρέφεται από τις roleInheritsFrom() και resourceInheritsFrom().

Προσθήκη ως services

Το ACL που δημιουργήσαμε πρέπει να το περάσουμε στη διαμόρφωση ως service, ώστε να αρχίσει να το χρησιμοποιεί το αντικείμενο $user, δηλαδή ώστε να είναι δυνατό να χρησιμοποιούμε στον κώδικα π.χ. $user->isAllowed('article', 'view'). Γι' αυτόν τον σκοπό θα γράψουμε ένα factory γι' αυτό:

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;
	}
}

Και θα το προσθέσουμε στη διαμόρφωση:

services:
	- App\Model\AuthorizatorFactory::create

Στους presenters τότε μπορείτε να επαληθεύσετε τα δικαιώματα για παράδειγμα στη μέθοδο startup():

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
έκδοση: 4.0