As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.
Casos de uso de controle de acesso para proteger solicitações e respostas
Na seção Segurança você aprendeu sobre os diferentes modos de Autorização para proteger sua API e uma introdução foi dada sobre mecanismos de Autorização refinada para entender os conceitos e o fluxo. Como o AWS AppSync permite que você realize operações completas de lógica em dados por meio do uso de Modelos de mapeamento do resolvedor do GraphQL, você pode proteger dados em leitura ou gravação de forma bem flexível usando uma combinação de identidade do usuário, condicionais e injeção de dados.
Se não estiver familiarizado com a edição de resolvedores do AWS AppSync, reveja o guia de programação.
Visão geral
Conceder acesso aos dados em um sistema é tradicionalmente feito por meio de uma Matriz de controle do acesso
O AWS AppSync usa recursos na sua própria conta e informações da identidade (usuário/função) de threads na solicitação e resposta do GraphQL como um objeto de contexto, que pode ser usado no resolvedor. Isso significa que as permissões podem ser concedidas adequadamente em operações de leitura ou gravação com base na lógica do resolvedor. Se essa lógica está a nível de recurso, por exemplo, apenas alguns usuários ou grupos designados podem ler/gravar em uma determinada linha de banco de dados, então esses "metadados de autorização" devem ser armazenados. AWS O AppSync não armazena quaisquer dados, portanto é necessário armazenar esses metadados de autorização com os recursos para que as permissões possam ser calculadas. Os metadados de autorização geralmente são um atributo (coluna) em uma tabela do DynamoDB, como um proprietário ou lista de usuários/grupos. Por exemplo, pode haver atributos Leitores e Gravadores.
Em um alto nível, isso significa que, se estiver lendo um item individual de uma fonte de dados, execute uma instrução #if () ... #end
condicional no modelo da resposta depois que o resolvedor leu a fonte de dados. A verificação normalmente usará valores de usuário ou grupo em $context.identity
para verificações de associação nos metadados de autorização retornados de uma operação de leitura. Para vários registros, como listas retornadas de uma tabela Scan
ou Query
, você enviará a verificação de condição como parte da operação à fonte de dados usando valores de usuário ou grupo semelhantes.
De maneira semelhante à gravação de dados você aplicará uma instrução condicional para a ação (como PutItem
ou UpdateItem
) para ver se o usuário ou grupo que faz uma mutação tem permissão. Novamente, o condicional muitas vezes usará um valor em $context.identity
para comparar nos metadados de autorização daquele recurso. Para ambos os modelos da solicitação e da resposta, você também pode usar cabeçalhos personalizados de clientes para executar verificações de validação.
Leitura de dados
Conforme descrito acima, os metadados de autorização para realizar uma verificação devem ser armazenados com um recurso ou enviados para a solicitação do GraphQL (identidade, cabeçalho, etc.). Para demonstrar isso suponha que você tem a tabela do DynamoDB abaixo:

A chave primária é id
e os dados a serem acessados são Data
. As outras colunas são exemplos de verificações que podem ser executadas para autorização. Owner
seria uma String
enquanto PeopleCanAccess
e GroupsCanAccess
seriam String Sets
, conforme descrito na Referência do modelo de mapeamento do resolvedor para o DynamoDB.
Na visão geral do modelo de mapeamento do resolvedor o diagrama mostra como o modelo da resposta contém não apenas o objeto de contexto, mas também os resultados da fonte de dados. Para consultas do GraphQL de itens individuais, você pode usar o modelo da resposta para verificar se o usuário tem permissão para ver esses resultados ou retornar uma mensagem de erro de autorização. Isso geralmente é indicado como um "Filtro de autorização". Para consultas do GraphQL que retornam listas, usando uma Scan ou Query, é mais eficiente realizar a verificação no modelo da solicitação e retornar dados somente se uma condição de autorização for atendida. A implementação é:
-
GetItem – verificação de autorização para registros individuais. Feita usando instruções
#if() ... #end
. -
Operações Scan/Query – verificação de autorização é uma instrução
"filter":{"expression":...}
. As verificações comuns são a igualdade (attribute = :input
) ou verificar se um valor está em uma lista (contains(attribute, :input)
).
Em #2 o attribute
em ambas as instruções representa o nome da coluna do registro em uma tabela, como Owner
no exemplo acima. Você pode transformar isso em alias com um sinal #
e usar "expressionNames":{...}
, mas não é obrigatório. O :input
é uma referência ao valor que você está comparando com o atributo do banco de dados, que será definido em "expressionValues":{...}
. Você verá esses exemplos abaixo.
Caso de uso: o proprietário pode fazer leitura
Usando a tabela acima, se apenas deseja retornar dados se Owner ==
Nadia
para uma única operação de leitura (GetItem
) o modelo terá a seguinte aparência:
#if($context.result["Owner"] == $context.identity.username) $utils.toJson($context.result) #else $utils.unauthorized() #end
Algumas coisas devem ser mencionadas aqui, que serão reutilizadas nas seções restantes. Primeiro, a verificação usa $context.identity.username
que será o nome de login de usuário amigável se grupos de usuários do Amazon Cognito for usado e será a identidade do usuário se o IAM for usado (incluindo as Identidades federadas do Amazon Cognito). Existem outros valores a serem armazenados para um proprietário, como o valor exclusivo "identidade do Amazon Cognito", que é útil ao federar logins de vários locais, e você deve revisar as opções disponíveis na Referência de contexto do modelo de mapeamento do resolvedor.
Segundo, a verificação condicional else respondendo com $util.unauthorized()
é totalmente opcional, mas recomendado como uma das melhores práticas ao projetar sua API GraphQL.
Caso de uso: codificar acesso específico
// 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: filtrar uma lista de resultados
No exemplo anterior você pôde executar uma verificação em $context.result
diretamente pois retornou um único item, no entanto algumas operações como uma verificação retornarão vários itens em $context.result.items
, onde será necessário executar o filtro de autorização e retornar apenas os resultados que o usuário tem permissão para ver. Digamos que o campo Owner
tinha o IdentityID do Amazon Cognito dessa vez definido no registro, então você poderia usar o seguinte modelo de mapeamento da resposta para filtrar a exibição somente dos registros de propriedade do usuário:
#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: várias pessoas podem fazer leitura
Outra opção de autorização popular é permitir que um grupo de pessoas seja capaz de ler dados. No exemplo abaixo, o "filter":{"expression":...}
retorna apenas valores de uma verificação de tabela se o usuário que executa a consulta do GraphQL estiver listado no conjunto de 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: o grupo pode fazer leitura
Semelhante ao último caso de uso, pode ser que apenas as pessoas em um ou mais grupos tenham direitos para ler determinados itens em um banco de dados. O uso da operação "expression":
"contains()"
é semelhante, no entanto é um OU lógico de todos os grupos dos quais um usuário pode fazer parte, o que precisa ser considerado na associação definida. Nesse caso, acumulamos uma instrução $expression
abaixo para cada grupo em que o usuário está e, em seguida, enviamos isso ao 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) } }
Gravação de dados
A gravação de dados em mutações sempre é controlada no modelo de mapeamento da solicitação. No caso de fontes de dados do DynamoDB, a chave é usar um "condition":{"expression"...}"
adequado que executa a validação nos metadados de autorização nessa tabela. Em Segurança, fornecemos um exemplo que pode ser usado para verificar o campo Author
em uma tabela. Os casos de uso nessa seção exploram mais casos de uso.
Caso de uso: vários proprietários
Usando a tabela de exemplo do diagrama anterior, suponha que a 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: o grupo pode criar um novo registro
#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: o grupo pode atualizar um 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 e privados
Com os filtros condicionais você também pode optar por marcar os dados como privado, público ou alguma outra verificação Booliana. Isso pode ser combinado como parte de um filtro de autorização dentro do modelo da resposta. Usar essa verificação é uma boa maneira de ocultar temporariamente os dados ou removê-los da visualização sem tentar controlar a associação de grupo.
Por exemplo, suponha que você adicionou um atributo em cada item na tabela do DynamoDB chamada public
com um valor de yes
ou no
. O seguinte modelo da resposta pode ser usado em uma chamada GetItem
para exibir os dados somente se o usuário estiver em um grupo que tem acesso E se esses dados estiverem marcados como público:
#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
O código acima também pode usar um OU lógico (||
) para permitir que as pessoas façam leitura se tiverem permissão para um registro ou se for público:
#if($hasPermission || $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end
No geral, os operadores padrão ==
, !=
, &&
e ||
serão úteis ao executar verificações de autorização.
Dados em tempo real
Você pode aplicar Controles de acesso refinados em assinaturas do GraphQL no momento em que um cliente faz uma assinatura, usando as mesmas técnicas descritas anteriormente nessa documentação. Anexe um resolvedor ao campo de assinatura, no momento em que pode consultar dados de uma fonte de dados e realizar a lógica condicional nos modelos de mapeamento da solicitação ou da resposta. Você também pode retornar dados adicionais para o cliente, como os resultados iniciais de uma assinatura, desde que a estrutura de dados corresponda àquela do tipo retornado na assinatura do GraphQL.
Caso de uso: o usuário pode assinar apenas conversas específicas
Um caso de uso comum para dados em tempo real com assinaturas do GraphQL é a criação de um aplicativo mensagens ou bate-papo privado. Ao criar um aplicativo de bate-papo com vários usuários, as conversas podem ocorrer entre duas pessoas ou entre várias pessoas. Elas podem ser agrupadas em "salas", privadas ou públicas. Dessa forma, você deseja autorizar apenas um usuário para assinar uma conversa (que pode ser um a um ou entre um grupo) à qual terão o acesso concedido. Para fins de demonstração, o exemplo abaixo mostra um caso de uso simples de um usuário que envia uma mensagem privada para outro. A configuração tem duas tabelas do Amazon DynamoDB:
-
Tabela de mensagens: (chave primária)
toUser
, (chave de classificação)id
-
Tabela de permissões: (chave primária)
username
A tabela de Mensagens armazena as mensagens que realmente foram enviadas por meio de uma mutação do GraphQL. A tabela de Permissões é verificada pela assinatura do GraphQL para autorização no momento da conexão do cliente. O exemplo abaixo pressupõe que você está usando o seguinte esquema do 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 }
Algumas das operações padrão, como createUserPermissions()
, não são abordadas abaixo para ilustrar os resolvedores de assinatura, mas são implementações padrão de resolvedores do DynamoDB. Em vez disso, vamos nos concentrar nos fluxos de autorização da assinatura com resolvedores. Para enviar uma mensagem de um usuário para outro, anexe um resolvedor ao campo sendMessage()
e selecione a fonte de dados da tabela de Mensagens com o seguinte modelo da solicitação:
{ "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), } }
Neste exemplo, usamos $context.identity.username
. Isso retorna as informações de usuário para usuários do AWS Identity and Access Management ou do Amazon Cognito. O modelo da resposta é uma simples passagem de $util.toJson($ctx.result)
. Salvar e voltar à página do esquema. Em seguida, anexe um resolvedor para a assinatura newMessage()
, usando a tabela de Permissões como uma fonte de dados e o seguinte modelo de mapeamento da solicitação:
{ "version": "2018-05-29", "operation": "GetItem", "key": { "username": $util.dynamodb.toDynamoDBJson($ctx.identity.username), }, }
Em seguida, use o seguinte modelo de mapeamento da resposta para executar as verificações de autorização usando os dados da tabela de Permissões:
#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
Nesse caso, você está fazendo três verificações de autorização. A primeira garante que um resultado seja retornado. A segunda garante que o usuário não esteja assinando mensagens destinadas a outra pessoa. A terceira garante que o usuário tenha permissão para assinar qualquer campo, verificando um atributo do DynamoDB de isAuthorizedForSubscriptions
armazenado como um BOOL
.
Para fazer testes, faça login no console do AWS AppSync usando grupos de usuários do Amazon Cognito e um usuário chamado "Nadia", depois, execute a seguinte assinatura do GraphQL:
subscription AuthorizedSubscription { newMessage(toUser: "Nadia") { id toUser fromUser content } }
Se, na tabela de Permissões, houver um registro para o atributo chave username
de Nadia
com isAuthorizedForSubscriptions
definido como true
, você verá uma resposta bem-sucedida. Se tentar um username
diferente na consulta newMessage()
acima, um erro será retornado.