Zend CMS

Nachdem ich die ersten Schritte mit dem Zend Framework gemacht habe, möchte ich natürlich tiefere Kenntnisse darin erwerben. Da ich für kleine Projekte immer wieder mal eine solide Basis brauche wie z. B. eine kleine Navigation und etwas Text, bietet sich die Entwicklung eines kleinen CMS Frameworks an. Also, was brauche ich… zuerst einmal eine Url, über die ich eine Login aufrufen kann. Ich dachte da an /admin. Bin ich da nicht einfallsreich?. Also, wie geht das?

Zuerst generieren wir mit dem zf-Tool ein Admin-Modul:

zf create module admin

Anschließend einen einfachen Controller:

zf create controller Index index-action-included=1 admin

In der Console sieht das dann so aus:

Zend Admin Modul erzeugen

In Zend ein Admin Modul erzeugen

In Zend ein Controller im Admin Modul erzeugen

In Zend ein Controller im Admin Modul erzeugen

Nun kann die Url /admin bereits aufgerufen werden. Der erzeugte Controller stellt den Startbildschirm des Admin-Bereichs dar. Wir brauchen also noch einen Login-Controller:

zf create controller Login index-action-included=1 admin

Dieser Controller wird dann über die Url /admin/login angesprochen. Jetzt brauchen wir noch eine generelle Überprüfung, ob irgendeine URL überhaupt aufgerufen werden darf. Das geht soweit, dass wir jede x-beliebige URL auf Berechtigungen prüfen können, also z. B. ob eine bestimmte Seite im Frontend oder ob eine Seite des Adminbereichs aufgerufen werden darf. Das kann man einfach über ein Frontend-Controller-Plugin lösen. Solch ein Plugin wird z. B. immer vor der eigentlichen Controller-Routine aufgerufen (preDispatch).

Um den Code später für andere Projekte wiederverwenden zu  können, habe ich einen Unterordner im library-Verzeichnis angelegt. Damit der Autoloader dort nach PHP-Klassen sucht, muß eine Anweisung in die application.ini:

autoloadernamespaces.jacms = "Jacms_"

Der Ordner heißt also /library/jacms. Im nächsten Schritt definieren wir die Plugin-Klasse:

resources.frontController.plugins.acls = "Jacms_Admin_Plugin_Acls"

Diese Zeile besagt, dass es eine Klasse Jacms_Admin_Plugin_Acls geben muß, die im Filesystem unter /library/jacms/admin/plugin/Acls.php gespeichert ist.

Nun können wir gleich mit der Definition der Klasse beginnen.

class Jacms_Admin_Plugin_Acls extends Zend_Controller_Plugin_Abstract
{
  // overrides/implements
  public function preDispatch(Zend_Controller_Request_Abstract $request)
  {
    // Login und Fehler immer erlauben
    if($request->getControllerName()=='login' ||
       $request->getControllerName()=='error')
    {
      return;
    }

    // Schlüssel für angefragte Resource erzeugen
    // Die Methode getControllerKey macht nix anderes als die drei 
    // Funktionsparameter zu einem String zu verbinden
    // z. B. admin/index/index
    $resource=Jacms_Security_Acl::getControllerKey(
                 $this->getRequest()->getModuleName(), 
                 $this->getRequest()->getControllerName(), 
                 $this->getRequest()->getActionName());

    // Falls ein ACL Mapper vorhanden ist, dann werden dort die 
    // Berechtigungen für den Aufruf geprüft, falls nicht, oder 
    // falls keine Berechtigung vorhanden, 
    // wird auf den Login umgeleitet.
    if(!Jacms_Security_Acl::getAclMapper() ||
       (Jacms_Security_Acl::getAclMapper() && 
          !Jacms_Security_Acl::getAclMapper()->hasPrivilege(
               $resource, 
               Jacms_Security_Acl::PRIVILEGE_READ)))
    {
      $redirect=new Zend_Controller_Action_Helper_Redirector();
      $redirect->direct('index', 
                        'login', 
                        'admin', 
                        array('back'=>$request->getModuleName()));
    }
  }
}

Die Klasse muß von der Zend-Klasse Zend_Controller_Plugin_Abstract ableiten und die Methode preDispatch implementieren. Die Controller login und error sind immer erlaubt. Mein Grundgedanke ist, dass alle Module, Controller und Actions über Zend_Acls abgefragt werden können. Damit das Plugin an die Zend_Acls kommt, gibt es eine Klasse Jacms_Security_Acl, über die in der eigentlichen Application ein „ACL Mapper“ registriert werden kann. Das kann z. B. so aussehen:

Jacms_Security_Acl::registerAclMapper(Admin_Model_Mapper_Acls::get());

Diese Registrierung führe ich noch vor dem gesamten Dispatch-Prozess durch, also am besten die index.php leicht anpassen:

$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);
$application->bootstrap();
// Initialisierung Sicherheit
Jacms_Security_Acl::registerAclMapper(Admin_Model_Mapper_Acls::get());
$application->run();

Von dem Mapper Admin_Model_Mapper_Acls hab ich noch gar nicht geschrieben. Kurz gesagt: Es ist ein Mapper wie man ihn aus dem Zend Tutorial kennt, also eine Klasse, die aus der Datenbank Datensätze ließt und diese auf eine Model-Klasse abbildet. Er liest beispielweise für eine Ressource (bei mir ist das momentan ein Schlüssel aus Modul-Controller-Action) die Berechtigungen aus. Ich brauche das, da ich ja die Berechtigungen im Admin-Bereich pflegen will und diese nicht statisch im Code hinterlegt sein sollen. Aber zurück zu unserem Plugin. Die Methode Jacms_Security_Acl::getAclMapper() liefert ein Objekt Jacms_Security_AclMapper zurück. Das ist nichts anderes als ein einfaches Interface:

interface Jacms_Security_AclMapper
{
  /**
   * @return Zend_Acl
   */
  public function getAcls();
  /**
   * @param string $resource
   * @param string $privilege
   */
  public function hasPrivilege($resource, $privilege);
}

Dieses Interface wird von dem bereits erwähnten Mapper implementiert:

class Admin_Model_Mapper_Acls implements Jacms_Security_AclMapper
{
  // interface
  public function getAcls() { ... }
  public function hasPrivilege($resource, $privilege=null) { ... }

  // accessors/mutators
  public function find(...
  public function findAll(...
}

Da es in meiner Anwendung bei dem Mapper um ein Singelton handelt, müssen die ACLS nur einmal aus der Datenbank ausgelesen werden. Je nach Komplexität könnte man hier noch mit Memcache oder APC arbeiten. Für meine Zwecke reicht das jedoch vollkommen aus.

Richtig super finde ich die Klasse Zend_Acls. Hier ein kleines Beispiel:

// Erzeugt die Klasse Zend_Acls
$acl=new Zend_Acl();
// fügt eine Resource hinzu
$acl->addResource('admin/index/list');
// Rolle Viewer definieren
$acl->addRole('Viewer');
// Rolle Admin definieren, die von Viewer erbt
// Es gibt also hier eine Hirarchie. Super!
$acl->addrole('Admin', 'Viewer');
// Etwas verbieten
$acl->deny('Viewer', 'admin/index/list', 'Bearbeiten');
$acl->deny('Viewer', 'admin/index/list', 'Löschen');
// Etwas erlauben
$acl->allow('Viewer', 'admin/index/list', 'Ansehen');
// Admin darf löschen (d. h. auch bearbeiten)
// diese Logik muß man selbst in seiner Anwendung
// implementieren
$acl->allow('Admin', 'admin/index/list', 'Löschen');

Im Ganzen ist es vielleicht erstmal nicht leicht zu verstehen, aber ich bin mit dieser Lösung vollkommen zufrieden. Das ganze läßt sich natürlich erweitern, beispielsweise um benutzerspezifische Ressourcen.

Kommentar verfassen