PHP: Erste Version der ACL (Access Control List)
Wie im Artikel „Die ACL meines Frameworks“ schon angekündigt wurde, wird mein MVC-Framework um eine ACL-Klasse erweitert. Vorbild ist hierbei das ZEND-Framework, das ich normalerweise nicht so gut finde. Die ACL wurde dort aber super umgesetzt.
Was kann die ACL-Klasse?
Es gibt wie bei Zend auch Rollen, Resourcen und Aktionen (in den Resourcen)
Die Rollen können ihre Rechte untereinander vererben. Es gibt Mehrfachvererbung (anders als hier in den Kommentaren geschrieben), das heißt, dass eine Rolle die Rechte von beliebig vielen anderen Rollen erben kann. Die neue Rolle, die aus den Elternrollen entstanden ist, kann danach natürlich um neue Rechte ergänzt werden.
Der Code
Ich möchte euch den Code nicht länger vorenthalten:
Acl.class.php
< ?php /** * @author Simon H. * @package MVC-Framework * @version 0.2 * * 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. */ 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(); public function __construct() { //initialisiere Rechte $this->rules = array ( "permission_all" => null, "resources" => array() ); } public function hasPermission($role, $resource, $do_what) { $role = (string)strtolower($role); $resource = (string)strtolower($resource); $do_what = (string)strtolower($do_what); //als erstes wird geprüft, ob die Rolle Rechte auf alles hat. if(isset($this->rules["permission_all"][$role])) { return true; } //wenn nicht, wird geprüft, ob rechte auf die geasmte Ressource bestehen if(isset($this->rules["resources"][$resource]["roles"][$role]) && count($this->rules["resources"][$resource]["roles"][$role]) < 1) { return true; } //wenn er nich 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["resources"][$resource]["roles"][$role]) && in_array($do_what, $this->rules["resources"][$resource]["roles"][$role])) { 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; } /** * ein leeres $do_what-array heißt, dass alles getan werden darf. * $resource leer = zugriff auf alles (admin) */ public function addRule($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["permission_all"][$role])) { unset($this->rules["permission_all"][$role]); } if(count($do_what) < 1) //hat alle rechte { //ein leeres Array signalisiert, dass alle aktionen erlaubt sind $this->rules["resources"][$resource]["roles"][$role] = array(); } else { //hier werden die aktuellen rechte mit den neuen zusammengeführt $rightsNow = (isset($this->rules["resources"][$resource]["roles"][$role])) ? $this->rules[$resource]["roles"][$role] : array(); //array_unique löscht alle doppelte Werte aus dem Array $this->rules["resources"][$resource]["roles"][$role] = array_unique(array_merge($rightsNow, $do_what)); } } else { //falls schon spezielle rechte auf resourcen gegeben wurden, sollen sie gelöscht werden, //da der eintrag in permission_all das ersetzt foreach($this->rules["resources"] as $resource => $roles) { if(isset($this->rules["resources"][$resource]["roles"][$role])) { unset($this->rules["resources"][$resource]["roles"][$role]); } } //Rollen in permission_all dürfen alles $this->rules["permission_all"][$role] = "__ALL__"; } return $this; } protected function roleExists($role) { return isset($this->roles[strtolower($role)]); } protected function resourceExists($resource) { return isset($this->resources[strtolower($resource)]); } } ?>
Interface.class.php
< ?php interface FW_Acl_Interface { public function hasPermission($role, $resource, $do_what); } ?>
Resource.class.php
< ?php class FW_Acl_Resource implements FW_Acl_Resource_Interface { protected $resource_name; public function __construct($resource_name) { $this->resource_name = (string) $resource_name; } public function getResourceName() { return $this->resource_name; } } ?>
< ?php class FW_Acl_Role implements FW_Acl_Role_Interface { protected $role_name; public function __construct($role_name) { $this->role_name = (string) $role_name; } public function getRoleName() { return $this->role_name; } } ?>
Der Code ist gut kommentiert, sollte also auch leicht verständlich sein. Die vielfach Verschachtelten Arrays könnten evtl. Verständnisprobleme verursachen 😛
Die Anwendung
Hier zeige ich euch noch, wie man das Ganze verwenden kann:
(neue ACL erstellen)
$acl = new FW_Acl;
(Rollen anlegen)
$acl->addRole(new FW_Acl_Role("gast")); $acl->addRole(new FW_Acl_Role("nochEineRolle")); $acl->addRole(new FW_Acl_Role("user"), array("gast", "nochEineRolle")); $acl->addRole(new FW_Acl_Role("moderator"), array("user")); $acl->addRole(new FW_Acl_Role("admin"));
(Resourcen anlegen)
$acl->addResource(new FW_Acl_Resource("startseite")); $acl->addResource(new FW_Acl_Resource("forum")); $acl->addResource(new FW_Acl_Resource("adminbereich")); $acl->addResource(new FW_Acl_Resource("login"));
(Regeln definieren)
$acl->addRule("admin"); //darf alles tun $acl->addRule("gast", "forum", array("read")); $acl->addRule("nochEineRolle", "forum", array("somethingToDo", "anotherThingToDo")); $acl->addRule("user", "forum", array("write"));
(Berechtigungen prüfen )
echo "User darf ".($acl->hasPermission("user", "forum", "read") ? "" : "nicht")." im Forum lesen";
Das würde in diesem Fall „User darf im Forum lesen“ ausgeben.
Zum besseren Verständnis gibts noch ein print_r()
Das sollte den Aufbau der Arrays verdeutlichen.
FW_Acl Object ( [roles:protected] => Array ( [gast] => Array ( [children] => Array ( [user] => FW_Acl_Role Object ( [role_name:protected] => user ) ) [instance] => FW_Acl_Role Object ( [role_name:protected] => gast ) [parents] => Array ( ) ) [irgendjemand] => Array ( [children] => Array ( [user] => FW_Acl_Role Object ( [role_name:protected] => user ) ) [instance] => FW_Acl_Role Object ( [role_name:protected] => irgendjemand ) [parents] => Array ( ) ) [user] => Array ( [children] => Array ( [moderator] => FW_Acl_Role Object ( [role_name:protected] => moderator ) ) [instance] => FW_Acl_Role Object ( [role_name:protected] => user ) [parents] => Array ( [gast] => FW_Acl_Role Object ( [role_name:protected] => gast ) [irgendjemand] => FW_Acl_Role Object ( [role_name:protected] => irgendjemand ) ) ) [moderator] => Array ( [children] => Array ( ) [instance] => FW_Acl_Role Object ( [role_name:protected] => moderator ) [parents] => Array ( [user] => FW_Acl_Role Object ( [role_name:protected] => user ) ) ) [admin] => Array ( [children] => Array ( ) [instance] => FW_Acl_Role Object ( [role_name:protected] => admin ) [parents] => Array ( ) ) ) [resources:protected] => Array ( [startseite] => FW_Acl_Resource Object ( [resource_name:protected] => startseite ) [forum] => FW_Acl_Resource Object ( [resource_name:protected] => forum ) [adminbereich] => FW_Acl_Resource Object ( [resource_name:protected] => adminbereich ) [login] => FW_Acl_Resource Object ( [resource_name:protected] => login ) ) [rules:protected] => Array ( [permission_all] => Array ( [admin] => __ALL__ ) [resources] => Array ( [forum] => Array ( [roles] => Array ( [gast] => Array ( [0] => read ) [irgendjemand] => Array ( [0] => somethingtodo [1] => anotherthingtodo ) [user] => Array ( [0] => write ) ) ) ) ) )
Eure Meinung ist mir sehr wichtig!
Wie findet ihr die Klasse(n)? Was würdet ihr anders machen? Was findet ihr besonders gut gelöst? Über Kommentare freue ich mich wie immer 😉
[…] PHP: Erste Version der ACL (Access Control List) Dez […]
Die Rollen,Ressourcen usw. werden aber nach jeder Laufzeit wieder gelöscht durch das Beenden des Scripts?
Ich habe nirgendwo entdecken können, dass du sie speicherst bzw. sie in einer DB ablegst.
Hi,
das stimmt!
Es ist aber möglich, das ACL-Objekt zu serialisieren und in einer DB abzulegen. So kann man das Objekt in mehreren Skripts verwenden und ändern.
Simon
[…] erste Version der Access Control List (ACL) des Frameworks bot folgende […]
[…] man in der ACL nur erlauben und nicht verbieten konnte, funktionierte alles wunderbar. Mit Einführung der disallow-Methode haben sich aber […]
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/