Casos de uso de controle de acesso para proteger solicitações e respostas - AWS AppSync

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 seu API e foi apresentada uma introdução aos mecanismos de autorização refinados para entender os conceitos e o fluxo. Como AWS AppSync permite que você execute operações lógicas completas nos dados por meio do uso dos modelos de mapeamento do GraphQL Resolver, você pode proteger os dados na leitura ou gravação de uma maneira muito flexível usando uma combinação de identidade do usuário, condicionais e injeção de dados.

Se você não estiver familiarizado com a edição de AWS AppSync resolvedores, consulte 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 em que a interseção de uma linha (recurso) e uma coluna (usuário/função) são as permissões concedidas.

AWS AppSync usa recursos em sua própria conta e insere informações de identidade (usuário/função) na solicitação e resposta do GraphQL como um objeto de contexto, que você pode usar 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 estiver no nível do recurso, por exemplo, somente determinados usuários ou grupos nomeados podem ler/gravar em uma linha específica do banco de dados, então esses “metadados de autorização” devem ser armazenados. AWS AppSync não armazena nenhum dado, portanto, você deve 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:

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

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 é:

  1. GetItem - verificação de autorização para registros individuais. Feita usando instruções #if() ... #end.

  2. 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 qual será o nome de cadastro amigável do usuário se os grupos de usuários do Amazon Cognito forem usados e será a identidade do usuário IAM se for usada (incluindo 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 que responde com $util.unauthorized() é completamente opcional, mas recomendada como uma prática recomendada ao projetar seu GraphQL. API

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 modelo de resposta a seguir pode ser usado em uma GetItem chamada para exibir dados somente se o usuário estiver em um grupo que tenha acesso AND se esses dados estiverem 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

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 informações do usuário AWS Identity and Access Management ou dos usuários 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 testar as coisas, você pode entrar no AWS AppSync console usando grupos de usuários do Amazon Cognito e um usuário chamado “Nadia” e, em seguida, executar 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.