Έλεγχος πρόσβασης (εξουσιοδότηση)

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

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

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

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

if ($user->isLoggedIn()) { // είναι συνδεδεμένος ο χρήστης;
	deleteItem(); // Αν ναι, μπορεί να διαγράψει ένα στοιχείο
}

Ρόλοι

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

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

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

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

Εξουσιοδότης

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

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

Ένας εξουσιοδοτητής είναι ένα αντικείμενο που αποφασίζει αν ένας συγκεκριμένος ρόλος έχει δικαίωμα να εκτελέσει μια συγκεκριμένη λειτουργία με συγκεκριμένο πόρο. Είναι ένα αντικείμενο που υλοποιεί τη διεπαφή 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 στη διαμόρφωση ως υπηρεσία του DI container:

services:
	- MyAuthorizator

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

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

if ($user->isAllowed('file', 'delete')) { // επιτρέπεται στον χρήστη να διαγράψει τον πόρο 'αρχείο';
	deleteFile();
}

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

Άδεια ACL

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

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

Έτσι, έχουμε ορίσει ορισμένους ρόλους (guest, registered και admin) και έχουμε αναφέρει πόρους (article, comments, 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' κληρονομεί από το 'registered'

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

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

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

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

// όλα αρνούνται τώρα

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

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

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

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

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

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

// μπορεί ο επισκέπτης να δει τα άρθρα;
$acl->isAllowed('guest', 'article', 'view'); // true

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

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

// μπορεί ο επισκέπτης να προσθέσει σχόλια;
$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(); // αντικείμενο Εγγεγραμμένος
	$resource = $acl->getQueriedResource(); // αντικείμενο Άρθρο
	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');

// παράδειγμα A: ο ρόλος 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().

Προσθήκη ως υπηρεσία

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

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

startup(), για παράδειγμα:

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