Verificación de permisos (Autorización)

La autorización determina si un usuario tiene permisos suficientes, por ejemplo, para acceder a un recurso determinado o para realizar alguna acción. La autorización presupone una autenticación previa exitosa, es decir, que el usuario haya iniciado sesión.

Instalación y requisitos

En los ejemplos utilizaremos un objeto de la clase Nette\Security\User, que representa al usuario actual y al que puedes acceder solicitándolo mediante inyección de dependencias. En los presentadores, basta con llamar a $user = $this->getUser().

En sitios web muy simples con administración, donde no se distinguen los permisos de los usuarios, es posible utilizar como criterio de autorización el método ya conocido isLoggedIn(). En otras palabras: tan pronto como el usuario inicia sesión, tiene todos los permisos y viceversa.

if ($user->isLoggedIn()) { // ¿ha iniciado sesión el usuario?
	deleteItem(); // entonces tiene permiso para la operación
}

Roles

El propósito de los roles es ofrecer un control de permisos más preciso y permanecer independiente del nombre de usuario. A cada usuario, justo al iniciar sesión, le asignamos uno o más roles en los que actuará. Los roles pueden ser cadenas simples como admin, member, guest, etc. Se indican como segundo parámetro del constructor SimpleIdentity, ya sea como una cadena o un array de cadenas – roles.

Como criterio de autorización ahora usaremos el método isInRole(), que indica si el usuario actúa en el rol dado:

if ($user->isInRole('admin')) { // ¿está el usuario en el rol de administrador?
	deleteItem(); // entonces tiene permiso para la operación
}

Como ya sabes, después de cerrar la sesión del usuario, no es necesario borrar su identidad. Es decir, el método getIdentity() sigue devolviendo el objeto SimpleIdentity, incluidos todos los roles otorgados. Nette Framework sigue el principio de “menos código, más seguridad”, donde escribir menos conduce a un código más seguro, por lo tanto, al verificar los roles no necesitas verificar también si el usuario ha iniciado sesión. El método isInRole() trabaja con roles efectivos, es decir, si el usuario ha iniciado sesión, se basa en los roles indicados en la identidad, si no ha iniciado sesión, tiene automáticamente el rol especial guest.

Autorizador

Además de los roles, introduciremos los conceptos de recurso y operación:

  • rol es una propiedad del usuario – p. ej., moderador, editor, visitante, usuario registrado, administrador…
  • recurso (resource) es algún elemento lógico del sitio web – artículo, página, usuario, elemento de menú, encuesta, presentador, …
  • operación (operation) es alguna actividad específica que el usuario puede o no puede hacer con el recurso – por ejemplo, borrar, editar, crear, votar, …

Un autorizador es un objeto que decide si un rol dado tiene permiso para realizar una operación determinada con un recurso específico. Es un objeto que implementa la interfaz Nette\Security\Authorizator con un ú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;
	}
}

Añadimos el autorizador a la configuración como un servicio del contenedor DI:

services:
	- MyAuthorizator

Y sigue un ejemplo de uso. Atención, esta vez llamamos al método Nette\Security\User::isAllowed(), no al autorizador, por lo que no está el primer parámetro $role. Este método llama a MyAuthorizator::isAllowed() secuencialmente para todos los roles del usuario y devuelve true si al menos uno de ellos tiene permiso.

if ($user->isAllowed('file')) { // ¿puede el usuario hacer cualquier cosa con el recurso 'file'?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // ¿puede realizar 'delete' sobre el recurso 'file'?
	deleteFile();
}

Ambos parámetros son opcionales, el valor predeterminado null significa cualquier cosa.

Permission ACL

Nette viene con una implementación incorporada de autorizador, la clase Nette\Security\Permission que proporciona al programador una capa ACL (Access Control List) ligera y flexible para gestionar permisos y accesos. Trabajar con ella consiste en definir roles, recursos y permisos individuales. Los roles y recursos permiten crear jerarquías. Para explicarlo, mostraremos un ejemplo de una aplicación web:

  • guest: visitante no autenticado, que puede leer y navegar por la parte pública del sitio web, es decir, leer artículos, comentarios y votar en encuestas
  • registered: usuario registrado y autenticado, que además puede comentar
  • admin: puede administrar artículos, comentarios y encuestas

Hemos definido ciertos roles (guest, registered y admin) y mencionado recursos (article, comment, poll), a los que los usuarios con algún rol pueden acceder o realizar ciertas operaciones (view, vote, add, edit).

Creamos una instancia de la clase Permission y definimos los roles. Se puede utilizar la llamada herencia de roles, que asegura que, por ejemplo, un usuario con el rol de administrador (admin) pueda hacer también lo que un visitante común del sitio web (y por supuesto, más).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' hereda de 'guest'
$acl->addRole('admin', 'registered'); // y de él hereda 'admin'

Ahora definimos también la lista de recursos a los que los usuarios pueden acceder.

$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');

Los recursos también pueden usar herencia, sería posible, por ejemplo, especificar $acl->addResource('perex', 'article').

Y ahora lo más importante. Definimos entre ellos las reglas que determinan quién puede hacer qué con qué:

// primero, nadie puede hacer nada

// permitamos que guest pueda ver artículos, comentarios y encuestas
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// y en las encuestas, además, votar
$acl->allow('guest', 'poll', 'vote');

// registrado hereda los derechos de guest, le damos además el derecho a comentar
$acl->allow('registered', 'comment', 'add');

// el administrador puede ver y editar cualquier cosa
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

¿Qué pasa si queremos impedir a alguien el acceso a un recurso determinado?

// el administrador no puede editar encuestas, eso sería antidemocrático
$acl->deny('admin', 'poll', 'edit');

Ahora que hemos creado la lista de reglas, podemos simplemente hacer consultas de autorización:

// ¿puede guest ver artículos?
$acl->isAllowed('guest', 'article', 'view'); // true

// ¿puede guest editar artículos?
$acl->isAllowed('guest', 'article', 'edit'); // false

// ¿puede guest votar en encuestas?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// ¿puede guest comentar?
$acl->isAllowed('guest', 'comment', 'add'); // false

Lo mismo se aplica al usuario registrado, pero este también puede comentar:

$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false

El administrador puede editar todo, excepto las encuestas:

$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true

Los permisos también pueden evaluarse dinámicamente y podemos dejar la decisión a nuestro propio callback, al que se le pasarán todos los parámetros:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	return /* ... */;
};

$acl->allow('registered', 'comment', null, $assertion);

Pero, ¿cómo resolver, por ejemplo, la situación en la que no basta solo con los nombres de roles y recursos, sino que quisiéramos definir que, por ejemplo, el rol registered puede editar el recurso article solo si es su autor? En lugar de cadenas, usaremos objetos, el rol será un objeto Nette\Security\Role y el recurso un objeto Nette\Security\Resource. Sus métodos getRoleId() resp. getResourceId() devolverán las cadenas originales:

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

Y ahora creamos la regla:

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

Y la consulta a la ACL se realiza pasando los objetos:

$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');

Un rol puede heredar de otro rol o de varios roles. Pero, ¿qué sucede si un ancestro tiene la acción prohibida y otro permitida? ¿Cuáles serán los derechos del descendiente? Se determina según el peso del rol: el último rol mencionado en la lista de ancestros tiene el mayor peso, el primer rol mencionado el menor. Es más claro con un ejemplo:

$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');

$acl->addResource('backend');

$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');

// caso A: el rol admin tiene menos peso que el rol guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// caso B: el rol admin tiene más peso que guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Los roles y recursos también se pueden eliminar (removeRole(), removeResource()), también se pueden revertir las reglas (removeAllow(), removeDeny()). El array de todos los roles padres directos lo devuelve getRoleParents(), si dos entidades heredan una de otra lo devuelven roleInheritsFrom() y resourceInheritsFrom().

Añadir como servicios

Necesitamos pasar nuestra ACL creada a la configuración como un servicio, para que el objeto $user comience a usarla, es decir, para que sea posible usar en el código, por ejemplo, $user->isAllowed('article', 'view'). Para ello, escribiremos una fábrica para ella:

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

Y la añadimos a la configuración:

services:
	- App\Model\AuthorizatorFactory::create

En los presentadores, puedes verificar los permisos, por ejemplo, en el método startup():

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
versión: 4.0