Verificação de permissões (Autorização)
A autorização verifica se um usuário tem permissões suficientes, por exemplo, para acessar um determinado recurso ou para executar alguma ação. A autorização pressupõe autenticação prévia bem-sucedida, ou seja, que o usuário esteja logado.
Nos exemplos, usaremos o objeto da classe Nette\Security\User, que representa o usuário atual e
ao qual você pode acessar solicitando-o através de injeção de dependência. Nos presenters, basta
chamar $user = $this->getUser().
Para sites muito simples com administração, onde as permissões dos usuários não são diferenciadas, é possível usar
o método já conhecido isLoggedIn() como critério de autorização. Em outras palavras: assim que o usuário
está logado, ele tem todas as permissões e vice-versa.
if ($user->isLoggedIn()) { // o usuário está logado?
deleteItem(); // então ele tem permissão para a operação
}
Papéis
O objetivo dos papéis é oferecer um controle de permissões mais preciso e permanecer independente do nome de usuário.
A cada usuário, logo ao fazer login, atribuímos um ou mais papéis nos quais ele atuará. Os papéis podem ser strings simples,
por exemplo, admin, member, guest, etc. Eles são indicados como o segundo parâmetro do
construtor SimpleIdentity, seja como uma string ou um array de strings – papéis.
Como critério de autorização, agora usaremos o método isInRole(), que informa se o usuário está atuando no
papel especificado:
if ($user->isInRole('admin')) { // o usuário está no papel de admin?
deleteItem(); // então ele tem permissão para a operação
}
Como você já sabe, após o logout do usuário, sua identidade não precisa ser apagada. Ou seja, o método
getIdentity() continua retornando o objeto SimpleIdentity, incluindo todos os papéis concedidos.
O Nette Framework adota o princípio “menos código, mais segurança”, onde menos escrita leva a um código mais seguro,
portanto, ao verificar os papéis, você não precisa verificar também se o usuário está logado. O método
isInRole() trabalha com papéis efetivos, ou seja, se o usuário estiver logado, baseia-se nos papéis
indicados na identidade; se não estiver logado, tem automaticamente o papel especial guest.
Autorizador
Além dos papéis, introduziremos também os conceitos de recurso e operação:
- papel é uma propriedade do usuário – por exemplo, moderador, editor, visitante, usuário registrado, administrador…
- recurso (resource) é algum elemento lógico do site – artigo, página, usuário, item de menu, enquete, presenter, …
- operação (operation) é alguma atividade específica que o usuário pode ou não fazer com o recurso – por exemplo, excluir, editar, criar, votar, …
O autorizador é um objeto que decide se um determinado papel tem permissão para realizar uma determinada
operação com um determinado recurso. É um objeto que implementa a interface Nette\Security\Authorizator com um único
método isAllowed():
class MyAuthorizator implements Nette\Security\Authorizator
{
public function isAllowed($role, $resource, $operation): bool
{
if ($role === 'admin') {
return true;
}
if ($role === 'user' && $resource === 'article') {
return true;
}
// ...
return false;
}
}
Adicionaremos o autorizador à configuração como um serviço do contêiner DI:
services:
- MyAuthorizator
E segue um exemplo de uso. Atenção, desta vez chamamos o método Nette\Security\User::isAllowed(), não
o autorizador, então não há o primeiro parâmetro $role. Este método chama
MyAuthorizator::isAllowed() sequencialmente para todos os papéis do usuário e retorna true se pelo menos um deles
tiver permissão.
if ($user->isAllowed('file')) { // o usuário pode fazer qualquer coisa com o recurso 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // pode realizar 'delete' no recurso 'file'?
deleteFile();
}
Ambos os parâmetros são opcionais, o valor padrão null significa qualquer coisa.
Permission ACL
O Nette vem com uma implementação integrada de autorizador, a classe Nette\Security\Permission que fornece ao programador uma camada ACL (Access Control List) leve e flexível para gerenciar permissões e acessos. O trabalho com ela consiste em definir papéis, recursos e permissões individuais. Onde papéis e recursos permitem criar hierarquias. Para explicar, mostraremos um exemplo de aplicação web:
guest: visitante não logado, que pode ler e navegar na parte pública do site, ou seja, ler artigos, comentários e votar em enquetesregistered: usuário registrado logado, que além disso pode comentaradmin: pode gerenciar artigos, comentários e enquetes
Definimos, portanto, certos papéis (guest, registered e admin) e mencionamos recursos
(article, comment, poll), aos quais usuários com algum papel podem acessar ou realizar
certas operações (view, vote, add, edit).
Criamos uma instância da classe Permission e definimos os papéis. É possível usar a chamada herança de papéis, que
garante que, por exemplo, um usuário com o papel de administrador (admin) possa fazer também o que um visitante
comum do site faz (e, claro, mais).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' herda de 'guest'
$acl->addRole('admin', 'registered'); // e dele herda 'admin'
Agora definimos também a lista de recursos, aos quais os usuários podem acessar.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Recursos também podem usar herança, seria possível, por exemplo, especificar
$acl->addResource('perex', 'article').
E agora o mais importante. Definimos entre eles as regras que determinam quem pode fazer o quê com o quê:
// inicialmente, ninguém pode fazer nada
// que o guest possa visualizar artigos, comentários e enquetes
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// e nas enquetes, além disso, votar
$acl->allow('guest', 'poll', 'vote');
// registrado herda direitos de guest, damos a ele adicionalmente o direito de comentar
$acl->allow('registered', 'comment', 'add');
// administrador pode visualizar e editar qualquer coisa
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
E se quisermos impedir alguém de acessar um determinado recurso?
// administrador não pode editar enquetes, isso seria antidemocrático
$acl->deny('admin', 'poll', 'edit');
Agora, quando temos a lista de regras criada, podemos simplesmente fazer consultas de autorização:
// guest pode visualizar artigos?
$acl->isAllowed('guest', 'article', 'view'); // true
// guest pode editar artigos?
$acl->isAllowed('guest', 'article', 'edit'); // false
// guest pode votar em enquetes?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// guest pode comentar?
$acl->isAllowed('guest', 'comment', 'add'); // false
O mesmo se aplica ao usuário registrado, que, no entanto, também pode comentar:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
O administrador pode editar tudo, exceto as enquetes:
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
As permissões também podem ser avaliadas dinamicamente e podemos deixar a decisão para um callback próprio, ao qual todos os parâmetros são passados:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Mas como resolver, por exemplo, a situação em que apenas os nomes dos papéis e recursos não são suficientes, mas
gostaríamos de definir que, por exemplo, o papel registered pode editar o recurso article apenas se
for seu autor? Em vez de strings, usaremos objetos, o papel será um objeto Nette\Security\Role e o recurso um objeto Nette\Security\Resource. Seus métodos
getRoleId() resp. getResourceId() retornarão as strings originais:
class Registered implements Nette\Security\Role
{
public $id;
public function getRoleId(): string
{
return 'registered';
}
}
class Article implements Nette\Security\Resource
{
public $authorId;
public function getResourceId(): string
{
return 'article';
}
}
E agora criamos a regra:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // objeto Registered
$resource = $acl->getQueriedResource(); // objeto Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
E a consulta à ACL é realizada passando os objetos:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Um papel pode herdar de outro papel ou de múltiplos papéis. Mas o que acontece se um ancestral tiver a ação proibida e outro permitida? Quais serão os direitos do descendente? Isso é determinado pelo peso do papel – o último papel listado na lista de ancestrais tem o maior peso, o primeiro papel listado tem o menor. Isso é mais claro no exemplo:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// caso A: o papel admin tem menor peso que o papel guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// caso B: o papel admin tem maior peso que guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Papéis e recursos também podem ser removidos (removeRole(), removeResource()), regras também podem
ser revertidas (removeAllow(), removeDeny()). O array de todos os papéis pais diretos é retornado por
getRoleParents(), se duas entidades herdam uma da outra é retornado por roleInheritsFrom() e
resourceInheritsFrom().
Adição como serviços
Precisamos passar nossa ACL criada para a configuração como um serviço, para que o objeto $user comece a
usá-la, ou seja, para que seja possível usar no código, por exemplo, $user->isAllowed('article', 'view'). Para
esse fim, escreveremos uma fábrica para ela:
namespace App\Model;
class AuthorizatorFactory
{
public static function create(): Nette\Security\Permission
{
$acl = new Nette\Security\Permission;
$acl->addRole(/* ... */);
$acl->addResource(/* ... */);
$acl->allow(/* ... */);
return $acl;
}
}
E a adicionaremos à configuração:
services:
- App\Model\AuthorizatorFactory::create
Nos presenters, você pode então verificar as permissões, por exemplo, no método startup():
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}