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.
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 encuestasregistered
: usuario registrado y autenticado, que además puede comentaradmin
: 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);
}
}