Zend_Acl e database, situazione decisamente complessa

Ciao Gianni sto realizzando un sistemino per memorizzare moduli, risorse (controller), privilegi (azioni) e i ruoli degli utenti in un database, il concetto base è ispirato ad un tutorial di Jason Eisen.

Sarà per la stanchezza e le sole tre ore di sonno, ma sono decisamente confuso e non sto riuscendo a giungere al capo del problema. Ma ora vediamo di descrivere un po' il problema: praticamente ho 5 tabelle:

aclRoles <id, extends, name>: tabella dei ruoli degli utenti, questa tabella contiene i gruppi di utenti.

Il campo id e name sono autoesplicativi, extends invece rappresenta il ruolo dal quale si vuole ereditare i privilegi.

Di default sono presenti questi due ruoli nel database:

<1, 0, visitors>, <2, 255, administrators>.

I visitatori estendono il ruolo 0 e gli amministratori quello 255, entrambi inesistenti, ma verranno mappati a livello di codice nel codice dell'Acl. Saranno due ruoli speciali che servono per non far sollevare eccezioni "role not found".

aclModules <id, name>: contiene i moduli dell'applicazione

aclResources <id, module, name>: contiene i controller dell'applicazione

aclPrivileges <id, resource, name>: contiene le azioni dell'applicazione

aclRolesPrivileges <role, privilege>: contiene il mapping per l'autorizzazione degli utenti

Il discorso si fa complesso ora: tutti i ruoli sono disabilitati di default, se un ruolo si trova associato ad un privilegio(->risorsa->modulo) vuol dire esso è autorizzato.

Per l'ereditarietà dei ruoli sono riuscito a risolvere grazie alla semplicità e alla potenza delle api dello zend framework.

Putroppo non sono riuscito a mappare bene i privilegi e ad eseguire il controllo sulle autorizzazioni.

Basta parole ora, passiamo al codice:

DROP TABLE IF EXISTS `aclModules`;
CREATE TABLE`aclModules` (
    `id` tinyint(3) unsigned not null auto_increment,
    `name` varchar(32) not null,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB;

DROP TABLE IF EXISTS `aclResources`;
CREATE TABLE `aclResources` (
    `id` tinyint(3) unsigned not null auto_increment,
    `module` tinyint(3) unsigned not null,
    `name` varchar(32) not null,
  PRIMARY KEY  (`id`),
  KEY `aclModuleId` (`id`),
  
  CONSTRAINT `fkAclResources` FOREIGN KEY (`module`) REFERENCES `aclModules` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE
) ENGINE=InnoDB;

DROP TABLE IF EXISTS `aclPrivileges`;
CREATE TABLE `aclPrivileges` (
    `id` smallint(5) unsigned not null auto_increment,
    `resource` tinyint(3) unsigned not null,
    `name` varchar(32) not null,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `uniqueResources` (`id`, `name`),
  KEY `aclPrivilegeId` (`id`),
  KEY `aclPrivilegeName` (`name`),
  
  CONSTRAINT `fkAclPrivileges` FOREIGN KEY (`resource`) REFERENCES `aclResources` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE
) ENGINE=InnoDB;

DROP TABLE IF EXISTS `aclRoles`;
CREATE TABLE `aclRoles` (
    `id` tinyint(3) unsigned not null auto_increment,
    `extends` tinyint(3) unsigned null default null,
    `name` varchar(32) not null,
      PRIMARY KEY  (`id`),
  KEY `aclRoleName` (`name`)
) ENGINE=InnoDB;

DROP TABLE IF EXISTS `aclRolesPrivileges`;
CREATE TABLE `aclRolesPrivileges` (
    `role` tinyint(3) unsigned not null,
    `privilege` smallint(5) unsigned not null,
    `allow` bit(1) not null default 1,
  PRIMARY KEY  (`role`, `privilege`),
  KEY `aclRoleId` (`role`),
  KEY `aclPrivilegesId` (`privilege`),
  
  CONSTRAINT `fkAclRolesPrivileges` FOREIGN KEY (`role`) REFERENCES `aclRoles` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  CONSTRAINT `fkAclRolesPrivileges2` FOREIGN KEY (`privilege`) REFERENCES `aclPrivileges` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE
) ENGINE=InnoDB;

Questo è il codice per la generazione delle tabelle.

Ammettiamo di avere questi dati:

aclRoles
<1, 0, visitors>
<2, 255, administrators>
<3, 1, users>
<4, 3, author>

aclModules
<1, default>
<2, blog>

aclResources
<1, 1, index> che mappato sarebbe: default/index
<2, 2, index> che mappato sarebbe: blog/index




aclPrivileges
<1, 1, index>  che mappato sarebbe: default/index/index
<2, 2, read> che mappato sarebbe: blog/index/read
<3, 2, post> che mappato sarebbe: blog/index/post
<4, 2, edit> che mappato sarebbe: blog/index/edit

aclRolesPrivileges
<1, 1> Visitor può accedere a default/index/index
<3, 2> User può accedere a blog/index/read
<4, 3> Author può accedere a blog/index/post
<4, 3> Author può accedere a blog/index/edit

Il codice dell'Acl è questo:

<?php

Zend_Loader::loadClass('Zend_Acl');

class Gulp_Acl extends Zend_Acl {

    protected static $_instance = null;
    protected $_db;

    protected $_roles;

    private function __construct()
    {
        $this->_db = Zend_Controller_Front::getInstance()->getParam('database');

        Zend_Loader::loadClass('Zend_Acl_Resource');
        Zend_Loader::loadClass('Zend_Acl_Role');

        $defaultMap = array(
            //ruolo fittizio per i visitatori
            array(
                'role' => 0,
                'extends' => null
            ),
            //ruolo fittizio per gli amministratori
            array(
                'role' => 255,
                'extends' => null
            )
        );

        $roles = $this->_db->fetchAll("select id, extends from aclRoles");
        
        $resources = $this->_db->fetchAll(
            "select
                aclModules.name as module,
                aclResources.name as resource,
                aclPrivileges.name as privilege,
                aclRoles.id as role,
                aclRoles.extends as extends

                from aclRolesPrivileges
                inner join aclPrivileges
                    on aclRolesPrivileges.privilege = aclPrivileges.id
                inner join aclResources
                    on aclPrivileges.resource = aclResources.id
                inner join aclModules
                    on aclResources.module = aclModules.id
                inner join aclRoles
                    on aclRolesPrivileges.role = aclRoles.id"
        );
        
        $roles = array_merge($defaultMap, $roles);

        foreach ($roles as $role) {
             if (!$this->hasRole($role['id'])) {
                $this->addRole(new Zend_Acl_Role($role['id']), $resource['extends']);
            }
        }
        
        foreach ($resources as $resource) {
            $resourceId = $resource['module'] .'/'. $resource['resource'];
            if (!$this->has($resourceId)) {
                $this->add(new Zend_Acl_Resource($resourceId));
            }
        }
        $this->deny();
        foreach ($resources as $resource) {
            $resourceId = $resource['module'] .'/'. $resource['resource'];            
            //se il ruolo estende 255 abbiamo un amministratore, abilitiamolo a fare qualsiasi cosa!
            if ($resource['extends'] == 255) {
                $this->allow($resource['role']);
            }
            //altrimenti abilitiamo come specificato nel database
            $this->allow($resource['role'], $resourceId, $resource['privilege']);
        }
    }

    private function __clone()
    {
    }

    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

}

Il codice del plugin di autenticazione, che dovrebbe controllare se:

  • Se l'utente è autenticato estraiamo l'id del gruppo di appartenenza per le successive operazioni
  • Se l'utente non è loggato assegniamo l'id 1, quello del visitatore
  • Se è un amministratore, saltare ogni controllo e restituire la risorsa richiesta, se non esiste l'error controller dovrebbe dare l'erro action (default/error/error), giusto?!
  • Se l'utente non è un amministratore controllare se è abilitato alla risorsa richiesta, se ha i permessi necessari restituire la risorsa richiesta, altrimenti reindirizzare alla pagina di notifica di permessi non sufficienti (default/error/privileges)
  • Se l'utente non è autenticato e la risorsa non è pubblica, richiedere l'autenticazione (default/login/index)
<?php

Zend_Loader::loadClass('Zend_Controller_Plugin_Abstract');

class Gulp_Controller_Plugin_Authentication extends Zend_Controller_Plugin_Abstract
{
    protected $_auth;
    protected $_acl;
    
    public function __construct()
    {
        $this->_auth = Zend_Auth::getInstance();
        $this->_acl = Gulp_Acl::getInstance();
    }
    
    public function preDispatch($request)
    {
        if ($this->_auth->hasIdentity()) {
            //supponiamo che l'id del gruppo di appartenenza
            //sia memorizzato nel registro con la chiave userRole
            $role = Zend_Registry::get('userRole');
        } else {
            //risistemare ad 1:visitor
            $role = 1;
        }

        $module     = $request->getModuleName();
        $controller = $request->getControllerName();
        $action     = $request->getActionName();
        $resource   = $module . '/' . $controller;
        
        Zend_Loader::loadClass('Zend_Acl_Exception');
        
        //se l'utente non ha i permessi:
        if (!$this->_acl->isAllowed($role, $resource, $action)) {
            //se non è autenticato
            if (!$this->_auth->hasIdentity()) {
                //invia all'autenticazione
                $module     = 'default';
                $controller = 'login';
            } else {
                //invia alla pagina di errore, permessi non sufficienti
                $module     = 'default';
                $controller = 'error';
                $action     = 'privileges';
            }
        }

        $request->setModuleName($module)
                ->setControllerName($controller)
                ->setActionName($action)
                ->setDispatched(true);
    }
}

I problemi che ho riscontrato sono questi:

Plugin:

Se la risorsa non è stata registrata viene sollevata un'eccezione, questo è un problema visto che vorrei evitare di registrare

TUTTE le pagine accessibili a meno che non debbano essere utilizzate dagli utenti, in tal caso sarà sufficiente aggiungere

una regola nel database, ma comunque l'admin dovrebbe accedere dovunque senza bisogno di specificare nulla -.-

Questo scatena una sterminata serie di altri problemi che con il mal di testa che ho non ricordo più.

Appena mi ritornano in mente li posto, nel frattempo aspetto fiducioso qualche illuminazione ;)

:bye:

inviato 8 anni fa
Andrea Turso
Andrea Turso
86
X 0 X

Ho aggiunto un campo bit(1) per contenere il valore permetti/nega in modo da modificare la logica di negazione totale + abilitazione progressiva in una più flessibile che mi permetta ad esempio di abilitare completamente tutte le azioni di un controller e poi negarle selettivamente in base a quelle che sono specificate come negate.

Non so se serva davvero, ma a questo punto le sto provando tutte vista la confusione c'ho in testa.

:bye:

risposto 8 anni fa
Andrea Turso
Andrea Turso
86
X 0 X
class Gulp_Controller_Plugin_Authentication extends Zend_Controller_Plugin_Abstract
{
    protected $_auth;
    protected $_acl;
    
    public function __construct()
    {
        $this->_auth = Zend_Auth::getInstance();
        $this->_acl = Gulp_Acl::getInstance();
    }
    
    public function preDispatch($request)
    {
        if ($this->_auth->hasIdentity()) {
            $role = Zend_Registry::get('userRole');
        } else {
            $role = 1;
        }

        $module     = $request->getModuleName();
        $controller = $request->getControllerName();
        $action     = $request->getActionName();
        $resource   = $module . '/' . $controller;

        Zend_Loader::loadClass('Zend_Acl_Exception');

        try {
            echo "<p>Gruppo: {$role}<br />Risorsa richiesta: {$resource}<br /> Privilegio richiesto: {$action} <br /> Autorizzato a visualizzare la risorsa: " . ($this->_acl->isAllowed($role, $resource, $action) ? 'S&igrave;' : 'No') . "<br />Autenticato:" . ($this->_auth->hasIdentity() ? 'S&igrave;' : 'No') . "</p>";
            if (!$this->_acl->isAllowed($role, $resource, $action) && $role != 2) {
                if (!$this->_auth->hasIdentity()) {
                    echo "<p>You're not authenticated, please let yourself in and retry accessing this resource.</p>";
                    $module     = 'default';
                    $controller = 'authentication';
                    $action     = 'login';
                } else {
                    echo "<p>After a deep search you seem to not have enough privileges to access this resource. Try corrupting the administrator.</p>";
                    $module     = 'default';
                    $controller = 'error';
                    $action     = 'privileges';
                }
            }
        } catch (Zend_Acl_Exception $e) {
            $module     = 'default';
            $controller = 'error';
            $action     = 'no-route';
        }

        $request->setModuleName($module)
                ->setControllerName($controller)
                ->setActionName($action)
                ->setDispatched(true);
    }
}

Perché questo codice, richiedendo: user/profile

(accessibile al gruppo 1)

Produce questo giro:

Gruppo: 1
Risorsa richiesta: default/user
Privilegio richiesto: profile
Autorizzato a visualizzare la risorsa: Sì
Autenticato:No

Gruppo: 1
Risorsa richiesta: default/error
Privilegio richiesto: error
Autorizzato a visualizzare la risorsa: No
Autenticato:No

Innanzitutto perché viene eseguito due volte?

Cerchiamo di partire dal capire questo, magari è quello il problema principale.

:bye:

risposto 8 anni fa
Andrea Turso
Andrea Turso
86
X 0 X

Bene risolto:

cambiato preDispatch in dispatchLoopStartup.

Stay tuned ^^

risposto 8 anni fa
Andrea Turso
Andrea Turso
86
X 0 X

Ho risolto tutti i problemi con l'auth plugin per ora.

È stato necessario creare un plugin error handler per la gestione degli errori (più che altro per svincolare le responsabilità di controllo di esitenza dei file dal plugin di autenticazione) ed aggiungere un blocco di controllo sull'esistenza della risorsa. Questo perché visto che l'amministratore ha accesso a tutte le risorse volevo evitare di dover definire tutte le regole per l'admin e così ho deciso di privilegiare l'amministratore con il bypass del controllo nel database.

Ma se la risorsa nel database non esiste c'era il problema dell'eccezione "resource not found". Così ho aggiunto un costrutto try catch per gestire l'eccezione e nella gestione dell'eccezione controllo che la risorsa richiesta esista fisicamente, se l'utente è admin do' l'accesso, altrimenti ridirigo alla pagina di errore permessi, altrimenti ancora a file non trovato.

Ecco qui il codice

<?php

Zend_Loader::loadClass('Zend_Controller_Plugin_Abstract');

class Gulp_Controller_Plugin_Authentication extends Zend_Controller_Plugin_Abstract
{
    protected $_auth;
    protected $_acl;
    
    public function __construct()
    {
        $this->_auth = Zend_Auth::getInstance();
        $this->_acl = Gulp_Acl::getInstance();
    }
    
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $targets = array(
            '_noRoute' => array(
                'module'        => 'default',
                'controller'    => 'error',
                'action'        => 'no-route'
            ),
            '_noPrivileges' => array(
                'module'        => 'default',
                'controller'    => 'error',
                'action'        => 'privileges'
            ),
            '_noAuthentication' => array(
                'module'        => 'default',
                'controller'    => 'authentication',
                'action'        => 'login'
            )
        );
        if ($this->_auth->hasIdentity()) {
            $role = Zend_Registry::get('userRole');
        } else {
            $role = 1;
        }

        $module     = $request->getModuleName();
        $controller = $request->getControllerName();
        $action     = $request->getActionName();
        $resource   = $module . '/' . $controller;
        
        $target = array(
            'module'        => $module,
            'controller'    => $controller,
            'action'        => $action
        );
        
        Zend_Loader::loadClass('Zend_Acl_Exception');

        try {
            if (!$this->_acl->isAllowed($role, $resource, $action) && $role != 2) {
                if (!$this->_auth->hasIdentity()) {
                    echo "<p>You're not authenticated, please let yourself in and retry accessing this resource.</p>";
                    $target = $targets['_noAuthentication'];
                } else {
                    echo "<p>After a deep search you seem to not have enough privileges to access this resource. Try corrupting the administrator.</p>";
                    $target = $targets['_noPrivileges'];
                }
            }
        } catch (Zend_Acl_Exception $e) {
            $front = Zend_Controller_Front::getInstance();
            $error = $front->getPlugin('Gulp_Controller_Plugin_ErrorHandler');
            $dispatcher = $front->getDispatcher();

            if (!$error->isProperAction($dispatcher, $request)) {
                $target = $targets['_noRoute'];
            } else {
                if ($role != 2) {
                    $target = $targets['_noPrivileges'];
                }
            }
        }
        $request->setModuleName($target['module'])
                ->setControllerName($target['controller'])
                ->setActionName($target['action']);
    }
}

Ora torno a gestire i problemi con la generazione delle risorse e dei ruoli.

Stay tuned.

:bye:

risposto 8 anni fa
Andrea Turso
Andrea Turso
86
X 0 X
Effettua l'accesso o registrati per rispondere a questa domanda