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);
}
}