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.

Instalação e requisitos

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 enquetes
  • registered: usuário registrado logado, que além disso pode comentar
  • admin: 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);
	}
}
versão: 4.0