Controle de acesso (Autorização)
A autorização determina se um usuário tem privilégios suficientes, por exemplo, para acessar um recurso específico ou para executar uma ação. A autorização pressupõe uma autenticação prévia bem sucedida, ou seja, que o usuário está logado.
Nos exemplos, usaremos um objeto da classe Nette\Security\User, que representa o usuário atual e que
você obtém ao passá-lo usando a injeção de dependência. Nos
apresentadores, basta ligar para $user = $this->getUser()
.
Para sites muito simples com administração, onde os direitos do usuário não são diferenciados, é possível utilizar
o método já conhecido como critério de autorização isLoggedIn()
. Em outras palavras: uma vez que um usuário
esteja logado, ele tem permissões para todas as ações e vice-versa.
if ($user->isLoggedIn()) { // o usuário está logado?
deleteItem(); // se estiver, ele pode excluir um item
}
Papéis
O objetivo das funções é oferecer uma gestão de permissão mais precisa e permanecer independente do nome do usuário.
Assim que o usuário faz o login, lhe é atribuído um ou mais papéis. Os papéis em si podem ser simples cordas, por exemplo,
admin
, member
, guest
, etc. Eles são especificados no segundo argumento do construtor
SimpleIdentity
, seja como uma string ou como uma array.
Como critério de autorização, utilizaremos agora o método isInRole()
, que verifica se o usuário está na
função em questão:
if ($user->isInRole('admin')) { // a função administrativa é atribuída ao usuário?
deleteItem(); // se for o caso, ele pode apagar um item
}
Como você já sabe, o log out do usuário não apaga sua identidade. Assim, o método getIdentity()
ainda
devolve o objeto SimpleIdentity
, incluindo todas as funções concedidas. O Nette Framework adere ao princípio de
„menos código, mais segurança“, portanto, quando você está verificando funções, não precisa verificar se o usuário
também está logado. O método isInRole()
funciona com ** funções efetivas**, ou seja, se o usuário estiver
logado, funções atribuídas à identidade são usadas, se ele não estiver logado, uma função especial automática
guest
é usada em seu lugar.
Autorizador
Além das funções, introduziremos os termos recurso e operação:
- role é um atributo do usuário – por exemplo, moderador, editor, visitante, usuário registrado, administrador, …
- **resource*** é uma unidade lógica da aplicação – artigo, página, usuário, item de menu, enquete, apresentador, …
- **operação*** é uma atividade específica, que o usuário pode ou não fazer com recurso – ver, editar, apagar, votar, …
Um autorizador é um objeto que decide se um determinado role tem permissão para realizar uma determinada
operação com recurso específico. É um objeto que implementa a interface Nette\Security\Authorizator com apenas um 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;
}
}
Acrescentamos o autorizador à configuração como um serviço do recipiente DI:
services:
- MyAuthorizator
E o seguinte é um exemplo de uso. Note que desta vez chamamos o método Nette\Security\User::isAllowed()
, e
não o do autorizador, portanto não há o primeiro parâmetro $role
. Este método chama
MyAuthorizator::isAllowed()
sequencialmente para todas as funções do usuário e retorna verdadeiro se pelo menos
uma delas tiver permissão.
if ($user->isAllowed('file')) { // o usuário tem permissão para fazer tudo com o recurso 'arquivo'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // o usuário tem permissão para excluir um 'arquivo' de recurso?
deleteFile();
}
Ambos os argumentos são opcionais e seu valor padrão significa todas as coisas.
Permissão ACL
Nette vem com uma implementação integrada do autorizador, a classe Nette\Security\Permission, que oferece uma camada leve e flexível de ACL (Access Control List) para permissão e controle de acesso. Quando trabalhamos com esta classe, definimos as funções, os recursos e as permissões individuais. E as funções e recursos podem formar hierarquias. Para explicar, vamos mostrar um exemplo de uma aplicação web:
guest
: visitante que não está logado, autorizado a ler e navegar na parte pública da web, ou seja, ler artigos, comentar e votar em enquetesregistered
: usuário logado, que pode, além dos comentários do postadmin
: pode gerenciar artigos, comentários e enquetes
Assim, definimos certas funções (guest
, registered
e admin
) e mencionamos recursos
(article
, comments
, poll
), aos quais os usuários podem acessar ou tomar medidas
(view
, vote
, add
, edit
).
Criamos uma instância da classe Permission e definimos **roles***. É possível utilizar a herança de funções, o que
garante que, por exemplo, um usuário com uma função admin
possa fazer o que um visitante comum do site pode fazer
(e, claro, mais).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registrado' herda de 'convidado';
$acl->addRole('admin', 'registered'); // e 'admin' herda de 'registrado'.
Vamos agora definir uma lista de **recursos*** que os usuários podem acessar:
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Os recursos também podem utilizar a herança, por exemplo, podemos acrescentar
$acl->addResource('perex', 'article')
.
E agora a coisa mais importante. Vamos definir entre eles regras determinando quem pode fazer o quê:
// tudo é negado agora
// deixe o convidado ver artigos, comentários e enquetes
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// e também votar nas urnas
$acl->allow('guest', 'poll', 'vote');
// o registrado herda as permissões do guesta, também o deixaremos comentar
$acl->allow('registered', 'comment', 'add');
// o administrador pode visualizar e editar qualquer coisa
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
E se quisermos prevenir alguém de acessar um recurso?
// o administrador não pode editar pesquisas, o que seria antidemocrático.
$acl->deny('admin', 'poll', 'edit');
Agora, quando tivermos criado o conjunto de regras, podemos simplesmente solicitar as consultas de autorização:
// os convidados podem ver os artigos?
$acl->isAllowed('guest', 'article', 'view'); // true
// o convidado pode editar um artigo?
$acl->isAllowed('guest', 'article', 'edit'); // false
// os convidados podem votar nas urnas?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// pode o convidado acrescentar comentários?
$acl->isAllowed('guest', 'comment', 'add'); // false
O mesmo se aplica a um usuário registrado, mas ele 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 pesquisas:
$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 nosso próprio retorno de chamada, 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 uma situação em que os nomes dos papéis e recursos não são suficientes, ou seja, gostaríamos de definir
que, por exemplo, um papel registered
pode editar um recurso article
somente se for seu autor? Usaremos
objetos em vez de cordas, o papel será o objeto Nette\Security\Role e a fonte Nette\Security\Resource. Seus métodos
getRoleId()
resp. getResourceId()
devolverão as cordas 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 vamos criar uma regra:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // object Registered
$resource = $acl->getQueriedResource(); // object Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
O ACL é consultado através da passagem de objetos:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Um papel pode herdar um ou mais papéis. Mas o que acontece, se um antepassado tem determinada ação permitida e o outro a negou? Então o peso do papel entra em jogo – o último papel da série de papéis a herdar tem o maior peso, primeiro o mais baixo:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// exemplo A: o papel administrador tem menos peso que o papel convidado
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// exemplo B: o papel administrativo tem maior peso do que o papel convidado
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Os papéis e recursos também podem ser retirados (removeRole()
, removeResource()
), as regras também
podem ser revertidas (removeAllow()
, removeDeny()
). O conjunto de todos os papéis dos pais diretos
retorna getRoleParents()
. Se duas entidades herdam uma da outra retorna roleInheritsFrom()
e
resourceInheritsFrom()
.
Adicionar como um serviço
Precisamos adicionar a ACL criada por nós à configuração como um serviço para que ela possa ser usada pelo objeto
$user
, ou seja, para que possamos usar em código, por exemplo $user->isAllowed('article', 'view')
.
Para este 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 vamos acrescentá-la à configuração:
services:
- App\Model\AuthorizatorFactory::create
Nos apresentadores, você pode então verificar as permissões no método startup()
, por exemplo:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}