GraphQL — Acceso a datos basado en roles de usuario con express-graphql

Cómo crear y obtener roles de usuario para acceso a datos, en la definición de objetos tipos y sus campos

Dailos Rafael Díaz Lara
CanariasJS

--

En una entrada anterior expuse cómo podemos obtener la cabecera HTTP Authentication con el objetivo de obtener el token del usuario y de este modo, saber si el usuario puede trabajar con nuestra API GraphQL.

No obstante, en la mayoría de las veces, que el usuario esté autorizado a acceder a la API no significa que dicho usuario pueda acceder a todos los datos gestionados por GraphQL. En este punto es donde el uso de roles empieza a cobrar sentido.

Por defecto, GraphQL no gestiona roles en sus objetos tipo ni en sus definiciones de campos. Sin embargo, podemos definir nuestros propios atributos y usarlos para establecer los roles que determinarán los niveles mínimos de acceso, para el consumo de los datos gestionados por dichos objetos tipo.

A lo largo de este documento voy a analizar una forma de realizar esta definición de campos y luego, cómo obtener dicha información desde las funciones resolve.

Agradecimientos

Antes de continuar con este post, me gustaría dar las gracias a Rob Crowley por su magnífica presentación con la cual, encontré una vía para continuar investigando e implementar el código que se va a mostrar a continuación.

Código base

Como ejemplo, voy a usar el siguiente código donde he definido una posible estructura que se encargaría de gestionar los datos relacionados con usuarios.

Por favor, tengamos en cuenta que PhoneNumberType, EmailAddressType y TimestampType son objetos personalizados e independientes, que se han importado para ser usados en este script.

En este punto, voy a definir los siguientes requisitos para el funcionamiento de mi API:

  • El rol de acceso vendrá definido por una cadena de caracteres con el siguiente formato: <role_name>.<access_level>.
  • <role_name> podrá tener los siguientes valores: admin, user y guest.
  • <access_level> podrá tener los siguientes valores: read, write y all.
  • Todo los datos de usuarios podrán ser accedidos por aquellos usuarios que tengan el rol de admin y el nivel de acceso all les permitirá realizar cualquier operación sobre los datos.
  • Los usuarios con rol user podrán realizar peticiones de consulta, en modo lectura (nivel de acceso read), pero únicamente obtendrán los campos name, surname, phoneNumber and emailAddress.

Me gustaría resaltar que esta definición de roles se ha hecho únicamente con fines didácticos. Obviamente tienes que analizar los requisitos tanto de tu API como de tu sistema, para definir la mejor solución para tu proyecto.

Hasta aquí todo bien así que vamos a ver cómo podemos realizar la definición de los roles.

Definiendo roles en el ObjectType y en sus campos

Para implementar los roles necesarios, voy a definir un nuevo atributo no estándar de GraphQL, al que llamaré allowedRoles.

A este atributo le asignaré un array cuyo contenido será el conjunto de definiciones de roles de acceso.

De este modo, la definición del objeto tipo, incluyendo este nuevo atributo, será el siguiente:

Extrayendo los roles de la definición del objeto typo

Una vez que hemos definido nuestro roles de acceso, vamos a analizar cómo podemos acceder a ellos.

Como ya sabemos, cada función resolve puede recibir hasta cuatro parámetros (parentValues, args, context and info). En este documento nos vamos a centrar en el último, el parámetro info (también conocido como ast dependiendo de la documentación que estemos usando para aprender GraphQL).

Para exponer esto, voy a definir una query muy básica que se aplicará sobre el objeto tipo UserType.

Cuando la función resolve se ejecuta, el parámetro info contiene toda la información relativa al nodo AST (query o mutation).

Este objeto contiene varios campos de los cuales, los más importantes para el desarrollo de este post son: returnType y schema.

  • El campo returnType contiene el nombre del objeto tipo que devolverá la función resolve. En este caso, el valor de este campo será User (echa un vistazo al atributo name en la definición del objeto tipo UserType).
  • El campo schema es un poco más complejo ya que es un objeto que contiene toda la información y componentes del nodo AST.

En el objetoschema vamos a centrarnos en su campo _typeMap.

Este campo es otro objeto que contiene todos los objetos tipo y los tipos de datos empleados en la petición (query o mutation).

Uno de estos objetos será el ObjectType que devolverá la petición query después de que la función resolve haya concluido. Para localizar dicho objeto, emplearemos el valor almacenado en el campo returnType.

Después de esto, la sintaxis para acceder a dichos atributos, empleando dot-notation, es la siguiente:

Una vez más, el objeto devuelto por la línea info.schema._typeMap[info.returnType] contendrá dos campos realmente importante para nosotros: _typeConfig y _fields.

  • El campo _typeConfig contiene información (en formato JSON) del objeto tipo seleccionado (UserType en este caso).
  • El atributo _fields contiene un JSON con la definición completa de los campos de dicho objeto tipo.

De esta manera, del campo _typeConfig podremos obtener la siguiente información:

Mientras que el atributo _fields nos proporcionará los siguientes datos:

Como se puede apreciar, hemos obtenido el campo personalizado allowedRoles que insertamos para definir nuestros roles en ambas partes, la definición del objeto tipo así como la de sus campos.

De ahora en adelante, sólo necesitaremos los datos de este campo para procesar el resultado de la función resolve y devolver de este modo, únicamente la información adecuada al role del usuario que ha ejecutado la consulta.

Modularizando la consulta de roles

Para asegurarnos de que el proceso de obtención de los roles definidos en nuestra API está disponible para todas las funciones resolve de la misma, podemos implementar, en un archivo independiente, un código genérico similar a este:

Una vez hecho esto, en el código del archivo server.js (o donde hayamos definido nuestro endpoint de GraphQL), podemos importar este nuevo código e inyectarlo en el context de GraphQL.

Finalmente, sólo necesitamos usar la función context.parseAst en cada función resolve donde deseemos analizar los roles del usuario.

Conclusión

Debido a que la función resolve recibe el objeto AST y que podemos navegar a través de todas sus propiedades, sólo necesitamos jugar un poco con ellas para obtener el comportamiento que deseamos.

Además, como hemos podido apreciar, usando esta técnica seremos capaces de definir roles de acceso a datos o cualquier otra funcionalidad que nuestro proyecto requiera, pudiendo acceder a ellas en las funciones resolve.

Espero que este documento te haya sido útil. Si es así, por favor, dale un voto positivo y compártelo en tus redes sociales.

Si lo deseas, puedes encontrarme en Twitter y en Linkedin.

Gracias y saludos.

--

--

Dailos Rafael Díaz Lara
CanariasJS

Multiplatform Software Developer seeking for new challenges