Backend GraphQL instantáneo con seguridad detallada utilizando FaunaDB
GraphQL se está volviendo popular y los desarrolladores buscan constantemente marcos que faciliten la configuración de una API GraphQL rápida, segura y escalable. En este artículo, aprenderemos cómo crear una API GraphQL rápida y escalable con autenticación y control detallado de acceso a datos (autorización). Como ejemplo, crearemos una API con funcionalidad de registro e inicio de sesión. La API tratará sobre usuarios y archivos confidenciales, por lo que definiremos reglas de autorización avanzadas que especifican si un usuario que ha iniciado sesión puede acceder a ciertos archivos.
Al utilizar GraphQL nativo y la capa de seguridad de FaunaDB, recibimos todas las herramientas necesarias para configurar dicha API en minutos. FaunaDB tiene un nivel gratuito para que puedas seguirlo fácilmente creando una cuenta en https://dashboard.fauna.com/. Dado que FaunaDB proporciona automáticamente los índices necesarios y traduce cada consulta GraphQL en una consulta de FaunaDB, su API también es lo más rápido posible ( ¡sin problemas n+1! ).
Configurar la API es simple: ingrese un esquema y estaremos listos para comenzar. ¡Entonces empecemos!
El caso de uso: usuarios y archivos confidenciales
Necesitamos un caso de uso de ejemplo que demuestre cómo la seguridad y las características de la API GraphQL pueden funcionar juntas. En este ejemplo, hay usuarios y archivos. Todos los usuarios pueden acceder a algunos archivos y otros solo los administradores pueden acceder. El siguiente esquema GraphQL definirá nuestro modelo:
type User { username: String! @unique role: UserRole!}enum UserRole { MANAGER EMPLOYEE}type File { content: String! confidential: Boolean!}input CreateUserInput { username: String! password: String! role: UserRole!}input LoginUserInput { username: String! password: String!}type Query { allFiles: [File!]!}type Mutation { createUser(input: CreateUserInput): User! @resolver(name: "create_user") loginUser(input: LoginUserInput): String! @resolver(name: "login_user")}
Al observar el esquema, es posible que observe que los campos createUser
y loginUser
Mutación se han anotado con una directiva especial denominada @resolver
. Esta es una directiva proporcionada por la API GraphQL de FaunaDB, que nos permite definir un comportamiento personalizado para un campo de consulta o mutación determinada. Dado que usaremos los mecanismos de autenticación integrados de FaunaDB, necesitaremos definir esta lógica en FaunaDB después de importar el esquema.
Importando el esquema
Primero, importamos el esquema de ejemplo a una nueva base de datos. Inicie sesión en FaunaDB Cloud Console con sus credenciales. Si aún no tienes una cuenta, puedes registrarte gratis en unos segundos.
Una vez que haya iniciado sesión, haga clic en el botón “Nueva base de datos” en la página de inicio:
Elija un nombre para la nueva base de datos y haga clic en el botón “Guardar”:
A continuación, importaremos el esquema GraphQL enumerado anteriormente a la base de datos que acabamos de crear. Para hacerlo, cree un archivo llamado schema.gql
que contiene la definición del esquema. Luego, seleccione la pestaña GRAPHQL en la barra lateral izquierda, haga clic en el botón “Importar esquema” y seleccione el archivo recién creado:
El proceso de importación crea todos los elementos de base de datos necesarios, incluidas colecciones e índices, para realizar copias de seguridad de todos los tipos definidos en el esquema. Crea automáticamente todo lo que su API GraphQL necesita para ejecutarse de manera eficiente.
Ahora tiene una API GraphQL completamente funcional que puede comenzar a probar en el área de juegos GraphQL. Pero aún no tenemos datos. Más específicamente, nos gustaría crear algunos usuarios para comenzar a probar nuestra API GraphQL. Sin embargo, dado que los usuarios formarán parte de nuestra autenticación, son especiales: tienen credenciales y pueden ser suplantados. ¡Veamos cómo podemos crear algunos usuarios con credenciales seguras!
Resolvedores personalizados para autenticación
Recuerde los campos de mutación createUser
y loginUser
que han sido anotados con una directiva especial denominada @resolver
. createUser
es exactamente lo que necesitamos para comenzar a crear usuarios; sin embargo, el esquema no define realmente cómo se debe crear un usuario; en cambio, fue etiquetado con una @resolver
etiqueta.
Al etiquetar una mutación específica con un solucionador personalizado como el que @resolver(name: "create_user")
informamos a FaunaDB que esta mutación aún no está implementada pero que será implementada mediante una función definida por el usuario (UDF). Dado que nuestro esquema GraphQL no sabe cómo expresar esto, el proceso de importación solo creará una plantilla de función que aún tendremos que completar.
Una UDF es una función personalizada de FaunaDB, similar a un procedimiento almacenado, que permite a los usuarios definir una operación personalizada en el lenguaje de consulta de Fauna (FQL). Luego, esta función se utiliza como resolución del campo anotado.
Necesitaremos un solucionador personalizado ya que aprovecharemos las capacidades de autenticación integradas que no se pueden expresar en GraphQL estándar. FaunaDB le permite establecer una contraseña en cualquier entidad de base de datos. Luego, esta contraseña se puede usar para hacerse pasar por esta entidad de base de datos con la Login
función que devuelve un token con ciertos permisos. Los permisos que tiene este token dependen de las reglas de acceso que escribiremos.
Sigamos implementando la UDF para el createUser
solucionador de campos para que podamos crear algunos usuarios de prueba. Primero, seleccione la pestaña Shell en la barra lateral izquierda:
Como se explicó anteriormente, ya se creó una plantilla UDF durante el proceso de importación. Cuando se llama, esta plantilla UDF imprime un mensaje de error que indica que debe actualizarse con una implementación adecuada. Para actualizarlo con el comportamiento previsto, utilizaremos la función Actualizar de FQL.
Entonces, copiemos la siguiente consulta FQL en el shell basado en web y hagamos clic en el botón "Ejecutar consulta":
Update(Function("create_user"), { "body": Query( Lambda(["input"], Create(Collection("User"), { data: { username: Select("username", Var("input")), role: Select("role", Var("input")), }, credentials: { password: Select("password", Var("input")) } }) ) )});
Su pantalla debería verse similar a:
La create_user
UDF será la encargada de crear correctamente un documento de Usuario junto con un valor de contraseña. La contraseña se almacena en el documento dentro de un objeto especial llamado credenciales que está cifrado y no puede recuperarse mediante ninguna función FQL. Como resultado, la contraseña se guarda de forma segura en la base de datos, lo que hace imposible leerla desde las API FQL o GraphQL. La contraseña se utilizará más adelante para autenticar a un usuario a través de una función FQL dedicada denominada Login
, como se explica a continuación.
Ahora, agreguemos la implementación adecuada para la UDF que realiza una copia de seguridad del loginUser
solucionador de campos mediante la siguiente consulta FQL:
Update(Function("login_user"), { "body": Query( Lambda(["input"], Select( "secret", Login( Match(Index("unique_User_username"), Select("username", Var("input"))), { password: Select("password", Var("input")) } ) ) ) )});
Copie la consulta enumerada arriba, péguela en el panel de comandos del shell y haga clic en el botón "Ejecutar consulta":
La login_user
UDF intentará autenticar a un usuario con las credenciales de nombre de usuario y contraseña proporcionadas. Como se mencionó anteriormente, lo hace a través de la Login
función. La Login
función verifica que la contraseña proporcionada coincida con la almacenada junto con el documento de usuario que se está autenticando. Tenga en cuenta que la contraseña almacenada en la base de datos no se muestra en ningún momento durante el proceso de inicio de sesión. Finalmente, en caso de que las credenciales sean válidas, la login_user
UDF devuelve un token de autorización llamado secreto que puede usarse en solicitudes posteriores para validar la identidad del Usuario.
Una vez implementados los solucionadores, continuaremos creando algunos datos de muestra. Esto nos permitirá probar nuestro caso de uso y nos ayudará a comprender mejor cómo se definen las reglas de acceso más adelante.
Creando datos de muestra
Primero, vamos a crear un usuario administrador. Seleccione la pestaña GraphQL de la barra lateral izquierda, copie la siguiente mutación en GraphQL Playground y haga clic en el botón "Reproducir":
mutation CreateManagerUser { createUser(input: { username: "bill.lumbergh" password: "123456" role: MANAGER }) { username role }}
Tu pantalla debería verse así:
A continuación, creemos un usuario empleado ejecutando la siguiente mutación a través del editor GraphQL Playground:
mutation CreateEmployeeUser { createUser(input: { username: "peter.gibbons" password: "abcdef" role: EMPLOYEE }) { username role }}
Deberías ver la siguiente respuesta:
Ahora, creemos un archivo confidencial ejecutando la siguiente mutación:
mutation CreateConfidentialFile { createFile(data: { content: "This is a confidential file!" confidential: true }) { content confidential }}
Como respuesta debería recibir lo siguiente:
Y por último, crea un archivo público con la siguiente mutación:
mutation CreatePublicFile { createFile(data: { content: "This is a public file!" confidential: false }) { content confidential }}
Si tiene éxito, debería generar la siguiente respuesta:
Ahora que todos los datos de muestra están en su lugar, necesitamos reglas de acceso, ya que este artículo trata sobre cómo proteger una API GraphQL. Las reglas de acceso determinan cómo se puede acceder a los datos de muestra que acabamos de crear, ya que de forma predeterminada un usuario sólo puede acceder a su propia entidad de usuario. En este caso vamos a implementar las siguientes reglas de acceso:
- Permita que los usuarios empleados lean únicamente archivos públicos.
- Permita que los usuarios administradores lean tanto archivos públicos como, solo durante los días laborables, archivos confidenciales.
Como ya habrás notado, estas reglas de acceso son muy específicas. Sin embargo, veremos que el sistema ABAC es lo suficientemente potente como para expresar reglas muy complejas sin interferir en el diseño de su API GraphQL.
Dichas reglas de acceso no forman parte de la especificación GraphQL, por lo que definiremos las reglas de acceso en Fauna Query Language (FQL) y luego verificaremos que estén funcionando como se esperaba ejecutando algunas consultas desde la API GraphQL.
Pero ¿qué es ese sistema “ABAC” que acabamos de mencionar? ¿Qué representa y qué puede hacer?
¿Qué es ABAC?
ABAC significa Control de acceso basado en atributos. Como su nombre indica, es un modelo de autorización que establece políticas de acceso basadas en atributos. En palabras simples, significa que puedes escribir reglas de seguridad que involucren cualquiera de los atributos de tus datos. Si nuestros datos contienen usuarios, podríamos utilizar la función, el departamento y el nivel de autorización para otorgar o rechazar el acceso a datos específicos. O podríamos usar la hora actual, el día de la semana o la ubicación del usuario para decidir si puede acceder a un recurso específico.
En esencia, ABAC permite la definición de políticas de control de acceso detalladas basadas en las propiedades ambientales y sus datos. Ahora que sabemos lo que puede hacer, definamos algunas reglas de acceso para darle ejemplos concretos.
Definición de las reglas de acceso
En FaunaDB, las reglas de acceso se definen en forma de roles. Un rol consta de los siguientes datos:
- nombre: el nombre que identifica el rol
- privilegios: acciones específicas que se pueden ejecutar en recursos específicos
- membresía: identidades específicas que deben tener los privilegios especificados
Los roles se crean a través de la CreateRole
función FQL, como se muestra en el siguiente fragmento de ejemplo:
CreateRole({ name: "role_name", membership: [ // ... ], privileges: [ // ... ]})
Puedes ver dos conceptos importantes en este rol; membresía y privilegios. La membresía define quién recibe los privilegios del rol y los privilegios definen cuáles son estos permisos. Para empezar, escribamos una regla de ejemplo simple: "Cualquier usuario puede leer todos los archivos".
Dado que la regla se aplica a todos los usuarios, definiríamos la membresía de esta manera:
membership: { resource: Collection("User")}
Sencillo ¿verdad? Luego continuamos definiendo el privilegio "Puede leer todos los archivos" para todos estos usuarios.
privileges: [ { resource: Collection("File"), actions: { read: true } }]
El efecto directo de esto es que cualquier token que reciba al iniciar sesión con un usuario a través de nuestra loginUser
mutación GraphQL ahora puede acceder a todos los archivos.
Esta es la regla más simple que podemos escribir, pero en nuestro ejemplo queremos limitar el acceso a algunos archivos confidenciales. Para hacer eso, podemos reemplazar la {read: true}
sintaxis con una función. Como hemos definido que el recurso del privilegio es la colección "Archivo", esta función tomará cada archivo al que accedería una consulta como primer parámetro. Luego puedes escribir reglas como: “Un usuario sólo puede acceder a un archivo si no es confidencial”. En el FQL de FaunaDB, dicha función se escribe usando Query(Lambda(‘x’, … logic that users Var(‘x’)))
.
A continuación se muestra el privilegio que solo proporcionaría acceso de lectura a archivos no confidenciales:
privileges: [ { resource: Collection("File"), actions: { // Read and establish rule based on action attribute read: Query( // Read and establish rule based on resource attribute Lambda("fileRef", Not(Select(["data", "confidential"], Get(Var("fileRef")))) ) ) } }]
Esto utiliza directamente las propiedades del recurso "Archivo" al que intentamos acceder. Dado que es sólo una función, también podríamos tener en cuenta propiedades ambientales como la hora actual. Por ejemplo, escribamos una regla que solo permita el acceso entre semana.
privileges: [ { resource: Collection("File"), actions: { read: Query( Lambda("fileRef", Let( { dayOfWeek: DayOfWeek(Now()) }, And(GTE(Var("dayOfWeek"), 1), LTE(Var("dayOfWeek"), 5)) ) ) ) } }]
Como se menciona en nuestras reglas, los archivos confidenciales solo deben ser accesibles para los administradores. Los administradores también son usuarios, por lo que necesitamos una regla que se aplique a un segmento específico de nuestra colección de usuarios. Por suerte, también podemos definir la membresía como una función; por ejemplo, el siguiente Lambda solo considera que los usuarios que tienen el MANAGER
rol forman parte de la membresía del rol.
membership: { resource: Collection("User"), predicate: Query( // Read and establish rule based on user attribute Lambda("userRef", Equals(Select(["data", "role"], Get(Var("userRef"))), "MANAGER") ) )}
En resumen, los roles de FaunaDB son entidades muy flexibles que permiten definir reglas de acceso basadas en todos los atributos de los elementos del sistema, con diferentes niveles de granularidad. El lugar donde se definen las reglas (privilegios o membresía) determina su granularidad y los atributos disponibles, y diferirá según cada caso de uso particular.
Ahora que hemos cubierto los conceptos básicos de cómo funcionan los roles, ¡continuemos creando las reglas de acceso para nuestro caso de uso de ejemplo!
Para mantener todo limpio y ordenado, crearemos dos roles: uno para cada una de las reglas de acceso. Esto nos permitirá ampliar los roles con más reglas de forma organizada si es necesario más adelante. No obstante, tenga en cuenta que, si fuera necesario, todas las reglas también podrían haberse definido juntas dentro de una sola función.
Implementemos la primera regla:
"Permitir que los usuarios empleados lean únicamente archivos públicos".
Para crear un rol que cumpla estas condiciones, utilizaremos la siguiente consulta:
CreateRole({ name: "employee_role", membership: { resource: Collection("User"), predicate: Query( Lambda("userRef", // User attribute based rule: // It grants access only if the User has EMPLOYEE role. // If so, further rules specified in the privileges // section are applied next. Equals(Select(["data", "role"], Get(Var("userRef"))), "EMPLOYEE") ) ) }, privileges: [ { // Note: 'allFiles' Index is used to retrieve the // documents from the File collection. Therefore, // read access to the Index is required here as well. resource: Index("allFiles"), actions: { read: true } }, { resource: Collection("File"), actions: { // Action attribute based rule: // It grants read access to the File collection. read: Query( Lambda("fileRef", Let( { file: Get(Var("fileRef")), }, // Resource attribute based rule: // It grants access to public files only. Not(Select(["data", "confidential"], Var("file"))) ) ) ) } } ]})
Seleccione la pestaña Shell en la barra lateral izquierda, copie la consulta anterior en el panel de comandos y haga clic en el botón "Ejecutar consulta":
A continuación, implementemos la segunda regla de acceso:
"Permitir a los usuarios administradores leer tanto archivos públicos como, solo durante los días laborables, archivos confidenciales".
En este caso, vamos a utilizar la siguiente consulta:
CreateRole({ name: "manager_role", membership: { resource: Collection("User"), predicate: Query( Lambda("userRef", // User attribute based rule: // It grants access only if the User has MANAGER role. // If so, further rules specified in the privileges // section are applied next. Equals(Select(["data", "role"], Get(Var("userRef"))), "MANAGER") ) ) }, privileges: [ { // Note: 'allFiles' Index is used to retrieve // documents from the File collection. Therefore, // read access to the Index is required here as well. resource: Index("allFiles"), actions: { read: true } }, { resource: Collection("File"), actions: { // Action attribute based rule: // It grants read access to the File collection. read: Query( Lambda("fileRef", Let( { file: Get(Var("fileRef")), dayOfWeek: DayOfWeek(Now()) }, Or( // Resource attribute based rule: // It grants access to public files. Not(Select(["data", "confidential"], Var("file"))), // Resource and environmental attribute based rule: // It grants access to confidential files only on weekdays. And( Select(["data", "confidential"], Var("file")), And(GTE(Var("dayOfWeek"), 1), LTE(Var("dayOfWeek"), 5)) ) ) ) ) ) } } ]})
Copie la consulta en el panel de comando y haga clic en el botón "Ejecutar consulta":
En este punto, hemos creado todos los elementos necesarios para implementar y probar nuestro caso de uso de ejemplo. Continuaremos verificando que las reglas de acceso que acabamos de crear estén funcionando como se esperaba...
Poniendo todo en acción
Empecemos comprobando la primera regla:
"Permitir que los usuarios empleados lean únicamente archivos públicos".
Lo primero que debemos hacer es iniciar sesión como usuario empleado para que podamos verificar qué archivos se pueden leer en su nombre. Para hacerlo, ejecute la siguiente mutación desde la consola GraphQL Playground:
mutation LoginEmployeeUser { loginUser(input: { username: "peter.gibbons" password: "abcdef" })}
Como respuesta, debería obtener un token de acceso secreto. El secreto representa que el usuario ha sido autenticado exitosamente:
En este punto, es importante recordar que las reglas de acceso que definimos anteriormente no están asociadas directamente con el secreto que se genera como resultado del proceso de inicio de sesión. A diferencia de otros modelos de autorización, el token secreto en sí no contiene ninguna información de autorización, sino que es solo una representación de autenticación de un documento determinado.
Como se explicó anteriormente, las reglas de acceso se almacenan en roles y los roles se asocian con documentos a través de su configuración de membresía. Después de la autenticación, el token secreto se puede utilizar en solicitudes posteriores para demostrar la identidad de la persona que llama y determinar qué roles están asociados con ella. Esto significa que las reglas de acceso se verifican efectivamente en cada solicitud posterior y no solo durante la autenticación. Este modelo nos permite modificar las reglas de acceso dinámicamente sin requerir que los usuarios se autentiquen nuevamente.
Ahora usaremos el secreto emitido en el paso anterior para validar la identidad de la persona que llama en nuestra próxima consulta. Para hacerlo, debemos incluir el secreto como token al portador como parte de la solicitud. Para lograr esto, tenemos que modificar el Authorization
valor del encabezado establecido por GraphQL Playground. Como no queremos perdernos el secreto de administrador que se utiliza de forma predeterminada, lo haremos en una nueva pestaña.
Haga clic en el botón más ( +
) para crear una nueva pestaña y seleccione el HTTP HEADERS
panel en la esquina inferior izquierda del editor GraphQL Playground. Luego, modifique el valor del encabezado Autorización para incluir el secreto obtenido anteriormente, como se muestra en el siguiente ejemplo. Asegúrese de cambiar también el valor del esquema de Básico a Portador:
{ "authorization": "Bearer fnEDdByZ5JACFANyg5uLcAISAtUY6TKlIIb2JnZhkjU-SWEaino"}
Con el secreto configurado correctamente en la solicitud, intentemos leer todos los archivos en nombre del usuario empleado. Ejecute la siguiente consulta desde GraphQL Playground:
query ReadFiles { allFiles { data { content confidential } }}
En la respuesta, debería ver solo el archivo público:
Dado que el rol que definimos para los usuarios empleados no les permite leer archivos confidenciales, ¡se han filtrado correctamente de la respuesta!
Pasemos ahora a verificar nuestra segunda regla:
"Permitir a los usuarios administradores leer tanto archivos públicos como, solo durante los días laborables, archivos confidenciales".
Esta vez iniciaremos sesión como usuario empleado. Dado que la mutación de inicio de sesión requiere un token secreto de administrador, primero debemos volver a la pestaña original que contiene la configuración de autorización predeterminada. Una vez allí, ejecute la siguiente consulta:
mutation LoginManagerUser { loginUser(input: { username: "bill.lumbergh" password: "123456" })}
Deberías recibir un nuevo secreto como respuesta:
Copie el secreto, cree una nueva pestaña y modifique el Authorization
encabezado para incluir el secreto como un token de portador como lo hicimos antes. Luego, ejecute la siguiente consulta para leer todos los archivos en nombre del usuario administrador:
query ReadFiles { allFiles { data { content confidential } }}
Siempre que ejecute esta consulta en un día laborable (si no, no dude en actualizar esta regla para incluir los fines de semana), debería obtener tanto el archivo público como el confidencial en la respuesta:
¡Y, finalmente, hemos verificado que todas las reglas de acceso funcionan correctamente desde la API GraphQL!
Conclusión
En esta publicación, hemos aprendido cómo se puede implementar un modelo de autorización integral sobre la API GraphQL de FaunaDB utilizando las funciones ABAC integradas de FaunaDB. También hemos revisado las capacidades distintivas de ABAC, que permiten definir reglas de acceso complejas basadas en los atributos de cada componente del sistema.
Si bien las reglas de acceso solo se pueden definir a través de la API FQL por el momento, se verifican de manera efectiva para cada solicitud ejecutada en la API GraphQL de FaunaDB. Ya está previsto para el futuro proporcionar soporte para especificar reglas de acceso como parte de la definición del esquema GraphQL.
En resumen, FaunaDB proporciona un mecanismo poderoso para definir reglas de acceso complejas además de la API GraphQL que cubre los casos de uso más comunes sin la necesidad de servicios de terceros.
Deja un comentario