Casi d'uso del controllo degli accessi per proteggere richieste e risposte - AWS AppSync

Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.

Casi d'uso del controllo degli accessi per proteggere richieste e risposte

Nella sezione Sicurezza sono state illustrate le diverse modalità di autorizzazione per proteggere i propri utenti API ed è stata fornita un'introduzione ai meccanismi di autorizzazione granulari per comprenderne i concetti e il flusso. Poiché AWS AppSync consente di eseguire operazioni logiche complete sui dati tramite l'uso di modelli GraphQL Resolver Mapping, è possibile proteggere i dati in lettura o scrittura in modo molto flessibile utilizzando una combinazione di identità utente, condizionali e data injection.

Se non hai dimestichezza con la modifica dei AWS AppSync Resolver, consulta la guida alla programmazione.

Panoramica

La concessione dell'accesso ai dati in un sistema viene tradizionalmente effettuata tramite una matrice di controllo degli accessi in cui l'intersezione di una riga (risorsa) e una colonna (utente/ruolo) rappresenta le autorizzazioni concesse.

AWS AppSync utilizza le risorse del proprio account e inserisce le informazioni sull'identità (utente/ruolo) nella richiesta e risposta GraphQL come oggetto di contesto, che è possibile utilizzare nel resolver. Ciò significa che le autorizzazioni possono essere concesse in modo appropriato per le operazioni di lettura o scrittura in base alla logica del resolver. Se questa logica è a livello di risorsa, ad esempio solo determinati utenti o gruppi denominati possono leggere/scrivere su una riga specifica del database, i «metadati di autorizzazione» devono essere archiviati. AWS AppSync non memorizza alcun dato, quindi è necessario archiviare questi metadati di autorizzazione con le risorse in modo da poter calcolare le autorizzazioni. I metadati di autorizzazione sono in genere un attributo (colonna) in una tabella DynamoDB, ad esempio un proprietario o un elenco di utenti/gruppi. Potrebbero ad esempio esserci gli attributi Readers e Writers.

Da un punto di vista generale, ciò significa che se stai leggendo una singola voce da un'origine dati, esegui un'istruzione #if () ... #end condizionale nel modello di risposta dopo che il resolver ha letto dall'origine dati. Il controllo usa in genere i valori di utenti o gruppi in $context.identity per i controlli di appartenenza in base ai metadati di autorizzazione restituiti da un'operazione di lettura. Per più record, ad esempio gli elenchi restituiti da una tabella Scan o Query, puoi inviare il controllo della condizione come parte dell'operazione all'origine dati usando valori di utenti o gruppi simili.

Analogamente, quando scrivi dati applichi un'istruzione condizionale all'operazione (ad esempio PutItem o UpdateItem) per verificare se l'utente o il gruppo che esegue la mutazione dispone di autorizzazione. Molto spesso l'istruzione condizionale usa un valore in $context.identity per eseguire il confronto in base ai metadati di autorizzazione nella risorsa. Per entrambi i modelli di richiesta e di risposta è anche possibile usare intestazioni personalizzate provenienti dai client per eseguire i controlli di convalida.

Lettura dei dati

Come illustrato in precedenza, i metadati di autorizzazione per eseguire un controllo devono essere archiviati con una risorsa o passati nella richiesta GraphQL (identità, intestazione e così via). Per illustrare questo concetto, supponi di avere la tabella DynamoDB seguente:

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

La chiave primaria è id e i dati a cui accedere corrispondono a Data. Le altre colonne sono esempi di controlli che è possibile eseguire per l'autorizzazione. Ownerrichiederebbe un String po' di tempo PeopleCanAccess e GroupsCanAccess sarebbe String Sets come indicato nel riferimento del modello di mappatura Resolver per DynamoDB.

Nella panoramica sui modelli di mappatura dei resolver il diagramma mostra che il modello di risposta contiene non solo l'oggetto context, ma anche i risultati dall'origine dati. Per le query GraphQL di singole voci, è possibile usare il modello di risposta per controllare se l'utente è autorizzato a visualizzare i risultati oppure restituire un messaggio di errore di autorizzazione. In questo caso si parla talvolta di "filtro di autorizzazione". Per le query GraphQL che restituiscono elenchi, usando un elemento Scan o Query, è preferibile eseguire il controllo nel modello di richiesta e restituire i dati solo se la condizione di autorizzazione viene soddisfatta. L'implementazione è quindi la seguente:

  1. GetItem - controllo delle autorizzazioni per i singoli record. Eseguito usando istruzioni #if() ... #end.

  2. Operazioni Scan/Query: il controllo di autorizzazione è un'istruzione "filter":{"expression":...}. I controlli comuni riguardano l'uguaglianza (attribute = :input) o la verifica della presenza di un valore in un elenco (contains(attribute, :input)).

Nel punto 2 l'elemento attribute in entrambe le istruzioni rappresenta il nome di colonna del record in una tabella, come Owner nell'esempio precedente. Puoi impostare un alias con un segno # e usare "expressionNames":{...}, ma non è obbligatorio. L'elemento :input è un riferimento al valore che stai confrontando con l'attributo di database, che definirai in "expressionValues":{...}. Gli esempi sono disponibili di seguito.

Caso d'uso: il proprietario può leggere

Usando la tabella precedente, se desideri restituire i dati solo se Owner == Nadia per una singola operazione di lettura (GetItem), il modello sarà analogo al seguente:

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

Ci sono alcuni aspetti da sottolineare che valgono anche per le sezioni successive. Innanzitutto, il controllo utilizza $context.identity.username il nome utente intuitivo per la registrazione se vengono utilizzati i pool di utenti di Amazon Cognito e l'identità dell'utente, IAM se utilizzato (incluse le identità federate di Amazon Cognito). Esistono altri valori da memorizzare per un proprietario, come il valore univoco «Amazon Cognito identity», utile per federare gli accessi da più sedi, e dovresti esaminare le opzioni disponibili nel Resolver Mapping Template Context Reference.

In secondo luogo, il controllo condizionale con cui rispondere $util.unauthorized() è completamente facoltativo ma consigliato come best practice per la progettazione di GraphQL. API

Caso d'uso: accesso specifico per codice rigido

// 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 d'uso: filtraggio di un elenco di risultati

Nell'esempio precedente è stato eseguito un controllo direttamente su $context.result, in quanto è stata restituita una singola voce, tuttavia alcune operazioni come una scansione restituiscono più voci in $context.result.items, quindi è necessario applicare il filtro di autorizzazione per restituire solo i risultati che l'utente può vedere. Supponiamo che questa volta Owner nel campo sia impostato l'IdentityID di Amazon Cognito nel record, quindi puoi utilizzare il seguente modello di mappatura delle risposte per filtrare in modo da mostrare solo i record di proprietà dell'utente:

#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 d'uso: più persone possono leggere

Un'altra opzione di autorizzazione comune consiste nel permettere a un gruppo di persone di leggere i dati. Nell'esempio seguente "filter":{"expression":...} restituisce i valori di una scansione di tabella solo se l'utente che esegue la query GraphQL è incluso nel set 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 d'uso: il gruppo può leggere

Analogamente all'ultimo caso d'uso, è possibile che solo le persone in uno o più gruppi abbiano i diritti per leggere determinate voci in un database. L'uso dell'operazione "expression": "contains()" è analogo, tuttavia nell'appartenenza a un set è necessario tenere conto dell'operatore logico OR che permette di includere tutti i gruppi di cui un utente potrebbe far parte. In questo caso, creiamo l'istruzione $expression seguente per ogni gruppo a cui appartiene l'utente e la passiamo 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) } }

Scrittura di dati

La scrittura di dati nelle mutazioni è sempre controllata nel modello di mappatura della richiesta. Nel caso delle origini dati DynamoDB, la chiave consiste nell'usare un elemento "condition":{"expression"...}" appropriato che esegue la convalida in base ai metadati di autorizzazione nella tabella. In Sicurezza, abbiamo fornito un esempio che è possibile utilizzare per controllare il campo Author in una tabella. In questa sezione vengono esaminati altri casi d'uso.

Caso d'uso: più proprietari

Usando il diagramma della tabella di esempio precedente, presupponi l'elenco 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 d'uso: il gruppo può creare un nuovo record

#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 d'uso: il gruppo può aggiornare il record esistente

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

Record pubblici e privati

Con i filtri condizionali, è anche possibile scegliere di contrassegnare i dati come privati, pubblici o con altri valori booleani. Questa impostazione può quindi essere integrata in un filtro di autorizzazione all'interno del modello di risposta. Usando questo controllo, è possibile nascondere temporaneamente i dati o rimuoverli dalla visualizzazione senza tentare di controllare l'appartenenza a un gruppo.

Supponi, ad esempio, di aggiungere un attributo in ogni voce della tabella DynamoDB chiamata public con un valore yes o no. Il seguente modello di risposta può essere utilizzato in una GetItem chiamata per visualizzare i dati solo se l'utente fa parte di un gruppo che ha accesso AND se tali dati sono contrassegnati come pubblici:

#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

Nel codice precedente è anche possibile usare un operatore OR logico (||) per permettere agli utenti la lettura se dispongono dell'autorizzazione per un record o se il record è pubblico:

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

In generale, gli operatori standard ==, !=, && e ||, sono utili per i controlli di autorizzazione.

Dati in tempo reale

È possibile applicare controlli di accesso granulari alle sottoscrizioni GraphQL nel momento in cui un client esegue una sottoscrizione, usando le stesse tecniche descritte in precedenza in questa documentazione. È possibile collegare un resolver al campo della sottoscrizione e quindi è possibile eseguire query sui dati di un'origine dati e applicare la logica condizionale nel modello di mappatura della richiesta o della risposta. Puoi anche restituire dati aggiuntivi al client, ad esempio i risultati iniziali di una sottoscrizione, a condizione che la struttura dei dati corrisponda a quella del tipo restituito nella sottoscrizione GraphQL.

Caso d'uso: l'utente può iscriversi solo a conversazioni specifiche

Un caso d'uso comune per i dati in tempo reale con le sottoscrizioni GraphQL consiste nella creazione di un'applicazione di messaggistica o di chat privata. Quando crei un'applicazione di chat con più utenti, le conversazioni possono avvenire tra due persone o tra più persone. Gli utenti possono essere raggruppati in "stanze", private o pubbliche. In questo caso, è necessario autorizzare un utente a eseguire solo la sottoscrizione di una conversazione (con una sola persona o con un gruppo) per la quale gli è stato concesso l'accesso. A scopo illustrativo, nell'esempio seguente è rappresentato un caso d'uso semplice di un utente che invia un messaggio privato a un altro utente. La configurazione ha due tabelle Amazon DynamoDB:

  • Tabella Messages: (chiave primaria) toUser, (chiave di ordinamento) id

  • Tabella Permissions: (chiave primaria) username

La tabella Messages archivia i messaggi effettivi inviati tramite una mutazione GraphQL. La tabella Permissions viene controllata dalla sottoscrizione GraphQL per verificare le autorizzazioni al momento della connessione client. L'esempio seguente presuppone che si stia usando lo schema GraphQL illustrato di seguito:

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 }

Alcune delle operazioni standard, ad esempio, non sono illustrate di seguito per illustrare i resolver in abbonamento, ma sono implementazioni standard dei resolver DynamoDB. createUserPermissions() Vengono invece analizzati i flussi di autorizzazione della sottoscrizione con i resolver. Per inviare un messaggio da un utente a un altro, collega un resolver al campo sendMessage() e seleziona l'origine dati della tabella Messages con il modello di richiesta seguente:

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

In questo esempio viene utilizzato $context.identity.username. Ciò restituisce le informazioni sugli utenti AWS Identity and Access Management o per gli utenti di Amazon Cognito. Il modello di risposta è un passthrough semplice di $util.toJson($ctx.result). Salva e torna alla pagina dello schema. Collega quindi un resolver alla sottoscrizione newMessage(), usando la tabella Permissions come origine dati e il modello di mappatura della richiesta seguente:

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

Usa quindi il modello di mappatura della risposta seguente per eseguire i controlli di autorizzazione con i dati della tabella 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

In questo caso, esegui tre controlli di autorizzazione. Il primo garantisce che venga restituito un risultato. Il secondo garantisce che l'utente non esegua la sottoscrizione di messaggi destinati a un'altra persona. Il terzo assicura che l'utente sia autorizzato a sottoscrivere qualsiasi campo, controllando un attributo DynamoDB isAuthorizedForSubscriptions di stored as a. BOOL

Per testare le cose, puoi accedere alla AWS AppSync console utilizzando i pool di utenti di Amazon Cognito e un utente chiamato «Nadia», quindi eseguire il seguente abbonamento GraphQL:

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

Se nella tabella Autorizzazioni c'è un record per l'attributo chiave username corrispondente a Nadia con valore di isAuthorizedForSubscriptions impostato su true, l'operazione avrà esito positivo. Se provi a usare un valore di username diverso nella query newMessage() precedente, verrà restituito un errore.