ACL com Zend Framework – Parte 1

É comum que em diversos portes de aplicações precisemos separar os usuários com níveis de acesso a fim de prover mais segurança e confiabilidade. O meio mais seguro de limitar acessos à áreas específicas é por meio de ACL (Access Control List). ACL consiste em criar um conjunto de regras de acesso que serão validadas a cada requisição, tornando pontos sensíveis da aplicação mais seguros.

Este artigo mostrará os conceitos básicos da utilização do ACL no Zend Framework 3, no entanto os exemplos são funcionais para o Zend 2 também. A ideia neste primeiro artigo é focar nos pontos simples, em um artigo futuro, escreverei sobre o uso mais complexo.

Do que precisaremos

  • Composer
  • PHP 5.5 ou superior
  • Sua IDE ou editor favorito

Inciando uma nova aplicação

Não entrarei em detalhes da criação do novo projeto com o Zend Skeleton Application, para isto, siga o exemplo de instalação do próprio Skeleton Application.

Neste primeiro artigo não será utilizado nenhum banco de dados, será tudo conforme a documentação do ACL do ZF, apenas traduzido para melhor entendimento de iniciantes e comentados alguns macetes que facilitarão o seu dia a dia.

Com o Skeleton Application instalado e configurado, chegou o momento de iniciarmos a configuração do nosso ACL, mas primeiro um pouco de conceito para que você que ainda não conhece, saber do que será falado daqui pra frente.

O que é ACL?

ACL significa Access Control List, ou na tradução livre para português, lista de controle de acesso. Quando trabalhamos com ACL possuímos dois atores principais, o role (papel) e resource (recurso), onde o resource é um objeto cujo o acesso é controlado e o role é quem solicita permissão para acessar determinado recurso. Seguindo o exemplo apresentado na própria documentação do Zend Framework, um role (vou referir sempre em inglês pra ser mais semântico) solicita acesso à recursos.

Ainda existe o privilege (privilégios) que nada mais é que a granulação das permissões do recurso. Ex:

Recurso: post
Privilégios: add, edit, delete

Desta forma é possível criar o seguinte relacionamento: Role → Resouce → Privileges. Tudo isso será descrito no decorrer deste artigo.

Pra simplificar o trabalho dos desenvolvedores, o ZF já possui implementação de ACL. No entanto este recurso não vem por padrão no Zend Skeleton Application, tampouco vem configurado. Então vamos o instalar e configurar.

Instalando Permissions ACL

Depois de já ter iniciado o projeto com o Zend Skeleton Application, rode o comando abaixo:

composer require zendframework/zend-permissions-acl

Não é necessário registrar no arquivo de módulos, estando instalado é o que basta.

Definindo os recursos

Para fim de exemplo somente, eu criei uma pasta chamada Acl dentro de module/Application/src mas você pode até mesmo criar um módulo à parte para tratar somente do ACL. Nesta pasta crie uma classe chamada Resources com o conteúdo a seguir.

<?php
# module/Application/src/Acl/Resources.php
namespace Application\Acl;

use Application\Controller\PostController;
use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Resource\GenericResource;

/**
 * Class Resources
 * @package Application\Acl
 */
class Resources
{
    public function __construct(Acl $acl)
    {
        $acl->addResource(new GenericResource(PostController::class));
    }
}

É necessário utilizar a classe GenericResource para registrar um resource? Não. É apenas uma forma de padronizar. Você pode adcionar uma string que funcionará da mesma forma. Ex: $acl->addResource(PostController::class); ou ainda $acl->addResource(‘post’);

O que foi feito?

Foi adicionado um recurso de post, o controller em si. Foi feito desta forma (PostController::class) pelos seguintes motivos:

  • Fácil localização. Não preciso ficar lembrando qual é o nome do recurso: foi ‘post’, ‘posts’, ‘Post’…? Simplesmente é o nome do controller;
  • Facilita a refatoração, uma vez que o nome da classe é que está associado, se este mudar, o resource é alterado em todos os pontos em que há validação.

Definindo os papéis

Agora crie uma classe chamada Roles. Ela fará o papel de informar quais são os papéis disponíveis em nossa aplicação.

<?php
# module/Application/src/Acl/Roles.php
namespace Application\Acl;

use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Role\GenericRole;

/**
 * Class Roles
 * @package Application\Acl
 */
class Roles
{
    public function __construct(Acl $acl)
    {
        $acl->addRole(new GenericRole('admin'));
        $acl->addRole(new GenericRole('editor'));
        $acl->addRole(new GenericRole('guest'));
    }
}

Da mesma forma que em addResource, em addRole não é necessário utilizar a classe GenericRole, pode ser uma string mesmo. Ex: $acl->addRole(‘admin’);

O que foi feito?

Foram criados 3 perfis de acesso, um admin, um editor e um visitante. Neste momento não existe nada ainda sobre os privilégios, então vamos defini-los agora.

Privilégios

Sabemos que um role pode solicitar acesso à um resource, mas será que ele está apto a utilizar um determinado privilégio deste resource? É aí que entram os registros dos privilégios. Para exemplificar, fica aí uma pergunta: Um editor (role) pode remover algo (privilege) de post (resource)? A resposta é obtida com utilização do $acl->allow() ou $acl->deny().

Em nossa classe Roles, adicionamos os privilégios de cada um dos recursos para cada um dos papéis.

<?php
# module/Application/src/Acl/Roles.php
namespace Application\Acl;

use Application\Controller\PostController;
use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Role\GenericRole;

/**
 * Class Roles
 * @package Application\Acl
 */
class Roles
{
    public function __construct()
    {
        $acl = new Acl();

        $acl->addRole(new GenericRole('admin'));
        $acl->addRole(new GenericRole('editor'));
        $acl->addRole(new GenericRole('guest'));

                  // role       resource                privileges
        $acl->allow('admin', PostController::class, ['add', 'edit', 'delete', 'index', 'view']);
        $acl->allow('editor', PostController::class,  ['add', 'edit', 'index', 'view']);
        $acl->allow('guest', PostController::class, ['index', 'view']);
    }
}

Agora sim, já temos nosso ACL minimamente definido e funcional. Só isso basta para que consigamos bloquear ou liberar acessos baseados no perfil do usuário.

Como mencionado, existe o método deny(), que serve para bloquear privilégios. Fica a seu critério o workflow a seguir:

  1. Fecha tudo a vai abrindo aos poucos (semelhante ao funcionamento de firewall) OU
  2. Abre tudo e vai fechando aos poucos.

Em projetos que desenvolvi sempre fechei tudo e fui liberando os acessos aos poucos (workflow 1), isso porque é mais fácil de controlar. É muito mais simples localizar “alguém” que não está tendo acesso e o liberar, do que identificar “quem” não deveria estar apto à um recurso/privilégio e o bloquear. Mas quem sabe para o seu cenário a opção 2 seja a mais indicada, tudo é passível de análise.

Veja um exemplo dos dois cenários para o papel guest.

// Workflow 1: Com tudo bloqueado, abre as permissões aos poucos
$acl->allow('guest', PostController::class, ['index', 'view']);

// Workflow 2:
// Libera tudo
$acl->allow('guest');

// Bloqueia aos poucos
$acl->deny('guest', PostController::class, ['add', 'edit', 'delete']);

Verificando as permissões

Crie um controller chamado PostController, nele adicione o conteúdo a seguir. Adicionei comentários em tudo pra que fique fácil de entender.

<?php
# module/Application/src/Controller/PostController.php
namespace Application\Controller;

use Application\Acl\Resources;
use Application\Acl\Roles;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Role\GenericRole;
use Zend\View\Model\ViewModel;

class PostController extends AbstractActionController
{
    public function indexAction()
    {
        // o privilégio do recurso PostController (index)
        $privilege = str_replace('Action', '',__FUNCTION__);

        // Chamando nosso Acl
        $acl = new Acl();

        /*
         * Carregando as configurações de resources, roles e privileges.
         *
         * A ordem tem que ser esta, recursos primeiro e roles depois senão, no
         * momento de registrar os privilégios um erro será lançado informando que
         * o recurso não existe.
         */
        (new Resources($acl));
        (new Roles($acl));

        // para fins de exemplo, definindo todos os roles.
        $admin = new GenericRole('admin');
        $editor = new GenericRole('editor');
        $guest = new GenericRole('guest');

        // e verificando um a um se possui acesso ao recurso com o devido privilégio
        $messageAdmin = $privilege . ': Acesso negado para admin';
        if ($acl->isAllowed($admin, __CLASS__, $privilege)) {
            $messageAdmin = $privilege . ': Acesso garantido para admin';
        }
        var_dump($messageAdmin);

        $messageEditor = $privilege . ': Acesso negado para editor';
        if ($acl->isAllowed($editor, __CLASS__, $privilege)) {
            $messageEditor = $privilege . ': Acesso garantido para editor';
        }
        var_dump($messageEditor);

        $messageGuest = $privilege . ': Acesso negado para guest';
        if ($acl->isAllowed($guest, __CLASS__, $privilege)) {
            $messageGuest = $privilege . ': Acesso garantido para guest';
        }
        var_dump($messageGuest);

        return new ViewModel();
    }
}

Com o controller criado, vamos registrar rota do mesmo para poder testar no navegador.

# module/Application/config/module.config.php

return [
    'router' => [
        'routes' => [
            // ...
            'post' => [
                'type'    => Segment::class,
                'options' => [
                    'route'    => '/post[/:action][/:id]',
                    'defaults' => [
                        'controller' => Controller\PostController::class,
                        'action'     => 'index',
                    ],
                ],
            ]
        ],
    ],
    'controllers' => [
        'factories' => [
            Controller\IndexController::class => InvokableFactory::class,
            Controller\PostController::class => InvokableFactory::class,
        ],
    ],
    // ...
];

E agora vamos rodar nosso Skeleton Application através do comando composer serve.

$ composer serve

E acessando o endereco https://www.andrebian.com/post/index temos o resultado de nosso ACL.

Pra testar um privilégio negado, alterei o nome da action de indexAction para addAction. O resultado esperado é: garantido para o admin, garantido para o editor e negado para o guest.

E está tudo funcionando! Agora basta que caso o recurso e privilégio sejam negados, o usuário seja redirecionado para uma rota qualquer. Preferencialmente esta rota deve ser para uma rota pública, como uma página de erro ou coisa do gênero, caso contrário um acesso redireciona para outro, que redireciona para outro e assim sucessivamente até 1) encontrar uma rota que o usuário possua a devida permissão ou 2) dar Too Many Redirects.

# module/Application/src/Controller/PostController.php

if (!$acl->isAllowed($guest, __CLASS__, $privilege)) {
    // Caso não tenha permissão, adiciono uma mensagem sobre o ocorrido
    $this->flashMessenger()->setNamespace('error')
        ->addMessage('Você não possui autorização para o recurso solicitado.');

    // e redireciono o usuário para uma rota padrão pra evitar Too Many Redirects
    return $this->redirect()->toRoute('home');
}

O resultado é este:

Padronização

Anos atrás eu escrevi sobre ACL com o CakePHP 2 e você pode dar uma passada pra ver como mesmo em frameworks distintos a implementaçao é muito semelhante. O mesmo ocorre para outros frameworks e linguagens de programação.

Concluindo

A intenção foi demonstrar que utilizar ACL no Zend Framework é uma tarefa muito simples. Para um exemplo mais detalhado, eu gravei um screencast e disponiblizei no Youtube.

Como já mencionei, em um artigo futuro vou mostrar como sair desse exemplo básico e implementar Acl “como gente grande”, lendo controllers e actions disponíveis e tornando as permissões de fácil gerenciamento, como na imagem a seguir.

Até breve!

Menu