Control de acceso (Autorización)

La autorización comprueba si un usuario tiene permisos suficientes, por ejemplo para acceder a un recurso concreto o para realizar una acción. La autorización presupone una autenticación previa correcta, es decir, que el usuario tiene la sesión iniciada.

Instalación y requisitos

En los ejemplos usaremos un objeto de la clase Nette\Security\User, que representa al usuario actual y que obtiene haciendo que se lo pasen mediante dependency injection. En los presenters basta con llamar a $user = $this->getUser().

En webs muy sencillas con administración donde no se diferencian los permisos de los usuarios, se puede usar como criterio de autorización el ya conocido método isLoggedIn(). Dicho de otro modo: en cuanto un usuario inicia sesión, tiene todos los permisos, y al revés.

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

Roles

El objetivo de los roles es ofrecer un control más preciso de los permisos y seguir siendo independientes del nombre de usuario. En cuanto un usuario inicia sesión, se le asignan uno o varios roles con los que actuará. Los roles pueden ser cadenas sencillas, por ejemplo admin, member, guest, etc. Se indican como segundo argumento del constructor de SimpleIdentity, ya sea como cadena o como array de cadenas, los roles.

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

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

Como ya sabe, cerrar la sesión del usuario no tiene por qué borrar su identidad. Así que el método getIdentity() sigue devolviendo el objeto SimpleIdentity, incluidos todos los roles concedidos. Nette Framework defiende el principio “menos código, más seguridad”, donde escribir menos lleva a un código más seguro. Por eso, al comprobar los roles no necesita verificar si el usuario tiene la sesión iniciada. El método isInRole() trabaja con los roles efectivos: si el usuario tiene la sesión iniciada, se basa en los roles indicados en la identidad; si no la tiene, automáticamente tiene el rol especial guest (o los roles de la identidad de invitado, si se proporciona alguna).

Autorizador

Además de los roles, introduciremos los términos recurso y operación:

  • rol es una propiedad del usuario: p. ej. moderador, editor, visitante, usuario registrado, administrador…
  • recurso es una unidad lógica de la web: artículo, página, usuario, elemento de menú, encuesta, presenter, …
  • operación es una actividad concreta que el usuario puede o no puede realizar con el recurso: p. ej. ver, editar, borrar, votar, …

Un autorizador es un objeto que decide si el rol dado tiene permiso para realizar cierta operación con un recurso concreto. 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;
	}
}

El autorizador lo añadimos a la configuración como servicio del contenedor DI:

services:
	- MyAuthorizator

Y aquí tiene un ejemplo de uso. Fíjese en que esta vez llamamos al método Nette\Security\User::isAllowed(), no al del autorizador, así que falta el primer parámetro $role. Este método llama a MyAuthorizator::isAllowed() sucesivamente 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 algo con el recurso 'file'?
	useFile();
}

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

Los dos argumentos son opcionales; el valor predeterminado null significa cualquier cosa.

Permission ACL

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

  • guest: un visitante no registrado que puede leer y navegar por la parte pública de la web, es decir, leer artículos y comentarios y votar en las encuestas.
  • registered: un usuario registrado con la sesión iniciada, que además puede publicar comentarios.
  • admin: puede gestionar los artículos, los comentarios y las encuestas.

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

Creamos una instancia de la clase Permission y definimos los roles. Es posible usar la llamada herencia de roles, que asegura que, p. ej., un usuario con el rol admin pueda hacer también lo que puede hacer un visitante corriente de la 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'); // 'admin' hereda de 'registered'

Ahora definimos 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 la herencia; por ejemplo, sería posible escribir $acl->addResource('perex', 'article').

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

// al principio nadie puede hacer nada

// dejamos que guest vea los artículos, los comentarios y las encuestas
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// y que también vote en las encuestas
$acl->allow('guest', 'poll', 'vote');

// registered hereda los permisos de guest, démosle 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']);

¿Y si queremos impedir que alguien acceda a cierto recurso?

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

Ahora que hemos creado el conjunto de reglas, podemos plantear sin más las consultas de autorización:

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

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

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

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

Lo mismo vale para un usuario registrado, pero este además puede comentar:

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

El administrador puede editarlo todo, salvo las encuestas:

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

Los permisos también se pueden evaluar dinámicamente, y podemos dejar la decisión a un callback propio al que se pasan 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 una situación en la que los nombres de los roles y los recursos no bastan, sino que querríamos definir que, por ejemplo, el rol registered pueda editar el recurso article solo si es su autor? Usaremos objetos en lugar de cadenas; el rol será un objeto Nette\Security\Role y el recurso un objeto Nette\Security\Resource. Sus métodos getRoleId() y getResourceId(), respectivamente, 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 al ACL se realiza pasando los objetos:

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

Un rol puede heredar de un rol o de varios roles. ¿Pero qué pasa si un antecesor tiene la acción denegada y el otro permitida? ¿Cuáles serán los derechos del descendiente? Eso lo determina el peso del rol: el último rol indicado en la lista de antecesores tiene el mayor peso, el primero el menor. Con el ejemplo se ve mejor:

$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 el rol guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Los roles y los recursos también se pueden eliminar (removeRole(), removeResource()), y las reglas también se pueden revertir (removeAllow(), removeDeny()). El array de todos los roles padre directos lo devuelve getRoleParents(). Si dos entidades heredan una de otra lo devuelven roleInheritsFrom() y resourceInheritsFrom(). La existencia de un rol o un recurso la comprueban hasRole() y hasResource(), las listas de todos los roles y recursos registrados las devuelven getRoles() y getResources(), y todo se puede borrar con removeAllRoles() y removeAllResources().

Añadirlo como servicio

El ACL que hemos creado hay que pasarlo a la configuración como servicio para que el objeto $user empiece a usarlo, es decir, para que en el código se pueda usar $user->isAllowed('article', 'view'). Para eso le escribiremos una factory:

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 presenters puede después 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.x