Cas d'utilisation du contrôle d'accès pour sécuriser les demandes et les réponses - AWS AppSync

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Cas d'utilisation du contrôle d'accès pour sécuriser les demandes et les réponses

Dans la section Sécurité, vous avez découvert les différents modes d'autorisation permettant de vous protéger API et une introduction a été donnée sur les mécanismes d'autorisation précis pour comprendre les concepts et le flux. Comme il vous AWS AppSync permet d'effectuer des opérations logiques complètes sur les données à l'aide de modèles de mappage GraphQL Resolver, vous pouvez protéger les données en lecture ou en écriture de manière très flexible en combinant identité utilisateur, conditions et injection de données.

Si vous n'êtes pas habitué à modifier des AWS AppSync résolveurs, consultez le guide de programmation.

Présentation

L'accès aux données d'un système se fait traditionnellement par le biais d'une matrice de contrôle d'accès où l'intersection d'une ligne (ressource) et d'une colonne (utilisateur/rôle) correspond aux autorisations accordées.

AWS AppSync utilise les ressources de votre propre compte et intègre les informations d'identité (utilisateur/rôle) dans la requête et la réponse GraphQL sous forme d'objet contextuel, que vous pouvez utiliser dans le résolveur. Cela signifie que les autorisations peuvent être accordées de façon appropriée sur des opérations de lecture ou d'écriture en fonction de la logique du résolveur. Si cette logique se situe au niveau des ressources, par exemple, seuls certains utilisateurs ou groupes nommés peuvent lire/écrire sur une ligne de base de données spécifique, ces « métadonnées d'autorisation » doivent être stockées. AWS AppSync ne stocke aucune donnée. Vous devez donc stocker ces métadonnées d'autorisation avec les ressources afin que les autorisations puissent être calculées. Les métadonnées d'autorisation sont généralement un attribut (colonne) d'une table DynamoDB, tel qu'un propriétaire ou une liste d'utilisateurs/groupes. Par exemple, il pourrait y avoir des attributs Readers et Writers.

Depuis un niveau élevé, cela signifie que si vous lisez un élément individuel à partir d'une source de données, vous devez effectuer une déclaration conditionnelle #if () ... #end dans le modèle de réponse après que le résolveur a lu à partir de la source de données. Le contrôle utilisera normalement les valeurs d'utilisateur ou de groupe dans $context.identity pour les contrôles d'appartenance par rapport aux métadonnées d'autorisation renvoyées par une opération de lecture. Pour plusieurs enregistrements, tels que les listes renvoyées à partir d'une table Scan ou Query, vous envoyez le contrôle de la condition à la source de données, comme partie intégrante de l'opération, à l'aide de valeurs d'utilisateur ou de groupe similaires.

De même, lors de l'écriture des données, vous appliquerez une instruction conditionnelle à l'action (comme un PutItem ou UpdateItem pour voir si l'utilisateur ou le groupe effectuant la mutation a l'autorisation). L'instruction conditionnelle utilisera à nouveau à plusieurs reprises une valeur dans $context.identity à titre de comparaison avec les métadonnées d'autorisation sur cette ressource. Pour les modèles de demande et de réponse, vous pouvez également utiliser des en-têtes personnalisés provenant des clients pour effectuer les contrôles de validation.

Lecture de données

Comme indiqué ci-dessus, les métadonnées d'autorisation pour effectuer un contrôle doivent être stockées avec une ressource ou transmises à la demande GraphQL (identité, en-tête, etc.). Pour illustrer cela, supposons que vous ayez la table DynamoDB ci-dessous :

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

La clé primaire est id et les données qui doivent être accessibles Data. Les autres colonnes sont des exemples de vérifications que vous pouvez effectuer pour obtenir une autorisation. Ownerprendrait un String certain temps PeopleCanAccess et GroupsCanAccess serait String Sets tel que décrit dans la référence du modèle de mappage Resolver pour DynamoDB.

Dans la présentation du modèle de mappage des résolveurs, le diagramme montre comment le modèle de réponse contient non seulement l'objet de contexte, mais aussi les résultats de la source de données. Pour les requêtes GraphQL des objets individuels, vous pouvez utiliser le modèle de réponse pour vérifier si l'utilisateur est autorisé à voir les résultats ou à renvoyer un message d'erreur relatif à l'autorisation. Cet élément est parfois appelé « filtre d'autorisation ». Pour les requêtes GraphQL renvoyant des listes, à l'aide d'une requête (Query) ou d'une analyse (Scan), il est plus performant d'effectuer le contrôle sur le modèle de demande et de ne renvoyer les données que si une condition d'autorisation est remplie. L'implémentation se présente alors ainsi :

  1. GetItem - contrôle d'autorisation pour les enregistrements individuels. Fait à l'aide d'instructions #if() ... #end.

  2. Opérations Query/Scan – Le contrôle d'autorisation est une déclaration "filter":{"expression":...}. Les contrôles courants concernent l'égalité (attribute = :input) ou la vérification de la présence d'une valeur dans une liste (contains(attribute, :input)).

Dans #2, l'attribut attribute des deux déclarations représente le nom de colonne de l'enregistrement dans une table, comme Owner dans notre exemple ci-dessus. Vous pouvez créer un alias à l'aide du signe # et utiliser "expressionNames":{...}, mais ce n'est pas obligatoire. L'élément :input est une référence à la valeur que vous comparez à l'attribut de base de données, que vous définissez dans "expressionValues":{...}. Voyez les exemples ci-dessous.

Cas d'utilisation : le propriétaire sait lire

À l'aide du tableau ci-dessus, si vous souhaitez uniquement renvoyer les données si Owner == Nadia dans le cas d'une opération de lecture (GetItem), votre modèle se présente comme suit :

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

Quelques remarques à mentionner ici seront réutilisées dans les sections restantes. Tout d'abord, le check utilise $context.identity.username le nom d'inscription convivial si les groupes d'utilisateurs Amazon Cognito sont utilisés et l'identité de l'utilisateur IAM s'il est utilisé (y compris les identités fédérées Amazon Cognito). Il existe d'autres valeurs à enregistrer pour un propriétaire, comme la valeur unique « identité Amazon Cognito », qui est utile lors de la fédération de connexions provenant de plusieurs sites. Vous devriez consulter les options disponibles dans la référence contextuelle du modèle de mappage du résolveur.

Ensuite, la vérification conditionnelle sinon qui répond par $util.unauthorized() est totalement facultative mais recommandée comme meilleure pratique lors de la conception de votre GraphQLAPI.

Cas d'utilisation : accès spécifique par code en dur

// 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

Cas d'utilisation : filtrage d'une liste de résultats

Dans l'exemple précédent, vous avez été en mesure de contrôler directement $context.result tandis qu'il renvoyait un seul élément ; cependant, certaines opérations telles qu'une analyse renvoient plusieurs éléments dans $context.result.items où vous devez exécuter le filtre d'autorisation de filtre et retourner uniquement les résultats que l'utilisateur est autorisé à afficher. Supposons que le Owner champ comporte cette fois l'identifiant Amazon Cognito IdentityID défini sur l'enregistrement. Vous pouvez ensuite utiliser le modèle de mappage des réponses suivant pour filtrer afin d'afficher uniquement les enregistrements appartenant à l'utilisateur :

#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)

Cas d'utilisation : plusieurs personnes peuvent lire

Une autre option d'autorisation populaire consiste à autoriser un groupe de personnes à pouvoir lire les données. Dans l'exemple ci-dessous, "filter":{"expression":...} renvoie uniquement les valeurs d'une analyse de table si l'utilisateur exécutant la requête GraphQL est répertorié dans l'ensemble 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) } } }

Cas d'utilisation : le groupe peut lire

Comme pour le dernier scénario, il se peut que seules les personnes d'un ou de plusieurs groupes aient les droits pour lire certains éléments d'une base de données. L'utilisation de l'opération "expression": "contains()" est similaire ; cependant, le fait qu'un utilisateur puisse faire partie de ce qui doit être pris en compte dans l'appartenance à l'ensemble relève d'un OR logique de tous les groupes. Dans ce cas, nous créons une instruction $expression ci-dessous pour chaque groupe dont fait partie l'utilisateur, puis la transmettons au filtre :

#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) } }

Écrire des données

L'écriture de données sur les mutations est toujours contrôlée sur le modèle de mappage de la demande. Dans le cas des sources de données DynamoDB, la clé consiste à utiliser un "condition":{"expression"...}" approprié, qui effectue la validation par rapport aux métadonnées d'autorisation de la table. Dans Sécurité, nous avons fourni un exemple que vous pouvez utiliser pour vérifier le champ Author dans une table. Les cas d'utilisation de cette section explorent d'autres scénarios.

Cas d'utilisation : propriétaires multiples

À l'aide de l'exemple de schéma de table précédent, imaginons la liste 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) } } }

Cas d'utilisation : le groupe peut créer un nouvel enregistrement

#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) } }

Cas d'utilisation : le groupe peut mettre à jour un enregistrement existant

#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) } }

Dossiers publics et privés

Avec les filtres conditionnels, vous pouvez également choisir de marquer les données comme privées, publiques ou booléennes. Elles peuvent ensuite être combinées dans le cadre d'un filtre d'autorisation à l'intérieur du modèle de réponse. L'utilisation de ce contrôle est un bon moyen de masquer les données temporairement sans tenter de contrôler l'appartenance au groupe.

Par exemple, imaginons que vous ayez ajouté un attribut sur chaque élément de votre table DynamoDB appelé public avec la valeur yes ou no. Le modèle de réponse suivant peut être utilisé lors d'un GetItem appel pour afficher les données uniquement si l'utilisateur fait partie d'un groupe qui y a accès AND si ces données sont marquées comme publiques :

#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

Le code ci-dessus peut également utiliser un OR logique (||) pour autoriser les personnes à lire si elles sont autorisés à accéder à un enregistrement ou s'il est public :

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

En général, vous trouverez les opérateurs standard ==, !=, && et || utiles lors de l'exécution des contrôles d'autorisation.

Données en temps réel

Vous pouvez appliquer les contrôles d'accès détaillé aux abonnements GraphQL au moment où un client effectue un abonnement, en utilisant les mêmes techniques que celles décrites plus haut dans la documentation. Vous attachez un résolveur au champ d'abonnement et pouvez alors interroger les données à partir d'une source de données et exécuter une logique conditionnelle dans le modèle de mappage de la demande ou de la réponse. Vous pouvez également renvoyer des données supplémentaires au client, telles que les résultats initiaux d'un abonnement, aussi longtemps que la structure des données correspond à celle du type retourné dans votre abonnement GraphQL.

Cas d'utilisation : l'utilisateur ne peut s'abonner qu'à des conversations spécifiques

Un cas d'utilisation courant pour les données en temps réel avec les abonnements GraphQL consiste à créer une application de messagerie ou chat privé. Lors de la création d'une application de chat qui comporte plusieurs utilisateurs, les conversations peuvent se produire entre deux ou plusieurs personnes. Celles-ci peuvent être regroupées en « salles », qui sont privées ou publiques. À ce titre, vous souhaitez uniquement autoriser un utilisateur à s'abonner à une conversation (qui pourrait être en face à face ou au sein d'un groupe) pour laquelle l'accès lui a été accordé. À des fins de démonstration, l'exemple ci-dessous illustre un simple scénario d'un utilisateur envoyant un message privé à un autre utilisateur. La configuration comporte deux tables Amazon DynamoDB :

  • Table Messages : (clé primaire) toUser, (clé de tri) id

  • Table Permissions : (clé primaire) username

La table Messages stocke les messages réels envoyés via une mutation GraphQL. La table Permissions est contrôlée par l'abonnement GraphQL pour l'autorisation au moment de la connexion du client. L'exemple suivant suppose que vous utilisez le schéma GraphQL suivant :

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 }

Certaines des opérations standard, telles quecreateUserPermissions(), ne sont pas abordées ci-dessous pour illustrer les résolveurs d'abonnement, mais sont des implémentations standard des résolveurs DynamoDB. Au lieu de cela, nous allons nous concentrer sur les flux d'autorisation d'abonnement avec les résolveurs. Pour envoyer un message d'un utilisateur à un autre, attachez un résolveur au champ sendMessage() et sélectionnez la source de données de la table Messages avec le modèle de demande suivant :

{ "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), } }

Dans cet exemple, nous utilisons $context.identity.username. Cela renvoie des informations utilisateur pour AWS Identity and Access Management ou pour les utilisateurs d'Amazon Cognito. Le modèle de réponse est une simple transmission de $util.toJson($ctx.result). Enregistrez et revenez à la page du schéma. Ensuite, attachez un résolveur pour l'abonnement newMessage(), à l'aide de la table Permissions comme source de données et du modèle de mappage de demande suivant :

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

Ensuite, utilisez le modèle de mappage de réponse suivant pour exécuter vos contrôles d'autorisation à l'aide des données de la table 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

Dans ce cas, vous effectuez trois contrôles d'autorisation. Le premier garantit qu'un résultat est retourné. Le second garantit que l'utilisateur ne s'abonne pas aux messages destinés à une autre personne. Le troisième garantit que l'utilisateur est autorisé à s'abonner à n'importe quel champ, en vérifiant qu'un attribut DynamoDB est stocké sous la forme isAuthorizedForSubscriptions d'un. BOOL

Pour tester les choses, vous pouvez vous connecter à la AWS AppSync console à l'aide des groupes d'utilisateurs Amazon Cognito et d'un utilisateur nommé « Nadia », puis exécuter l'abonnement GraphQL suivant :

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

Si, dans la table Permissions, il y a un enregistrement pour l'attribut clé username de Nadia avec isAuthorizedForSubscriptions défini sur true, vous obtiendrez une réponse positive. Si vous essayez un autre username dans la requête newMessage() ci-dessus, une erreur est renvoyée.