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.

Instalação e requisitos

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 enquetes
  • registered: usuário logado, que pode, além dos comentários do post
  • admin: 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);
	}
}
versão: 4.0