É 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.
Recurso: post Privilégios: add, edit, deleteDesta 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.
composer require zendframework/zend-permissions-aclNão é necessário registrar no arquivo de módulos, estando instalado é o que basta.
<?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');
<?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');
<?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:
// 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']);
<?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 serveE 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: