ACL em PHP com o CakePHP

Post publicado em 10/11/2014 19:55 Última atualização em 12/12/2017 15:47

Boas, vou mostrar hoje como habilitar e configurar o ACL (Access Control List) do CakePHP rodando sobre a engine PHP. Por padrão encontramos vastos exemplos com a engine Database (DbAcl) e toda a documentação que possuímos para o PhpAcl está contida no arquivo app/Config/acl.php. Mãos na massa! [UPDATE] Este post foi escrito em 2014 utilizando como base o CakePHP na ver

Antes de qualquer coisa

Você deve entender que há duas formas de tratar acessos à recursos, uma delas é o ACL e outra é o RBAC (Role Based Access Control). Apesar de estar explícito na documentação do Cake que ele utiliza ACL nada impede que você utilize este recurso como RBAC. O RBAC na verdade consiste em definir os privilégios baseados em grupos. Exemplo: o grupo admin possui o privilégio de acessar tudo no sistema, com isto, presume-se que todos os usuários do grupo admin possuem os mesmos privilégios. O grupo gerente possui os mesmos privilégios do admin, com a excessão de remover usuários. Logo todos os usuários do grupo gerente conseguem fazer tudo no sistema, menos remover usuários. Com o ACL esta lista pode ser mais específica, UM usuário específico do grupo gerente pode ter o privilégio de remover usuários. Neste caso já não estamos mais falando de uma simples lista de permissões por grupo, mas sim de uma lista específica onde cada usuário herda as permissões e privilégios de seu grupo base podendo ao longo do tempo ir recebendo mais e mais privilégios. Pois bem, o cake está preparado para ambas as situações, tanto no DbAcl quanto no PhpAcl e ainda no IniAcl. Ele é versátil o suficiente para que você trabalhe com RBAC e ACL utilizando as mesmas configurações ;)

Requisitos

  • CakePHP instalado e configurado
  • Acesso ao banco de dados
  • Um editor ou IDE que você prefira, utilizarei o Netbeans.
 

Criando o banco de dados

Assim como nos exemplos que encontramos para o DbAcl, precisaremos criar no mínimo 2 (duas) tabelas no banco de dados, uma para os papéis (roles) e outra para os usuários (users), siga o exemplo abaixo.
CREATE TABLE `roles` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `created` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(10) unsigned NOT NULL,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `password` varchar(60) COLLATE utf8_unicode_ci NOT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
 

"Bakeando" tudo

Agora que já temos a estrutura do banco de dados, utilize um pouco de mágica para prosseguir. Em seu terminal, entre na pasta onde seu projeto se encontra e digite:
$ ./app/Console/cake bake all Role 
$ ./app/Console/cake bake all User
  Perfeito, já temos toda a estrutura básica que precisamos, resta apenas criar as actions de login e logout e a view para o login.
<?php 
// UsersControllers.php

public function login() {
        if ($this->request->is('post')) {
            if ($this->Auth->login()) {
                $this->Session->setFlash('Logado com sucesso!');
                $this->redirect($this->Auth->redirectUrl());
            }
            $this->Session->setFlash('Dados de login inválidos');
        }
}

public function logout() {
        $this->Auth->logout();
        $this->redirect($this->Auth->redirectUrl());
}

?>

//View/Users/login.ctp

<?php echo $this->Form->create('User', array('id' => 'login')); ?>
<p>
    <?php echo $this->Form->input('email'); ?>
</p>
<p>
    <?php echo $this->Form->input('password'); ?>
</p>
<p>
    <?php echo $this->Form->end(__('Realizar login')); ?>
</p>
Agora já temos o CRUD para Role, o CRUD para User, o login e o logout. Vamos inicialmente adicionar um papel (role) no sistema. Em seu browser acesse seu projeto e ao final da URL adicione /roles, caso já tenha uma barra no final, adicione roles apenas. Deve aparecer uma tela semelhante à abaixo. 01-roles Clique em New Role e pra iniciar defina o nome como Admin, clique em "Submit". Em seguida cadastre um novo papel chamado Editor, apenas para exemplificar. Após isto o resultado devera ser como o apresentado abaixo. 02-roles Agora cadastraremos um usuário para cada papel, um para o admin e outro para o editor. 03-users Perceba que no campo senha é perceptível que estou utilizando o algoritmo Blowfish (início da string com $2a$10$), isto é opcional, caso você faça com outro algoritmo qualquer ou mesmo deixando com o default do Cake já funciona.  

Configurando o AppController

No AppController devemos configurar os seguintes itens:
  • Controller/action para o login do usuário
  • Controller/action após o login ser realizado com sucesso
  • Controller/action após o usuário deslogar-se
  • Actions habilitadas para usuários não logado (1 ao menos, a não ser que você queira ter um TOO_MANY_REDIRECTS...)
  • Mensagem para quando o usuário bater em um recurso ao qual não tenha privilégio
O código abaixo mostra os passos descritos restando apenas a validação de fato do ACL que será vista em breve.
// AppController.php

public function beforeFilter() {

        // Definindo o algorítmo de hash para a senha (OPCIONAL)
        $this->Auth->authenticate = array('Blowfish' => array(
                'userModel' => 'User',
                'fields' => array('username' => 'email')
        ));
        
        // Informando controller/action para login
        $this->Auth->loginAction = array(
            'controller' => 'users',
            'action' => 'login'
        );
        
        // controller/action após realizar o login
        $this->Auth->loginRedirect = array(
            'controller' => 'pages',
            'action' => 'home');
        
        // controller/action após realizar o logout
        $this->Auth->logoutRedirect = array(
            'controller' => 'users',
            'action' => 'login'
        );
        
        // Actions habilitadas para usuários não logados
        $this->Auth->allow('login', 'display', 'home');
        
        // Definindo uma mensagem de erro do ACL
        $this->Auth->authError = 'Suas permissões não concedem acesso ao recurso solicitado.';
       
        parent::beforeFilter();
    }
Vamos deixar o AppController completo para o funcionamento para somente então começarmos a definir nosso ACL. Ainda dentro do método beforeFilter adicione o seguinte trecho de código:
if ($this->Auth->user()) {
            
    if( !$this->isAuthorized() ) {
        $this->Session->setFlash($this->Auth->authError);
        $this->redirect($this->Auth->redirectUrl());
    }
            
    $this->Auth->allow();
}
Perceba que estamos chamando um método chamado isAuthorized, logo precisamos criá-lo:
protected function isAuthorized() {

    // verifica o recurso solicitado
    $aco = 'controllers/'.$this->params['controller'];

    //Informando qual é meu grupo
    $aro = $this->Auth->user('role_id');
        
    //Retornando a validação do privilégio solicitante - recurso/privilegio
    return $this->Acl->check($aro, $aco, $this->params['action']);
}
No AppController tudo pronto! Agora temos mais dois passos, a definição do mecanismo do ACL a ser utilzado e sua configuração e as definições de nossas regras (listas).  

Configurando o PhpAcl

Tudo começa no arquivo app/Config/core.php, na linha 263 altere o valor DbAcl para PhpAcl e remova o conteúdo da linha 264.
/**
 * The class name and database used in CakePHP's
 * access control lists.
 */
    Configure::write('Acl.classname', 'PhpAcl');

/**
Ok, agora já temos a definição de que o ACL será tratado pelo mecanismo (engine) PhpAcl e não mais pelo DbAcl.  

Criando regras

Antes de qualquer coisa devemos entender o que podemos configurar no app/Config/acl.php, vamos lá? Em $config['map']  apenas dizemos quem será comparado, pode ser User, pode ser Role, pode ser Group... enfim, será o que você registrar. No meu caso vou apenas adicionar o Role informando que o parâmetro de comparação é o campo role_id da model User.
$config['map'] = array(
    'Role' => 'User/role_id',
);
Em $config['alias'] apelidamos nossos papeis para que fique mais fácil o mapeamento. Como cadastramos os papéis admin e editor, adicionamos os mesmos nesta configuração informando qual é a model, o id e definimos seu apelido:
$config['alias'] = array(
    'Role/1' => 'Role/admin',
    'Role/2' => 'Role/editor',
);
A próxima configuração simplesmente deixamos como no exemplo abaixo:
$config['roles'] = array(
    'Role/admin'   => null,
    'Role/editor' => null,
);
Neste ponto, caso um papel precise estender privilégios de outro(s) basta que adicionemos 'Role/y' => 'Role/x, Role/a ...' ao invés de 'Role/y' => null. A última configuração são as definições dos recursos/privilégios. Neste ponto é bom que fiquem claro alguns pontos:
  • O ACL trabalha como um proxy, mas ao contrário, libera-se tudo e bloqueia aos poucos
  • Se um papel estender de um ou mais papéis seus privilégios devem estar na primeira linha juntamente com os que o mesmo estende
Como mencionei, em uma configuração de proxy normalmente quem está configurando bloqueia tudo e em seguida vem liberando aos poucos os acessos aos usuários. No ACL é ao contrário. Por trabalhar da forma FIFO (First In First Out) o correto é que todos os bloqueios sejam definidos por último. Com relação à um papel estender de outro, tive árduo trabalho de descobrir isso sozinho na prática. Quando eu setei dois papeis (role), o segundo estendendo do primeiro, adicionei os acessos do primeiro papel em uma linha e os acessos do segundo na linha abaixo, sempre tive acesso negado. Vamos ver na prática logo.
$config['rules'] = array(
    'allow' => array(
        '*' => 'Role/admin, Role/editor',
    ),
    'deny' => array(
        //'controllers/roles/view' => 'Role/admin'
    ),
);
Como mencionei há pouco, se eu tivesse feito declarado '*' => 'Role/admin' numa linha e '*' => 'Role/editor' na outra com certeza teríamos problemas ao acessar qualquer recurso estando logado com o perfil editor. Por isso esta configuração deve estar toda na primeira linha, a não ser que você tenha uma regra muito específica, coloque tudo na primeira linha.  

Na prática

Temos tudo preparado até o momento, note que deixei uma linha comentada em 'deny' na última configuração apresentada, ela será descomentada daqui a pouco. Acesse uma URL qualquer do seu projeto, se tudo ocorreu de forma correta sua tela inicial permanecerá a página inicial do Cake. Ao acessar qualquer recurso como /users ou /roles o login será solicitado. Caso não apareça a tela de login e você tenha a certeza de que não está logado, pare agora e confira tudo apresentado até o momento. Deve solicitar seu login! acl-redirect-to-login Feito o login como admin criado anteriormente (no meu caso foi email: admin@admin.com.br e senha: admin) você será redirecionado para o recurso que tentou acessar, no meu caso /users. acl-list-users Clique no botão "New Role". Neste momento o acesso deve ser concedido com sucesso. acl-add-reole Agora no arquivo app/Config/acl.php bloqueie o acesso à adicionar novo papel do admin, somente para teste:
$config['rules'] = array(
    'allow' => array(
        '*' => 'Role/admin, Role/editor',
    ),
    'deny' => array(
        'controllers/roles/add' => 'Role/admin'
    ),
);
Dê um refresh em seu browser e "ta dá"! acl-acesso-negado Pode remover este boqueio, afinal de contas você está logado como admin e, neste caso o admin pode tudo. Deslogue-se do sistema através da URL /users/logout e logue-se como editor. Eu cadastrei um usuário com email: andrecardosodev@gmail.com e senha: andre. Novamente solicitei o recurso /users, e tendo logado corretamente vou para a lista de usuários. Como eu sou um editor de um blog por exemplo, não devo ter permissão para adicionar, editar e deletar um grupo e também não pode adicionar e nem deletar um usuário. Vamos às definições:
$config['rules'] = array(
    'allow' => array(
        '*' => 'Role/admin, Role/editor',
    ),
    'deny' => array(
        'controllers/roles/(add|edit|delete)' => 'Role/editor',
        'controllers/users/(add|edit|delete)' => 'Role/editor',
    ),
);
Pronto, tudo que especificamos será negado ao usuário logado com o papel editor, vamos ao teste? Acesse a URL /roles, ok, aqui você pode chegar, agora clique no botão "View" em um role qualquer. Aqui também, tudo ok. Agora na tela de visualização clique em "Edit", ops... você não possui acesso à este recurso, funcionou! acl-acesso-negado Se quiser repetir os passos para os recursos de /users sinta-se à vontade. Devem ter as mesmas restrições que /roles, você não poderá adicionar, nem editar nem remover um usuário. Agora basta que as regras se apliquem à todos os papeis, recursos/privilégios de seu projeto. Por hoje é isso pessoal, espero que tenham gostado da explicação. Em um novo post dentro de alguns dias mostrarei como utilizar de fato o ACL, setando permissões especificas por usuários e não somente por grupos como mostrado aqui. Os fontes do exemplo aqui apresentado estão no Github. Aprenda mais sobre o CakePHP, clique aqui.


Scroll down