Επαλήθευση αδειών (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);
}
}