Casos de uso del control de acceso para proteger las solicitudes y respuestas - AWS AppSync

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Casos de uso del control de acceso para proteger las solicitudes y respuestas

En la sección de seguridad, aprendió sobre los diferentes modos de autorización para protegerlo API y se hizo una introducción sobre los mecanismos de autorización detallados para comprender los conceptos y el flujo. Dado que AWS AppSync le permite realizar operaciones lógicas completas en los datos mediante el uso de plantillas de mapeo de GraphQL Resolver, puede proteger los datos de lectura o escritura de una manera muy flexible mediante una combinación de identidad de usuario, condicionales e inyección de datos.

Si no estás familiarizado con la edición de AWS AppSync Resolvers, consulta la guía de programación.

Información general

La concesión de acceso a los datos de un sistema se realiza normalmente a través de una matriz de control de acceso, donde la intersección de una fila (recurso) y una columna (usuario/rol) es el permiso concedido.

AWS AppSync usa los recursos de tu propia cuenta y enlaza la información de identidad (usuario/rol) en la solicitud y respuesta de GraphQL como un objeto de contexto, que puedes usar en el solucionador. Esto significa que los permisos se pueden conceder de forma adecuada en operaciones de lectura o escritura en función de la lógica del solucionador. Si esta lógica se basa en el nivel de los recursos, por ejemplo, solo algunos usuarios o grupos determinados pueden leer o escribir en una fila específica de la base de datos, entonces esos «metadatos de autorización» deben almacenarse. AWS AppSync no almacena ningún dato, por lo que debe almacenar estos metadatos de autorización con los recursos para poder calcular los permisos. Normalmente los metadatos de la autorización son un atributo (columna) de una tabla de DynamoDB, por ejemplo un propietario o una lista de usuarios o grupos. Por ejemplo, podrían existir los atributos lectores y escritores.

En general esto significa que al leer un elemento individual de un origen de datos, ejecutará una instrucción condicional #if () ... #end para la plantilla de respuesta una vez que el solucionador haya leído el origen de datos. Normalmente la comprobación usará valores de usuario o grupo de $context.identity para realizar comprobaciones de pertenencia con los metadatos de autorización que devuelve una operación de lectura. Cuando haya varios registros, como en el caso de listas obtenidas con operaciones Scan o Query aplicadas a una tabla, la comprobación de condición se envía como parte de la operación al origen de datos usando valores de usuario o grupo similares.

Del mismo modo, al escribir datos se aplica una instrucción condicional a la acción (como PutItem o UpdateItem) a fin de comprobar si el usuario o el grupo que realiza la mutación tiene permiso para ello. A menudo, la instrucción condicional usará un valor de $context.identity para compararlo con los metadatos de autorización del recurso. Tanto para las plantillas de solicitud como para las de respuesta, también se pueden usar encabezados personalizados de clientes para realizar comprobaciones de validación.

Lectura de datos

Como se ha indicado anteriormente, los metadatos de autorización para realizar una comprobación se deben almacenar con el recurso o se deben transferir a la solicitud de GraphQL (identidad, encabezado, etc.). Como demostración, supongamos que tiene la tabla de DynamoDB siguiente:

DynamoDB table with ID, Data, PeopleCanAccess, GroupsCanAccess, and Owner columns.

La clave principal es id y los datos a los que se debe obtener acceso son Data. El resto de columnas son ejemplos de comprobaciones que puede realizar para la autorización. Owner es de tipo String, mientras que PeopleCanAccess y GroupsCanAccess son String Sets, como se describe en Resolver mapping template reference for DynamoD.

El diagrama de Información general sobre plantillas de mapeo de solucionador muestra cómo la plantilla de respuesta no solo contiene el objeto de contexto, sino también los resultados del origen de datos. En las consultas de GraphQL referidas a elementos individuales puede utilizar la plantilla de respuesta para comprobar si el usuario tiene permiso para ver estos resultados o devolver un mensaje de error de autorización. Esto es lo que a veces se denomina “filtro de autorización”. En el caso de las consultas de GraphQL que devuelven listas como resultado de Scan o Query, es más eficaz hacer la comprobación en la plantilla de solicitud y devolver los datos solo si se cumple una condición de autorización. La implementación es entonces la siguiente:

  1. GetItem - comprobación de autorizaciones para registros individuales. Se realiza con instrucciones #if() ... #end.

  2. Operaciones Scan o Query: la comprobación de autorización es una instrucción "filter":{"expression":...}. Algunas comprobaciones habituales son de igualdad (attribute = :input) o si un valor se encuentra en una lista (contains(attribute, :input)).

En el n.º 2, attribute representa en ambas instrucciones el nombre de columna del registro en una tabla, por ejemplo, Owner en el ejemplo anterior. Puede aplicar un alias con un signo # y utilizar, "expressionNames":{...}, pero no es obligatorio. :input es una referencia al valor que va a comparar con el atributo de base de datos, que definirá en "expressionValues":{...}. Verá estos ejemplos a continuación.

Caso de uso: el propietario puede leer

Conforme a la tabla anterior, si quiere que solo se devuelvan datos cuando Owner == Nadia en una operación de lectura individual (GetItem), la plantilla será similar a la siguiente:

#if($context.result["Owner"] == $context.identity.username) $utils.toJson($context.result) #else $utils.unauthorized() #end

Aquí deben tenerse en cuenta algunos puntos, ya que se volverán a usar en las secciones restantes. En primer lugar, la verificación utiliza $context.identity.username el nombre de registro fácil de usar si se utilizan grupos de usuarios de Amazon Cognito y la identidad del usuario IAM si se utiliza (incluidas las identidades federadas de Amazon Cognito). También deben almacenarse otros valores para el propietario, como el valor único “identidad de Amazon Cognito”, que es útil cuando se federan inicios de sesión desde varias ubicaciones, y no se olvide revisar las opciones disponibles en Resolver Mapping Template Context Reference.

En segundo lugar, el condicional con el que se responde a la verificación $util.unauthorized() es completamente opcional, pero se recomienda como práctica recomendada a la hora de diseñar tu GraphQLAPI.

Caso de uso: acceso específico codificado

// This checks if the user is part of the Admin group and makes the call #foreach($group in $context.identity.claims.get("cognito:groups")) #if($group == "Admin") #set($inCognitoGroup = true) #end #end #if($inCognitoGroup) { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) }, "attributeValues" : { "owner" : $util.dynamodb.toDynamoDBJson($context.identity.username) #foreach( $entry in $context.arguments.entrySet() ) ,"${entry.key}" : $util.dynamodb.toDynamoDBJson($entry.value) #end } } #else $utils.unauthorized() #end

Caso de uso: filtro de una lista de resultados

En el ejemplo anterior pudimos realizar directamente una comprobación con $context.result, ya que se devolvía un único elemento. Sin embargo, algunas operaciones, como Scan, devuelven varios elementos en $context.result.items, por lo que hay que filtrar la autorización y devolver únicamente los resultados que el usuario tiene permiso para ver. Supongamos que esta vez el campo Owner tiene el IdentityID de Amazon Cognito establecido en el registro. En este caso, puede utilizar la siguiente plantilla de mapeo de respuestas para filtrar los registros y mostrar solo los que son propiedad del usuario:

#set($myResults = []) #foreach($item in $context.result.items) ##For userpools use $context.identity.username instead #if($item.Owner == $context.identity.cognitoIdentityId) #set($added = $myResults.add($item)) #end #end $utils.toJson($myResults)

Caso de uso: varias personas pueden leer

Otra opción de autorización frecuente consiste en permitir a un grupo de personas leer datos. En el ejemplo siguiente, "filter":{"expression":...} solo devuelve valores obtenidos de una tabla si el usuario que ejecuta la consulta GraphQL aparece en el conjunto PeopleCanAccess.

{ "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.count}) $util.toJson($context.arguments.count) #else 20 #end, "nextToken": #if(${context.arguments.nextToken}) $util.toJson($context.arguments.nextToken) #else null #end, "filter":{ "expression": "contains(#peopleCanAccess, :value)", "expressionNames": { "#peopleCanAccess": "peopleCanAccess" }, "expressionValues": { ":value": $util.dynamodb.toDynamoDBJson($context.identity.username) } } }

Caso de uso: un grupo puede leer

Al igual que en el caso de uso anterior, puede ocurrir que solo las personas de uno o varios grupos tengan derecho a leer determinados elementos de una base de datos. El uso de la operación "expression": "contains()" es similar. Sin embargo se trata de una disyuntiva OR lógica de todos los grupos a los que puede pertenecer el usuario, lo que debe tenerse en cuenta en la pertenencia a conjuntos. En este caso hemos creado una instrucción $expression para cada grupo al que pertenece el usuario y la pasamos al filtro:

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.count}) $util.toJson($context.arguments.count) #else 20 #end, "nextToken": #if(${context.arguments.nextToken}) $util.toJson($context.arguments.nextToken) #else null #end, "filter":{ "expression": "$expression", "expressionValues": $utils.toJson($expressionValues) } }

Escritura de datos

La escritura de datos en las mutaciones siempre se controla en la plantilla de mapeo de solicitud. En el caso de los orígenes de datos de DynamoDB, la clave radica en utilizar una expresión de condición "condition":{"expression"...}" adecuada que efectúe validaciones aplicando los metadatos de autorización de la tabla. En Seguridad proporcionamos un ejemplo que puede utilizar para comprobar el campo Author de una tabla. Los casos de uso de esta sección exploran otras posibilidades.

Caso de uso: varios propietarios

Partiendo del diagrama de tabla del ejemplo anterior, observemos la lista PeopleCanAccess.

{ "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) }, "update" : { "expression" : "SET meta = :meta", "expressionValues": { ":meta" : $util.dynamodb.toDynamoDBJson($ctx.args.meta) } }, "condition" : { "expression" : "contains(Owner,:expectedOwner)", "expressionValues" : { ":expectedOwner" : $util.dynamodb.toDynamoDBJson($context.identity.username) } } }

Caso de uso: el grupo puede crear un registro nuevo

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "PutItem", "key" : { ## If your table's hash key is not named 'id', update it here. ** "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) ## If your table has a sort key, add it as an item here. ** }, "attributeValues" : { ## Add an item for each field you would like to store to Amazon DynamoDB. ** "title" : $util.dynamodb.toDynamoDBJson($ctx.args.title), "content": $util.dynamodb.toDynamoDBJson($ctx.args.content), "owner": $util.dynamodb.toDynamoDBJson($context.identity.username) }, "condition" : { "expression": $util.toJson("attribute_not_exists(id) AND $expression"), "expressionValues": $utils.toJson($expressionValues) } }

Caso de uso: el grupo puede actualizar un registro existente

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) }, "update":{ "expression" : "SET title = :title, content = :content", "expressionValues": { ":title" : $util.dynamodb.toDynamoDBJson($ctx.args.title), ":content" : $util.dynamodb.toDynamoDBJson($ctx.args.content) } }, "condition" : { "expression": $util.toJson($expression), "expressionValues": $utils.toJson($expressionValues) } }

Registros públicos y privados

Los filtros condicionales también le permiten marcar datos como privados, públicos o efectuar alguna comprobación booleana. Esto puede combinarse como parte de un filtro de autorización dentro de la plantilla de respuesta. El uso de esta comprobación es una buena manera de ocultar temporalmente datos o eliminarlos de la vista sin tener que controlar la pertenencia al grupo.

Por ejemplo, supongamos que añade un atributo a cada elemento de una tabla de DynamoDB denominada public con el valor yes o no. La siguiente plantilla de respuesta se puede usar en una GetItem llamada para mostrar datos únicamente si el usuario está en un grupo al que tiene acceso AND si esos datos están marcados como públicos:

#set($permissions = $context.result.GroupsCanAccess) #set($claimPermissions = $context.identity.claims.get("cognito:groups")) #foreach($per in $permissions) #foreach($cgroups in $claimPermissions) #if($cgroups == $per) #set($hasPermission = true) #end #end #end #if($hasPermission && $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end

El código anterior también puede utilizar un operador lógico O (||) para permitir a los usuarios leer si tienen permiso sobre un registro o si este es público:

#if($hasPermission || $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end

En general los operadores estándar ==, !=, && y || le resultarán útiles cuando realice comprobaciones de autorización.

Datos en tiempo real

Puede aplicar controles de acceso precisos en las suscripciones a GraphQL en el momento en que un cliente realice una suscripción utilizando las técnicas descritas anteriormente en esta documentación. Asocie un solucionador al campo de suscripción y ya podrá consultar un origen de datos y aplicar lógica condicional en la plantilla de mapeo de solicitud o en la de respuesta. También puede devolver datos adicionales al cliente, como los resultados iniciales de una suscripción, siempre que la estructura de datos coincida con la del tipo devuelto en la suscripción de GraphQL.

Caso de uso: el usuario solo puede suscribirse a conversaciones específicas

Un caso de uso común de datos en tiempo real con suscripciones de GraphQL consiste en crear una aplicación de mensajería o de chat privado. Al crear una aplicación de chat para varios usuarios, pueden producirse conversaciones entre dos personas o entre varias personas. Los usuarios pueden agruparse en “salas” que pueden ser privadas o públicas. En consecuencia, es necesario autorizar a cada usuario a suscribirse únicamente a las conversaciones (con otro usuario o dentro de un grupo) para las que se le haya concedido permiso. Con fines de demostración, el ejemplo siguiente muestra un caso de uso sencillo de un usuario que envía un mensaje privado a otro. La configuración tiene dos tablas de Amazon DynamoDB:

  • Tabla Messages (Mensajes): (clave principal) toUser, (clave de ordenación) id

  • Tabla Permissions (Permisos): (clave principal) username

La tabla Messages almacena los mensajes que se envían a través de una mutación de GraphQL. La suscripción de GraphQL comprueba la tabla Permissions para consultar si existe una autorización en el tiempo de conexión del cliente. En el siguiente ejemplo se presupone que se usa el siguiente esquema de GraphQL:

input CreateUserPermissionsInput { user: String! isAuthorizedForSubscriptions: Boolean } type Message { id: ID toUser: String fromUser: String content: String } type MessageConnection { items: [Message] nextToken: String } type Mutation { sendMessage(toUser: String!, content: String!): Message createUserPermissions(input: CreateUserPermissionsInput!): UserPermissions updateUserPermissions(input: UpdateUserPermissionInput!): UserPermissions } type Query { getMyMessages(first: Int, after: String): MessageConnection getUserPermissions(user: String!): UserPermissions } type Subscription { newMessage(toUser: String!): Message @aws_subscribe(mutations: ["sendMessage"]) } input UpdateUserPermissionInput { user: String! isAuthorizedForSubscriptions: Boolean } type UserPermissions { user: String isAuthorizedForSubscriptions: Boolean } schema { query: Query mutation: Mutation subscription: Subscription }

Algunas de las operaciones estándar, como createUserPermissions(), no se tratan a continuación para ilustrar los solucionadores de suscripción, pero son implementaciones estándar de solucionadores de DynamoDB. En su lugar, vamos a centrarnos en los flujos de autorización de suscripción con solucionadores. Para enviar un mensaje de un usuario a otro, asocie un solucionador al campo sendMessage() y seleccione como origen de datos la tabla Messages con la plantilla de solicitud siguiente:

{ "version" : "2017-02-28", "operation" : "PutItem", "key" : { "toUser" : $util.dynamodb.toDynamoDBJson($ctx.args.toUser), "id" : $util.dynamodb.toDynamoDBJson($util.autoId()) }, "attributeValues" : { "fromUser" : $util.dynamodb.toDynamoDBJson($context.identity.username), "content" : $util.dynamodb.toDynamoDBJson($ctx.args.content), } }

En este ejemplo, usaremos $context.identity.username. Esto devuelve la información de usuario de AWS Identity and Access Management nuestros usuarios de Amazon Cognito. La plantilla de respuesta simplemente traslada $util.toJson($ctx.result). Guarde el resultado y vuelva a la página de esquema. Asocie entonces un solucionador para la suscripción newMessage() utilizando la tabla Permissions como origen de datos y la siguiente plantilla de mapeo de solicitud:

{ "version": "2018-05-29", "operation": "GetItem", "key": { "username": $util.dynamodb.toDynamoDBJson($ctx.identity.username), }, }

Use entonces la siguiente plantilla de mapeo de respuesta para realizar las comprobaciones de autorización con los datos de la tabla Permissions:

#if(! ${context.result}) $utils.unauthorized() #elseif(${context.identity.username} != ${context.arguments.toUser}) $utils.unauthorized() #elseif(! ${context.result.isAuthorizedForSubscriptions}) $utils.unauthorized() #else ##User is authorized, but we return null to continue null #end

En este caso se hacen tres comprobaciones de autorización. La primera asegura que se devuelva un resultado. La segunda garantiza que el usuario no se suscriba a mensajes destinados a otra persona. La tercera garantiza que el usuario tiene permiso para suscribirse a cualquier campo comprobando el atributo de isAuthorizedForSubscriptions de DynamoDB almacenado como BOOL.

Para probarlo, puede iniciar sesión en la AWS AppSync consola con los grupos de usuarios de Amazon Cognito y un usuario llamado «Nadia» y, a continuación, ejecutar la siguiente suscripción de GraphQL:

subscription AuthorizedSubscription { newMessage(toUser: "Nadia") { id toUser fromUser content } }

Si en la tabla Permissions hay un registro para el atributo de clave username Nadia con isAuthorizedForSubscriptions establecido en true, verá una respuesta correcta. Si prueba otro username en la consulta de newMessage() anterior, se devolverá un error.