Control de acceso (autorización)

La autorización determina si un usuario tiene privilegios suficientes, por ejemplo, para acceder a un recurso específico o para realizar una acción. La autorización presupone una autenticación previa correcta, es decir, que el usuario ha 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 que se obtiene pasándolo mediante inyección de dependencia. En los presentadores basta con llamar a $user = $this->getUser().

Para sitios web muy sencillos con administración, en los que no se distinguen derechos de usuario, es posible utilizar el método ya conocido como criterio de autorización isLoggedIn(). En otras palabras: una vez que un usuario ha iniciado sesión, tiene permisos para todas las acciones y viceversa.

if ($user->isLoggedIn()) { // is user logged in?
	deleteItem(); // if so, he may delete an item
}

Roles

El propósito de los roles es ofrecer una gestión de permisos más precisa y permanecer independiente del nombre de usuario. En cuanto un usuario se conecta, se le asignan uno o varios roles. Los roles pueden ser simples cadenas, por ejemplo, admin, member, guest, etc. Se especifican en el segundo argumento del constructor SimpleIdentity, ya sea como cadena o como matriz.

Como criterio de autorización, utilizaremos ahora el método isInRole(), que comprueba si el usuario está en el rol dado:

if ($user->isInRole('admin')) { // is the admin role assigned to the user?
	deleteItem(); // if so, he may delete an item
}

Como ya sabes, cerrar la sesión del usuario no borra su identidad. Por lo tanto, el método getIdentity() sigue devolviendo el objeto SimpleIdentity, incluyendo todos los roles concedidos. Nette Framework se adhiere al principio de “menos código, más seguridad”, por lo que cuando compruebe los roles, no tiene que comprobar también si el usuario ha iniciado sesión. El método isInRole() funciona con roles efectivos, es decir, si el usuario está conectado, se utilizan los roles asignados a la identidad, si no está conectado, se utiliza en su lugar un rol especial automático guest.

Autorizador

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

  • rol es un atributo del usuario – por ejemplo moderador, editor, visitante, usuario registrado, administrador, …
  • recurso es una unidad lógica de la aplicación – artículo, página, usuario, elemento de menú, encuesta, presentador, …
  • operación es una actividad específica, que el usuario puede o no hacer con recurso – ver, editar, borrar, votar, …

Un autorizador es un objeto que decide si un determinado rol tiene permiso para realizar una determinada operación 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 el siguiente es un ejemplo de uso. Nótese que esta vez llamamos al método Nette\Security\User::isAllowed(), no al del autorizador, por lo que no hay primer parámetro $role. Este método llama a MyAuthorizator::isAllowed() secuencialmente para todos los roles de usuario y devuelve true si al menos uno de ellos tiene permiso.

if ($user->isAllowed('file')) { // is user allowed to do everything with resource 'file'?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // is user allowed to delete a resource 'file'?
	deleteFile();
}

Ambos argumentos son opcionales y su valor por defecto significa todo.

ACL de permisos

Nette viene con una implementación incorporada del autorizador, la clase Nette\Security\Permission, que ofrece una capa ACL (Lista de Control de Acceso) ligera y flexible para el control de permisos y accesos. Cuando trabajamos con esta clase, definimos roles, recursos y permisos individuales. Y los roles y recursos pueden formar jerarquías. Para explicarlo, mostraremos un ejemplo de una aplicación web:

  • guest: visitante que no ha iniciado sesión, con permiso para leer y navegar por la parte pública de la web, es decir, leer artículos, comentar y votar en encuestas
  • registered: usuario conectado, que además puede publicar comentarios
  • admin: puede gestionar artículos, comentarios y encuestas

Así pues, hemos definido determinados roles (guest, registered y admin) y mencionado recursos (article, comments, poll), a los que los usuarios pueden acceder o sobre los que pueden realizar acciones (view, vote, add, edit).

Creamos una instancia de la clase Permission y definimos roles. Es posible utilizar la herencia de roles, lo que garantiza que, por ejemplo, un usuario con el rol admin pueda hacer lo mismo que un visitante normal del sitio web (y por supuesto más).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' inherits from 'guest'
$acl->addRole('admin', 'registered'); // and 'admin' inherits from 'registered'

Ahora definiremos una lista de recursos a los que los usuarios pueden acceder:

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

Los recursos también pueden utilizar la herencia, por ejemplo, podemos añadir $acl->addResource('perex', 'article').

Y ahora lo más importante. Definiremos entre ellos reglas que determinen quién puede hacer qué:

// everything is denied now

// let the guest view articles, comments and polls
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// and also vote in polls
$acl->allow('guest', 'poll', 'vote');

// the registered inherits the permissions from guesta, we will also let him to comment
$acl->allow('registered', 'comment', 'add');

// the administrator can view and edit anything
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

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

// administrator cannot edit polls, that would be undemocractic.
$acl->deny('admin', 'poll', 'edit');

Ahora, cuando hayamos creado el conjunto de reglas, podemos simplemente hacer las consultas de autorización:

// can guest view articles?
$acl->isAllowed('guest', 'article', 'view'); // true

// can guest edit an article?
$acl->isAllowed('guest', 'article', 'edit'); // false

// can guest vote in polls?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// may guest add comments?
$acl->isAllowed('guest', 'comment', 'add'); // false

Lo mismo se aplica a un usuario registrado, pero también puede hacer comentarios:

$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 ser evaluados dinámicamente y podemos dejar la decisión a nuestro propio callback, al que se le 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 son suficientes, es decir, nos gustaría definir que, por ejemplo, un rol registered puede editar un recurso article sólo si es su autor? Utilizaremos objetos en lugar de cadenas, el rol será el objeto Nette\Security\Role y el recurso 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 vamos a crear una regla:

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

La ACL se consulta pasando objetos:

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

Un rol puede heredar de uno o más roles. Pero, ¿qué ocurre si un ancestro tiene una acción permitida y el otro la tiene denegada? Entonces entra en juego el peso del rol – el último rol en el conjunto de roles a heredar tiene el mayor peso, el primero el menor:

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

$acl->addResource('backend');

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

// example A: role admin has lower weight than role guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// example B: role admin has greater weight than role guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

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

Añadir como servicio

Necesitamos añadir el ACL creado por nosotros a la configuración como un servicio para que pueda ser utilizado por el objeto $user, es decir, para que podamos utilizarlo en código por ejemplo $user->isAllowed('article', 'view'). Para ello escribiremos una factoría para ello:

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ñadiremos a la configuración:

services:
	- App\Model\AuthorizatorFactory::create

En presentadores, a continuación, puede verificar los permisos en el método startup(), por ejemplo:

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