Desenvolvimento

4 out, 2011

Um caso de uso do Zend_Auth e Zend_Acl com DB e LDAP

Publicidade

Durante uma das minhas consultas, eu ajudei um cliente a
implementar uma autenticação e um mecanismo de autorização para uma aplicação
do Zend Framework. Como esse cenário é bastante comum nas
aplicações de negócios PHP, decidi escrever este artigo para apresentar uma
solução possível.

Os requisitos para a aplicação web eram:

  • implementar
    uma página de login para acessar a aplicação web;
  • autorizar
    o uso da aplicação baseado em grupos de usuários;
  • gerenciamento
    da arquitetura MVC do ZF usando módulos;
  • implementar
    uma lista de controle de acesso (ACL) para autorizar módulos específicos,
    controllers e ações para grupos de usuários;
  • implementar
    uma autenticação LDAP ou autenticação de banco de dados (baseada no perfil do usuário);
  • usar
    um banco de dados para gerenciar os grupos de usuários e as relativas
    autorizações;
  • bom
    desempenho (o ACL pode ser grande).

Nessa implementação, usamos a classe Zend_Auth
para autenticar os usuários e Zend_Acl para implementar uma lista de
controle de acesso baseada em módulos ZF, controllers e ações. Usamos um plugin FrontController para inserir a autenticação e a checagem de autorização usando o
método preDispatch(). Usamos Zend_Auth_Adapter_DbTable e
Zend_Auth_Adapter_Ldap para implementar a autenticação usando um servidor
DB e LDAP.

A implementação segue o critério comum de uma
aplicação ZF. Na minha opinião, uma das partes mais interessantes é a definição
de permissão para os recursos com os grupos de usuários. Usamos uma sintaxe especial utilizando a
seguinte estrutura: “module/controller/action”, onde module, controller e action são
os nomes dos componentes específicos. Decidimos implementar uma maneira de
especificar componentes múltiplos usando o caractere *. Isso significa que você
pode utilizar recursos como “module/*/*” para habilitar ou desabilitar o acesso
a todos os controllers e as ações de um módulo específico.

Ao usar esse mecanismo, você pode fornecer regras
complexas com poucas definições, por exemplo, para permitir o acesso de controllers genéricos e para negar o acesso para algumas ações específicas, você
somente precisará apenas de dois recursos.

A ideia é habilitar ou desabilitar o acesso a
recursos específicos baseado na permissão, começando do mais genérico para o
mais específico, usando a abordagem de cima para baixo:

  1. */*/*
  2. module/*/*
  3. module/controller/*
  4. module/controller/action

Uma regra específica ganha de uma regra geral. A
não ser que seja especificado o contrário, todos os recursos são desabilitados
por padrão.

Usamos o seguinte banco de dados MySQL para implementar
a estrutura de dados do sistema de autenticação e autorização:

CREATE TABLE 'permissions' (
'id' INT(11) NOT NULL AUTO_INCREMENT,
'id_role' INT(11) NOT NULL,
'id_resource' INT(11) NOT NULL,
'permission' enum('allow','deny') NOT NULL,
PRIMARY KEY (`id`)
);

CREATE TABLE 'resources' (
'id' INT(11) NOT NULL AUTO_INCREMENT,
'resource' VARCHAR(128) NOT NULL,
PRIMARY KEY ('id')
);

CREATE TABLE 'roles' (
'id' INT(11) NOT NULL AUTO_INCREMENT,
'role' VARCHAR(40) NOT NULL,
'id_parent' INT(11) NOT NULL,
PRIMARY KEY (`id`)
);

CREATE TABLE 'users' (
'username' VARCHAR(40) NOT NULL,
'password' VARCHAR(40) NOT NULL,
'id_role' INT(11) NOT NULL,
'ldap' tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY USING BTREE (`username`)
);

Temos 4 tabelas: usuários, funções, recursos e permissões. Na tabela
usuários, nós armazenamos o perfil do usuário com a função de cada um deles e o
mecanismo de autenticação, usando um servidor LDAP (ldap=1) ou um banco de
dados (ldap=0).

Na tabela funções, fornecemos uma solução genérica
para os privilégios de herança de um grupo pai (usando o campo id_parent). Nas
permissões, inserimos um campo (permission) que pode ser usado para permitir ou
negar o acesso de um recurso para um grupo específico de usuários.

Para o requisito de desempenho, usamos um sistema de
caching para o objeto ACL. Isso significa que o sistema faz o cache do objeto
Zend Acl baseado na função do usuário e o reutiliza para pedidos diferents.
Dessa maneira, o acesso ao banco de dados MYSQL para o ACL é feito somente uma
vez para cada grupo de usuários. O tempo de vida do cache pode ser bastante
longo, porque geralmente o ACL é raramente feito, e está relacionado ao tempo
de atualização do ACL no banco de dados. Usamos os backends do APC para fazer o
cache da lista de controle de acesso.

Notas sobre o exemplo

Aqui você pode fazer o download da implementação: download do código-fonte

Criamos uma aplicação ZF que tem dois módulos: login
e home. No módulo login, inserimos toda a lógica de autorização e
de autenticação. Essa solução é bastante boa, porque você pode simplesmente
reutilizá-la em diferentes aplicações ZF sem grandes mudanças.

No módulo home, inserimos somente duas páginas
estáticas: index e menu. Isso é apenas para fornecer um exemplo
de premissas diferentes. No banco de dados fornecido com o exemplo, tempos
dois usuários: admin e enrico (com senhas ‘admin‘ e ‘enrico‘).
Admin consegue acessar todos os módulos, controllers e ações da aplicação
(usando o recurso de sintaxe ‘*/*/*’). Enrico consegue
acessar todos os recursos no módulo home, exceto pelo menu de ação no índice de controllers (isso é especificado com as duas regras a seguir: allow ‘home/*/*’,
deny ‘home/index/menu’).

Tente acessar com o admin, e peça a url /home/index/menu.
Depois faça o logout e o login com enrico para tentar acessar de novo a
url /home/index/menu. Você verá que a aplicação irá te redirecionar para
a página de login porque enrico não tem permissão para acessar essa url.

A parte mais interessante da implementação é a classe
Login_Plugin_SecurityCheck, que é o plugin de controle de frente. Aqui
está especificada a lógica da autorização do usuário no método privado _isAllowed()
(linhas 73-90).

Abaixo está o código-fonte desse plugin:

class Login_Plugin_SecurityCheck extends Zend_Controller_Plugin_Abstract
{
const MODULE_NO_AUTH='login';
private $_controller;
private $_module;
private $_action;
private $_role;

/**
* preDispatch
*
* @param Zend_Controller_Request_Abstract $request
*/
public function preDispatch (Zend_Controller_Request_Abstract $request)
{
$this->_controller = $this->getRequest()->getControllerName();
$this->_module= $this->getRequest()->getModuleName();
$this->_action= $this->getRequest()->getActionName();
$auth= Zend_Auth::getInstance();
$redirect=true;
if ($this->_module != self::MODULE_NO_AUTH) {
if ($this->_isAuth($auth)) {
$user= $auth->getStorage()->read();
$this->_role= $user['id_role'];
$bootstrap = Zend_Controller_Front::getInstance()
->getParam('bootstrap');
$db= $bootstrap->getResource('db');

$manager = $bootstrap->getResource('cachemanager');
$cache = $manager->getCache('acl');

if (($acl= $cache->load('ACL_'.$this->_role))===false) {
$acl= new Login_Acl($db,$this->_role);
$cache->save($acl,'ACL_'.$this->_role);
}

if ($this->_isAllowed($auth,$acl)) {
$redirect=false;
}
}
} else {
$redirect=false;
}

if ($redirect) {
$request->setModuleName('login');
$request->setControllerName('index');
$request->setActionName('index');
}
}
/**
* Check user identity using Zend_Auth
*
* @param Zend_Auth $auth
* @return boolean
*/
private function _isAuth (Zend_Auth $auth)
{
if (!empty($auth) && ($auth instanceof Zend_Auth)) {
return $auth->hasIdentity();
}
return false;
}
/**
* Check permission using Zend_Auth and Zend_Acl
*
* @param Zend_Auth $auth
* @param Zend_Acl $acl
* @return boolean
*/
private function _isAllowed(Zend_Auth $auth, Zend_Acl $acl)
{
if (empty($auth) || empty($acl) ||
!($auth instanceof Zend_Auth) ||
!($acl instanceof Zend_Acl)) {
return false;
}
$resources= array (
'*/*/*',
$this->_module.'/*/*',
$this->_module.'/'.$this->_controller.'/*',
$this->_module.'/'.$this->_controller.'/'.$this->_action
);
$result=false;
foreach ($resources as $res) {
if ($acl->has($res)) {
$result= $acl->isAllowed($this->_role,$res);
}
}
return $result;
}
}

?

Texto original disponível em http://www.zimuel.it/en/a-use-case-of-zend_acl-and-zend_auth-with-db-and-ldap/