Änderungen in der ACL meines Frameworks

Die erste Version der Access Control List (ACL) des Frameworks bot folgende Funktionalitäten:

  • Rollen anlegen
  • Resourcen Anlegen
  • Rechte zuteilen:
    • Rollen konnten Rechte auf alle Resourcen haben (Admin)
    • Rollen konnten Rechte auf alle Methoden einer Resource haben
    • Rollen konnten nur Rechte auf bestimmte Methoden einer Resource haben
  • Rechte vererben
  • Prüfen, ob Rechte vorliegen

Wenn man sich vorstellt, dass es eine Resource „User“ mit folgenden Methoden gibt:

  • showProfile
  • register
  • sendNewPassword
  • showUserlist

Und man will, dass ein Gast alle Methoden ausführen darf, nur showProfile nicht, dann müsste man für die anderen 3 einzeln Regeln erstellen:

$acl->allow("gast", "user",   array("register", "sendNewPassword", "showUserlist"));

Das wäre jetzt noch nicht so viel Aufwand. Wenn es aber 20 Methoden gibt, von denen der Gast nur eine nicht aufrufen darf, wäre es doch praktischer, nur diese eine Methode zu verbieten! Und genau das war die Neuerung, die ich eingeführt habe.

$acl->disallow("guest", "user", array("showProfile"));

Es ist nicht erforderlich, die anderen Methoden einzeln zu erlauben. Mein Framework erkennt das automatisch.


Der Code der ACL ist ziemlich lang und auch kompliziert. Für Neugierige poste ich ihn hier aber trotzdem. (Ist gut kommentiert)

/**
 * @licence See: /licence.txt
 */ 

/**
 * @author Simon H.
 * @package MVC-Framework
 * @version 0.5
 * 
 * Die Klasse FW_Acl implementiert das FW_Acl_Interface.
 * FW_Acl kann Zugriffsberechtigungen verwalten. Dazu werden Rollen, Regeln und
 * Resourcen verwendet.
 * 
 *  - Rolle: Z.B. eine Benutzergruppe
 *  - Regel: Welche Rolle darf was mit den Resourcen machen?
 *  - Resource: In der Regel ein Controller
 * 
 * Es gibt die Möglichkeit der Vererbung bei den Rollen. Eine Rolle kann von 
 * einer anderen Rolle erben. Damit erhält sie alle Rechte, die dise Rolle hat.
 * Die Rolle, die geerbt hat, kann danach eigene Rechte erhalten.
 * 
 * Sinnvoll wäre z.B. so eine Vererbung:
 * - Gast (darf nur lesen)
 *   - Mitglied (erbt alles von Gast) (darf auch schreiben)
 *     - Moderator (erbt alles von Mitglied und somit auch alles von Gast) (darf auch löschen)
 * 
 * Es besteht auch die Möglichkeit, einer Gruppe Zugriff auf alles zu gewähren.
 * Das macht bei Administratoren Sinn.    
 * 
 * Außerdem kann man Rechte wieder entfernen. Das geht mit deny()
 * Verbote werden mitvererbt.                     
 */ 
class FW_Acl implements FW_Acl_Interface
{
  /**
   * @access protected
   * @var array roles Speichert alle Rollen
   */        
  protected $roles = array();

  /**
   * @access protected
   * @var array resources Speichert alle Ressourcen 
   */  
  protected $resources = array();

  /**
   * @access protected
   * @var array rules Speichert alle Regeln 
   */  
  protected $rules = array();

  /**
   * @access public
   * 
   * Der Konstruktor dient nur zur Initialisierugn der Rechte      
   */     
  public function __construct()
  {
    //initialisiere Rechte
    $this->rules = array
                   (
                     "allow" => array(
                                      "permission_for_everything" => array(),
                                      "resources" => array()
                                     ),
                     "disallow" => array(
                                         "resources" => array()
                                        )
                   );
  }

  /**
   * @access public
   * @param string $role
   * @param string $resource
   * @param string $do_what
   * @return bool
   * 
   * Wenn eine $role auf eine $resource Zugriff hat und $do_what ausführen darf,
   * gibt hasPermission true zurück. Sonst false                        
   */     
  public function hasPermission($role, $resource, $do_what)
  {
    $role = (string)strtolower($role);
    $resource = (string)strtolower($resource);
    $do_what = (string)strtolower($do_what);

    /*
      Hier wird nicht geprüft, ob die Rollennamen und die Resourcennamen
      gültig sind, weil man so nicht jede einzelne Seite in seiner ACL angeben muss.
    */

    //als erstes wird geprüft, ob die Rolle Rechte auf alles hat.
    if(isset($this->rules["allow"]["permission_for_everything"][$role]))
    {
      return true;
    }

    //wenn nicht, wird geprüft, ob rechte auf die geasmte Ressource bestehen
    if(isset($this->rules["allow"]["resources"][$resource]["roles"][$role]) &&
       count($this->rules["allow"]["resources"][$resource]["roles"][$role]) < 1)
    {
      return true;
    }

    //wenn er nicht alle rechte auf alles hat und auch nicht alle rechte auf die
    //resource, wird geschaut, ob er die aktion do_what ausführen darf
    if(isset($this->rules["allow"]["resources"][$resource]["roles"][$role]) &&
       in_array($do_what, $this->rules["allow"]["resources"][$resource]["roles"][$role]))
    {
      return true;
    }

    //Wenn Verbote für diesen User und diese Resource existieren, wird geprüft, 
    //ob die Aktion auch verboten wurde. Wenn nicht, wird true zurück gegeben.    
    if(isset($this->rules["disallow"]["resources"][$resource]["roles"][$role]) 
       && !in_array($do_what, $this->rules["disallow"]["resources"][$resource]["roles"][$role])
       && count($this->rules["disallow"]["resources"][$resource]["roles"][$role]) > 0)
    {
      return true;
    }

    //vllt. gibt es vererbung?
    if(isset($this->roles[$role]["parents"]))
    {
      foreach($this->roles[$role]["parents"] as $parent_role_object)
      {
        if($this->hasPermission($parent_role_object->getRoleName(), $resource, $do_what))
        {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * @access public
   * @return instanz der klasse
   * @param FW_Acl_Role $role
   * @param array(string|FW_Acl_Role) parent      
   * 
   * Fügt eine neue Rolle hinzu. Die Rolle kann von einer bereits existierenden
   * Rolle erben. In diesem Fall erhält die Elternrolle einen Eintrag im 
   * children-Array.               
   */     
  public function addRole(FW_Acl_Role $role, array $parents = null)
  {
    /**
     * Der Rollenname ist später der Index in allen beteiligten Arrays
     */         
    $role_name = $role->getRoleName();   
    $parentArray = array();
    /**
     * Wenn diese Rolle von einer anderen Rolle erbt
     */         
    if($parents !== null)
    { 
     //durchlaufe alle Eltern und mache sie unter $parent zugänglich     
     foreach($parents as $parent)
     {
      /**
       * Wurde eine Instanz oder nur ein Rollenname übergeben?
       */             
      if($parent instanceof FW_Acl_Role)
      {
         $parentObject = $parent;
         $parent_name = $parentObject->getRoleName();
         if(!$this->roleExists($parent_name))
         {
           throw new FW_Acl_Exception("Diese Rolle gibt es nicht: ".$parent_name);
         }
      } 
      else
      {
         $parent_name = (string)$parent;          
         if(!$this->roleExists($parent_name))
         {
           throw new FW_Acl_Exception("Diese Rolle gibt es nicht: ".$parent_name);
         }
         $parentObject = $this->getRoleByName($parent_name);
      }      

      $parentArray[$parent_name] = $parentObject; //weise dem Rollennamen seine Instanz zu

      /**
       * Die Elternklasse soll wissen, dass sie Kinder hat.      
       */      
      $this->roles[$parent_name]["children"][$role_name] = $role;
     }
    }

    $this->roles[$role_name] = array
                               (
                                 "children" => array(),    //hat zu diesem Zeitpunkt noch keine Kinder
                                 "instance" => $role,      //Die Instanz der Rolle
                                 "parents" => $parentArray       //Falls geerbt wird, steht hier ein Array
                               );   

    return $this;
  }

  /**
   *  @access protected
   *  @return FW_Acl_Role instanz
   *  @param string $name
   *  
   * Gibt die zum Rollennamen zugehörige instanz zurück.              
   */  
  protected function getRoleByName($name)
  {
    return $this->roles[$name]["instance"];
  }

  /**
   * @access public
   * @return instanz der Klasse 
   * @param FW_Acl_Resource $resource
   *       
   * Die Methode fügt eine Ressouce auf dem Stack dieser ACL-Klasse an     
   */     
  public function addResource(FW_Acl_Resource $resource)
  {
    $this->resources[$resource->getResourceName()] = $resource;
    return $this;
  }

  /**
   * @access public
   * @return instanz der Klasse 
   * @param string $resource
   *       
   * Die Methode löscht eine Ressource auf dem Stack dieser ACL-Klasse    
   */ 
  public function removeResource($resource)
  {
    $resource = (string) $resource;
    unset($this->resources[$resource]);
    return $this;
  }

  /**
   * @access public
   * @return instanz der Klasse
   * @param string $role
   *        
   * Mit dieser Methode lassen sich Rollen entfernen. Zusätzlich werden auch
   * andere Rollen "aufgeräumt", falls diese von der zu löschenden Rolle erben
   * oder der Vater davon sind.          
   */ 
  public function removeRole($role)
  {
    $role = (string)$role;

    /**
     * Durchsuche den kompletten Baum nach Kindern und Eltern der Rolle
     */         
    foreach($this->roles as $key => $value)
    {
      /**
       * Ist diese Rolle ein Kind von einer anderen Rolle?
       */             
      if(isset($value["children"][$role]))
      {
        unset($this->roles[$key]["children"][$role]);
      } 

      /**
       * Ist diese Rolle der Vater von einer anderen Rolle?
       */
      if(isset($value["parents"][$role]))
      {
        unset($this->roles[$key]["parents"][$role]);
      }
    }   

     /**
       * Als letztes noch die Rolle entfernen
      */       
    unset($this->roles[$role]);
    return $this;
  }

  /**
   * @access public
   * @param string $role
   * @param string $resource
   * @param array $do_what
   * @return instance fluent interface
   *     
   * Fügt eine Zugriffsdefinition hinzu   (erlauben)           
   */     
  public function allow($role , $resource = null, array $do_what =array())
  {
    /**
     * Bringe alles auf Kleinbuchstaben    
     */    
    foreach($do_what as &$value)
    {
      $value = strtolower($value);
    }

    $role =  strtolower((string)$role);
    $resource = ($resource === null) ? null : strtolower((string)$resource);

    if(!$this->roleExists($role))
    {
      throw new FW_Acl_Exception("Diese Rolle gibt es nicht: ".$role);
    }

    //wurde eine ressource angegeben?
    if($resource !== null)
    {
      if(!$this->resourceExists($resource))
      {
        throw new FW_Acl_Exception("Diese Resource gibt es nicht: ".$resource);
      }

      //wenn diese rolle alle rechte auf alles hat, werden diese jetzt aufgehoben
      if(isset($this->rules["allow"]["permission_for_everything"][$role]))
      {
        unset($this->rules["allow"]["permission_for_everything"][$role]);
      }

      if(count($do_what) < 1) //hat alle rechte
      {
        //ein leeres Array signalisiert, dass alle aktionen erlaubt sind
        $this->rules["allow"]["resources"][$resource]["roles"][$role] = array();
      }
      else
      {
        //hier werden die aktuellen rechte mit den neuen zusammengeführt
        $rightsNow = (isset($this->rules["allow"]["resources"][$resource]["roles"][$role])) ? $this->rules["allow"]["resources"][$resource]["roles"][$role] : array();
        //array_unique löscht alle doppelte Werte aus dem Array
        $this->rules["allow"]["resources"][$resource]["roles"][$role] = array_unique(array_merge($rightsNow, $do_what));
      }
    }
    else  //keine Resource angegeben
    {
      //falls schon spezielle rechte auf resourcen gegeben wurden, sollen sie gelöscht werden,
      //da der eintrag in permission_for_everything das ersetzt
      foreach($this->rules["allow"]["resources"] as $resource => $roles)
      {
        if(isset($this->rules["allow"]["resources"][$resource]["roles"][$role]))
        {
          unset($this->rules["allow"]["resources"][$resource]["roles"][$role]);
        }
      }

      //Rollen in permission_for_everything dürfen alles
      $this->rules["allow"]["permission_for_everything"][$role] = "__ALL__";

      //Verbote entfernen
      if(isset($this->rules["disallow"]["resources"][$resource]["roles"][$role]))
      {
        unset($this->rules["disallow"]["resources"][$resource]["roles"][$role]);      
      }

      //Alle Rules durchlaufen und Verbote entfernen, weil alles erlaubt werden soll
      foreach($this->rules["disallow"]["resources"] as $_resourcename => $_resource)
      {
        foreach($_resource["roles"] as $_role => $_actions)
        {
          if($_role == $role)
          {
            unset($this->rules["disallow"]["resources"][$_resourcename]["roles"][$_role]);
          }
        }
      }
    }

    //Wurde die Aktion vorher schon verboten?   
    if(isset($this->rules["disallow"]["resources"][$resource]["roles"][$role]))
    {
      //alle erlaubten Aktionen aus ["disallow"] entfernen
      foreach($this->rules["disallow"]["resources"][$resource]["roles"][$role] as $action)
      {          
       $key = array_search($action, $this->rules["disallow"]["resources"][$resource]["roles"][$role]);
        if($key !== false)
        {
          unset($this->rules["disallow"]["resources"][$resource]["roles"][$role][$key]);
        }      
      }
    }

    return $this;    
  }

  /**
   * @access public
   * @param string $role
   * @param string $resource
   * @param array $actions
   * @return instance fluent interface
   *     
   * Fügt eine Zugriffsdefinition hinzu (verbieten)
   *                     
   */ 
  public function disallow($role, $resource, array $actions = array())
  {
    $role = strtolower($role);
    $resource = strtolower($resource);

    //Existiert die Resource?
    if(!$this->resourceExists($resource))
    {
        throw new FW_Acl_Exception("Diese Resource gibt es nicht: ".$resource);
    }

    //Existiert die Rolle?
    if(!$this->roleExists($role))
    {
        throw new FW_Acl_Exception("Diese Rolle gibt es nicht: ".$role);
    }

    //Wieviele Aktionen gibt es?
    $amount_actions = count($actions);

    //Keine Aktion angegeben? => Alles in dieser Resource verbieten
    if($amount_actions < 1)
    {
      //Komplette Resource soll verboten werden
      $this->rules["disallow"]["resources"][$resource]["roles"][$role] = array();

      //Erlaubnis entfernen 
      if(isset($this->rules["allow"]["resources"][$resource]["roles"][$role]))
      {
        unset($this->rules["allow"]["resources"][$resource]["roles"][$role]);
      }
    }
    else //nur bestimmte Aktionen verbieten
    { 
      foreach($actions as &$value) //Referenz, um String auf Kleinbuchstaben zu bringen
      {
        $value = strtolower($value);
      }

      //Alte Verbote in $disallow_old schreiben, um sie zu erweitern
      $disallow_old = (isset($this->rules["disallow"]["resources"][$resource]["roles"][$role]) 
                      ? $this->rules["disallow"]["resources"][$resource]["roles"][$role] 
                      : array());

      //$disallow_old mit $actions verbinden, doppelte Einträge löschen und der Verbotsliste zuweisen
      $this->rules["disallow"]["resources"][$resource]["roles"][$role] = array_unique(array_merge($disallow_old, $actions));
    }

    //Wurde die Aktion vorher schon erlaubt?
    if(isset($this->rules["allow"]["resources"][$resource]["roles"][$role]))
    {
      //alle verbotenen Aktionen aus ["allow"] entfernen
      foreach($this->rules["disallow"]["resources"][$resource]["roles"][$role] as $action)
      {
        //Key der verboteten Aktion suchen und dann löschen
        $key = array_search($action, $this->rules["allow"]["resources"][$resource]["roles"][$role]);
        unset($this->rules["allow"]["resources"][$resource]["roles"][$role][$key]);
      }
    }

    //Hat die Rolle vorher schon Rechte auf alle Resourcen bekommen (permission_for_everything)
    if(isset($this->rules["allow"]["permission_for_everything"][$role]))
    {
      //dann löschen
      unset($this->rules["allow"]["permission_for_everything"][$role]);
    }

    //Hat die Rolle vorher schon Rechte auf alle Aktionen der Resource bekommen?
    if(isset($this->rules["allow"]["resources"][$resource]["roles"][$role]) 
       && is_array($this->rules["allow"]["resources"][$resource]["roles"][$role])
       && count($this->rules["allow"]["resources"][$resource]["roles"][$role]) < 1)
    {
      //Entfernen
      unset($this->rules["allow"]["resources"][$resource]["roles"][$role]);
    }

    return $this;
  }

  /**
   * @access public
   * @return bool
   * @param string $role      
   * 
   * Prüft, ob eine Rolle existiert      
   */     
  protected function roleExists($role)
  {
    return isset($this->roles[strtolower($role)]);
  }

  /**
   * @access public
   * @return bool
   * @param string $resource 
   * 
   * Prüft, ob eine Resource existiert           
   */  
  protected function resourceExists($resource)
  {
    return isset($this->resources[strtolower($resource)]);
  }
}

Viel Spaß beim Testen!

1 Star2 Stars3 Stars4 Stars5 Stars (1 Stimme, durchschnittlich 1,00 / 5)
Loading...


2 Kommentare zu “Änderungen in der ACL meines Frameworks”

  1. […] man in der ACL nur erlauben und nicht verbieten konnte, funktionierte alles wunderbar. Mit Einführung der disallow-Methode haben sich aber irgendwo Fehler eingeschlichen. Der Code ist 17kb lang und da macht es natürlich […]

  2. Die aktuellste Version meines HMVC-Frameworks erhaltet ihr ab sofort immer hier: http://www.net-developers.de/blog/2011/02/13/download-info-shfw-hmvc-framework-in-php/

Hinterlasse einen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

»Informationen zum Artikel

Autor: Simon
Datum: 24.02.2009
Zeit: 14:15 Uhr
Kategorien: Mein MVC-Framework
Gelesen: 7955x heute: 2x

Kommentare: RSS 2.0.
Diesen Artikel kommentieren oder einen Trackback senden.

»Meta