Zend Framework FAQ: Wie setze ich eine ACL für eine Modul basierte Applikation um?

Bei der ersten Abstimmung für die Zend Framework Fragestunde, wurde diese Frage mit 37% der Stimmen auf Platz 1 gewählt: Wie setze ich eine ACL für eine Modul basierte Applikation um?

Hierfür müssen wir als erstes die Grundlage schaffen und festlegen, was eigentlich mit einer Modul basierten Applikation gemeint ist. Das Zend Framework bietet schon seit einen frühen Anfängen die Möglichkeit, eine Applikation in Module aufzuteilen. In einem Modul können somit Action-Controller, View-Skripte, Models, Formulare und mehr gekapselt werden. Die Klassen dieser Module bekommen einen eigenen vorangestellten Namensraum, damit es nicht zu Namenskonflikten kommen kann. Eine Zend Framework Applikation könnte somit aus den Modulen Gästebuch, Forum und CMS (für Ausgabe von Textbeiträgen) bestehen. Zusätzlich zu diesen Modulen gibt es in der Regel immer ein Default-Modul, dass sich unter anderem um die Auslieferung der Homepage kümmert. Zusammenfassend werden wir also mit den folgenden Module arbeiten:

  • default
  • guestbook
  • forum
  • cms

Um mit Zend_Acl eine ACL (Access Control List) umsetzen zu können, benötigen wir noch die Benutzerrollen. Wir verwenden an dieser Stelle die drei Benutzerrollen admin, editor und guest und gehen davon aus, dass in allen Modulen die selben Benutzerrollen verwendet werden sollen. Den Aspekt der Wiederverwendbarkeit der Module lasse ich an dieser Stelle bewusst unberücksichtigt. Denn dies würde das Ganze nur noch komplizierter machen.

Als nächstes müssen wir noch überlegen, wie wir die Ressourcen, Privilegien und Rechte in unserer ACL definieren. Hierbei gibt es verschiedene Ansätze. Wir könnten als Ressourcen unsere Models betrachten und deren Methoden als Privilegien einsetzen. Ein anderer Ansatz basiert auf dem Dreigespann Modul, Controller und Action. Diesen Ansatz möchte ich hier vorstellen. Als Privilegien verwenden wir die Namen der Aktionsmethoden und als Ressourcen die Namen der Action-Controller Klassen. Und wo bleibt das Modul? Ganz einfach, für jedes Modul erstellen wir eine eigene ACL. Dies hat auch den Vorteil, dass die ACL insgesamt nicht zu groß wird. Zudem können wir durch geschicktes Cachen der Zend_Acl Objekte zusätzlich die Performance steigern.

Hier folgt als Beispiel die ACL für unser CMS Modul. Die ACLs für die anderen Module sehen ähnlich aus und tragen die Klassennamen Default_Acl, Guestbook_Acl und Forum_Acl.

PHP:
  1. class Cms_Acl extends Zend_Acl
  2. {
  3.     public function __construct()
  4.     {
  5.         // Rollen anlegen
  6.         $this->addRole(new Zend_Acl_Role('guest'));
  7.         $this->addRole(new Zend_Acl_Role('editor'), 'guest');
  8.         $this->addRole(new Zend_Acl_Role('admin'), 'editor');
  9.        
  10.         // Ressourcen anlegen, jede Ressource entspricht einem Action-Controller
  11.         $this->addResource(new Zend_Acl_Resource('index'));
  12.         $this->addResource(new Zend_Acl_Resource('article'));
  13.         $this->addResource(new Zend_Acl_Resource('tag'));
  14.         $this->addResource(new Zend_Acl_Resource('comment'));
  15.        
  16.         // Privilegien und Rechte für Gäste anlegen
  17.         $this->allow('guest', 'index');
  18.         $this->allow('guest', 'article', array('index', 'show'));
  19.         $this->allow('guest', 'tag', array('index', 'show'));
  20.         $this->allow('guest', 'comment', array('index', 'show', 'create'));
  21.  
  22.         // Privilegien und Rechte für Redakteure anlegen
  23.         $this->allow('editor', 'article', array('create', 'update', 'delete'));
  24.         $this->allow('editor', 'tag', array('create', 'update'));
  25.         $this->allow('editor', 'comment', array('update', 'delete'));
  26.        
  27.         // Privilegien und Rechte für Admins anlegen
  28.         $this->allow('admin', 'article', array('approve', 'block'));
  29.         $this->allow('admin', 'tag', array('delete'));
  30.         $this->allow('admin', 'comment', array('mark-as-spam', 'hide'));
  31.     }
  32. }

Das ist keine große Sache und sollte jedem klar sein, der sich mit der Zend_Acl Komponente schon ein wenig befasst hat. Jetzt bleibt die Frage, wann wird die entsprechende ACL denn geladen. Dies kann erst passieren, nachdem das Routing statt gefunden hat und klar ist, welches Modul gerade aufgerufen wird. Dafür verwenden wir ein Plugin.

Das Plugin kümmert sich nur um die Benutzerrechte und nicht um die Authentifizierung. Es geht davon aus, dass diese bereits erfolgt ist. Das Plugin in an sich ist relativ selbsterklärend, hervorheben möchte ich nur, dass auf Basis des aktuellen Moduls der Klassenname der entsprechenden ACL erstellt wird. Der Rest sollte Routine sein. Wenn eine Benutzer nicht über entsprechende Rechte verfügt, wird er als Gast gebeten sich einzuloggen und als angemeldeter User bekommt einen entsprechenden Hinweis.

PHP:
  1. class Default_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
  2. {
  3.     public function routeShutdown(Zend_Controller_Request_Abstract $request)
  4.     {
  5.         // hole Module, Controller und Action
  6.         $module     = $request->getModuleName();
  7.         $controller = $request->getControllerName();
  8.         $action     = $request->getActionName();
  9.        
  10.         // hole Auth
  11.         $auth = Zend_Auth::getInstance();
  12.        
  13.         // hole Rolle
  14.         $role = $auth->hasIdentity()
  15.               ? Zend_Auth::getInstance()->getIdentity()->role
  16.               : 'guest';
  17.        
  18.         // erstelle Klassenname für ACL
  19.         $aclClass = ucfirst($module) . '_Acl';
  20.        
  21.         // Erstelle ACL Objekt
  22.         $acl = new $aclClass(); /* @var $acl Zend_Acl */
  23.        
  24.         // Prüfe Rechte
  25.         if (!$acl->isAllowed($role, $controller, $action))
  26.         {
  27.             // lege neue Action fest
  28.             $newAction = ('guest' == $role) ? 'login' : 'forbidden';
  29.            
  30.             // Ändere Request Objekt für neue Action
  31.             $request->setModuleName('default')
  32.                     ->setControllerName('user')
  33.                     ->setActionName($newAction);
  34.         }
  35.     }
  36. }

Das war es schon im Wesentlichen. Das Ganze ließe sich nun noch verfeinern, indem man sich z.B. eine Factory Methode schreibt, welche das Ermitteln des Klassennamens übernimmt. Dadurch kann die ACL auch außerhalb des Plugins anhand eines Moduls ermittelt werden. Diese Methode könnte My_Acl::factory() heißen, als Parameter $module entgegen nehmen und ein von Zend_Acl abgeleitetes Objekt zurückgeben. Diese Factory-Methode könnte sich dabei auch gleich um das Caching kümmern, so dass die ACLs nicht bei jedem Aufruf neu erstellt werden müssen.

Ich hoffe, die Ausgangsfrage ist somit im Wesentlichen beantwortet. Falls nicht, bitte in den Kommentaren melden. Natürlich auch bei Fragen zu diesem Beitrag.

Tweet this via redir.ec

17 Antworten für “Zend Framework FAQ: Wie setze ich eine ACL für eine Modul basierte Applikation um?”

  1. Ulf sagt:

    Schöner Beitrag, sehr gut erklärt! :)

    Ich möchte nur noch anmerken, dass eine Zugriffsbeschränkung durch ACL auf Modul, Controller und Action nicht immer sinnvoll ist. Typisches Beispiel ist da wohl das Redaktions-CMS, wo nur die jeweilige Autor seine Beiträge editieren (und löschen?) darf. Sicherlich könnte man das auch in dem PlugIn einbauen, bei größerer Komplexität werden das aber schnelle viele Datenbankzugriffe nur für die Überprüfung von Rechten. Sinnvoller ist es dann sinnerlich die ACL in das Model zu integrieren.

  2. chrisweb sagt:

    Ja sehr guter Artikel ... hmmm ist es ist nicht am einfachsten, einfach wie "früher" den Author eines Artikels in einer Tabelle zu hinterlegen (z.b. tabelle artikel_authoren: artikel_id / author_id) und diese Daten dann auszulesen und dabei ganz auf ein ACL zu verzichten.

    Naja denke aber ein modernes CMS wird das Editieren bzw Löschen eines Artikels nicht auf den Author beschränken, denke eher dass man dafür ein Workflow hat mit mehreren Rollen und eben einem ACL System. Die Redakteure dürfen alle Artikel bearbeiten, die Korrekteure überlesen ihn und schieben ihn weiter zum Chefredakteur und dieser veröffentlicht ihn (gibt ihn frei).

  3. Tobias sagt:

    Habe den Artikel gerade nur überflogen, da ich gerade thematisch woanders dran bin.

    @chrisweb: Dann denk mal nicht an ein CMS System sondern an ein Shop System, wo ein Kunde eine Bestellung aufgibt und diese auch noch verändern darf (für eine gewisse Zeitspanne) oder Login Bereich in dem die Kunden Ihre Statistiken zu Ihrer (und wirklich nur Ihrer) Seite abrufen können sollen.

    Aber spontan fällt mir auch keine Lösung ein für solch einen "exklsiven" Zugriff ...

  4. Ralf sagt:

    Soweit ich es in Erinnerung habe, sind für solche Zwecke die Assertions gedacht:

    http://framework.zend.com/manual/en/zend.acl.advanced.html#zend.acl.advanced.assertions

    Aber irgendwie ist das Beispiel im Manual nicht so schön. Statt eine IP zu verwenden, wäre der gefragtere Weg wohl, wie man nur User Foo erlauben kann, den Beitrag 123, aber nicht den Beitrag 456 zu ändern. Wäre ja glatt eine neue Frage für die nächste Runde... ;-)

  5. Ulf sagt:

    Genau diese Lösung kann bei einem komplexen Rollensystem, z.B. das von chrisweb angedachte Redaktionssystem, schnell in vielen Prüfungen / Sonderbedingungen etc. ausarten. Das kann bei größeren Übersichtsseiten (Auflisten von 50 Artikel pro Seite o.Ä.) schnell die Datenbank auslasten nur um zu prüfen ob man etwas darf. Eine saubere, performante und wartbare Lösung fällt mir auch nicht wirklich ein, ich wollte ja nur aussagen, dass nicht jedes Beispiel auf eine solch vereinfachte Rollenverteilung wie im Artikel zurückgeführt werden kann.

    Die richtige Lösung ist natürlich abhängig vom konkreten Anwendungsbeispiel! ;)

  6. Tobias sagt:

    Bin das gerade am testen an einem kleinen Beispiel, aber irgendwie bekomme ich das nicht zum laufen. Wo werden den die *_Acl.php Klassen gespeichert? (Bsp.: Default_Acl.php -> direkt in das Modulverzeichnis "default"? Was ist wenn ich kein default modul habe, sondern die default Controller, Models und Views nur im Ordner "application" liegen? - Aber auch mit dem Modul "User" habe ich die ACL Klasse in meinem Plugin nicht geladen bekommen ... )

  7. Ulf sagt:

    @Tobias Ich nehme an deine ACL-Klasse wird nicht richtig vom Autoloader geladen. Wenn du sie direkt im ordner application packst dann darf die Klasse auch nicht geprefix werden (bei Standard-Route Einstellungen des Zend-Autoloaders). Wenn du sie im user modul reinpackst und nicht richtig geladen wird, dann solltest du dir den Zend-Autoloader mal genauer anschauen, insbesondere die Handhabung von Zend_Application_Module_Autoloader.

    @Ralf Ich sehe das erst jetzt. Wieso überschreibst du die routeShutdown Methode und nicht die preDispatch Methode? Gibt es da sinnvolle Beweggründe oder ist das persönliche Präferenz?

  8. Ralf sagt:

    @Ulf,

    damit stelle ich sicher, dass die ACL so früh wie möglich erstellt und bereit gestellt wird. Andere Plugins könnten ja bereits in der preDispatch() Methode etwas machen, was sie nicht sollen, falls ein Nutzer zu wenige Rechte hat. Natürlich kann ich die Reihenfolge ja auch beim Registrieren der Plugins bestimmen, aber sicher ist sicher.

  9. Florian sagt:

    Hy

    Bin gerade dabei das beispiel einzubauen. Entweder ich seh den Wald vor Lauter Bäumen nicht oder es sind die Zahnschmerzen.

    Wo rufe ich den die _Acl Klassen auf?

  10. Tobias sagt:

    @Florian:
    Es handel sich bei der Klasse "Default_Controller_Plugin_Acl" um ein Plugin, welches selbstverständlich, im Front Controller noch regestriert werden muss. Ich würde die Klasse in einer nichtmodularen Anwendung "Plugin_Acl" nennen und in dem Ordner application/plugins ablegen ... in der modularen Applikation habe ich damit auch noch meine Problemchen mit dem Autoloader.

    @Ralf:
    Wo speicherst du deine *_Plugin_Acl Klasse (vielleicht in "application/controllers/plugins/Acl.php")? Und wo speicherst du deine Modul basierenden *_Acl Klassen?

  11. Ralf sagt:

    @Tobias

    die Beispiel ACL Klasse Cms_Acl sollte in application/modules/cms/Acl.php gespeichert sein. Man kann sie aber gerne anders ablegen, so wie man es besser findet. So wäre ein Unterverzeichnis application/modules/cms/acl/ ebenfalls denkbar.

  12. Tobias sagt:

    Also, ich hab das ganze nun nochmal aufgesetzt mit einem frischen "zf create project" und den Modifikationen für ein Module. Soweit funktioniert alles. Nun habe ich ein Modul "test" und in dem order "application/modules/test/" liegt meine Klasse "Test_Acl.php".

    Ein extra "default" Modul habe ich nicht angelegt - nutze also folgende verzeichnisse "application/controllers", "application/plugins", "application/views" usw.

    Mein Plugin habe ich in der application/Bootsrap regestriert:
    protected function _initAcl ()
    {
    $fc = Zend_Controller_Front::getInstance();
    $fc->registerPlugin( new Default_Plugin_Acl() );
    }

    Das Plugin läuft durch nur die Klasse "Default_Acl.php" (die neben der Bootstrap im application Ordner liegt) wird nicht gefunden. Wo gehört diese Datei hin??

  13. Tobias sagt:

    Anmerkung
    Zend_Debug::dump(Zend_Loader_Autoloader::getInstance()); gibt mir folgendes aus

    object(Zend_Application_Module_Autoloader)[10]
    protected '_basePath' => string '/home/path/to/project/application' (length=67)
    protected '_components' =>
    array
    ...
    protected '_defaultResourceType' => string 'model' (length=5)
    protected '_namespace' => string 'Default' (length=7)

  14. strolch00 sagt:

    Warum schreibst Du denn

    CODE:
    1. $role = $auth->hasIdentity()
    2.               ? Zend_Auth::getInstance()->getIdentity()->role
    3.               : 'guest';

    und nicht direkt

    CODE:
    1. $role = $auth->hasIdentity()
    2.               ? $auth->getIdentity()->role
    3.               : 'guest';

    Hat das einen speziellen Grund?

  15. Ralf sagt:

    @strolch

    Nein, hat keinen Grund. Du hast natürlich Recht, deine Lösung ist besser, weil kürzer.

  16. strolch00 sagt:

    Ah ok und nun nochmal was zu Ulf´s Beitrag.
    Bei den Acl´s mache ich das genauso mit "routeShutdown", das hat außerdem noch den Vorteil das wenn ein User nun zu wenig Berechtigungen hat, man den Dispatch nochmal durchlaufen muss mit setDispatched(false) oder so ähnlich. Funktioniert bei mir wunderbar, und ich spar Serverlast, wenn auch in einem geringen Maße.

    Btw: @Ralf
    Gerlungener Beitrag, genau das was ich grad gebraucht hatte, danke nochmal das ich mir bei deinem Rad was abschauen durfte, um es net ganz neu zu erfinden ;).

  17. riedi sagt:

    Eine schöne Lösung, jedoch nicht geeignet, wenn man eine Modulübergreifende Seitennavigation mit ACL betreiben möchte.

Hinterlasse eine Antwort


Better Tag Cloud