リクエストとレスポンスを保護するためのアクセスコントロールのユースケース - AWS AppSync

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

リクエストとレスポンスを保護するためのアクセスコントロールのユースケース

セキュリティセクションでは、 を保護するためのさまざまな認可モードについて学びAPI、概念とフローを理解するためのきめ細かな認可メカニズムについて紹介しました。 AWS AppSync では、GraphQL Resolver Mapping テンプレート を使用してデータに対してロジックフルオペレーションを実行できるため、ユーザー ID、条件、データインジェクションの組み合わせを使用して、読み取りまたは書き込み時のデータを非常に柔軟に保護できます。

AWS AppSync リゾルバーの編集に慣れていない場合は、プログラミングガイド を確認してください。

概要

システム内のデータへのアクセスの付与は、従来、行 (リソース) と列 (ユーザーまたはロール) の交点が付与されるアクセス許可である アクセスコントロールマトリックス を通じて行われています。

AWS AppSync は、独自のアカウントのリソースを使用し、アイデンティティ (ユーザー/ロール) 情報をコンテキストオブジェクト として GraphQL リクエストとレスポンスにスレッドします。これはリゾルバーで使用できます。つまり、リゾルバーのロジックに基づいて、書き込みまたは読み取りのオペレーションに対して適切にアクセス権限を付与できます。このロジックがリソースレベルである場合、例えば、特定の名前付きユーザーまたはグループのみが特定のデータベース行に読み書きできる場合、その「承認メタデータ」を保存する必要があります。 AWS AppSync はデータを保存しないため、アクセス許可を計算できるように、この承認メタデータをリソースに保存する必要があります。認証メタデータは通常、DynamoDB テーブル内の属性 (列) であり、所有者、ユーザーまたはグループのリストなどです。たとえば、ReadersWriters 属性があります。

ハイレベルでは、データソースから個々の項目を読み取っている場合、リゾルバーがデータソースから読み取った後に、レスポンステンプレートで条件ステートメント #if () ... #end を実行します。そのチェックでは通常、読み取りオペレーションから返された認証メタデータに対するメンバーシップ確認のために、$context.identity の user または group の値が使用されます。複数のレコードがある (テーブルの ScanQuery で返されるリストなど) 場合は、同様の user または group の値を使用して、データソースに対するオペレーションの一部として条件チェックを送信します。

同様に、データを書き込む場合は、アクション (ミューテーションを作成するユーザーやグループにアクセス権限があるかどうかを確認する PutItemUpdateItem など) に対して条件ステートメントを適用します。この条件チェックでは、$context.identity 内の値を使用して何度も行われ、リソースの認証メタデータと比較されます。リクエストテンプレートとレスポンステンプレートの両方で、クライアントからのカスタムヘッダーを使用して検証チェックを実行することもできます。

データの読み込み

前述のように、チェックを実行するための認証メタデータは、リソースに保存されているかまたは GraphQL リクエスト (ID、ヘッダーなど) で渡される必要があります。その例を示すために、次の DynamoDB テーブルがあるとします。

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

プライマリキーは id であり、アクセスするデータは Data です。その他の列は、認証のために実行できるチェックの例です。「DynamoDB のリゾルバーのマッピングテンプレートリファレンス」に概説されている通り、OwnerString になり、PeopleCanAccessGroupsCanAccessString Sets になります。

リゾルバーのマッピングテンプレートの概要」の図に示しているように、レスポンステンプレートには、コンテキストオブジェクトだけでなくデータソースからの結果も含まれています。個々の項目に対する GraphQL クエリでは、レスポンステンプレートを使用して、そのユーザーが結果を確認することを許可されているかどうかをチェックし、そうでない場合は認証エラーメッセージを返すことができます。これは、「認証フィルタ」と呼ばれることもあります。Scan または Query を使用してリストを返す GraphQL クエリでは、リクエストテンプレートでチェックを実行し、認証条件が満たされている場合にのみデータを返すのが、より効率的です。次のように実装します。

  1. GetItem - 個々のレコードの認証チェック。#if() ... #end ステートメントを使用して行われます。

  2. Scan/Query オペレーション - 認証チェックは "filter":{"expression":...} ステートメントです。よく使用されるチェックは、等価チェック (attribute = :input)、または値がリストあるかどうかのチェック (contains(attribute, :input)) です。

上記の 2 で両方のステートメントにある attribute は、テーブル内のレコードの列名 (上記の例では Owner など) を表しています。そのエイリアスとして # 記号と "expressionNames":{...} を使用できますが、必須ではありません。:input は、データベース属性と比較する値への参照であり、"expressionValues":{...} で定義します。その例を以下に示します。

ユースケース: 所有者が読み取り可能

上記のテーブルを使用して、個々の読み取りオペレーション (Owner == Nadia) で GetItem であるときにのみデータを返す場合、テンプレートは次のようになります。

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

ここで、以降のセクションで再利用する点について説明しておきます。まず、チェックでは、Amazon Cognito ユーザープールが使用されている場合は $context.identity.usernameをわかりやすいユーザーサインアップ名として使用し、 IAMが使用されている場合はユーザーアイデンティティ (Amazon Cognito フェデレーティッドアイデンティティを含む) として使用します。所有者に対して保存されるその他の値として、複数のロケーションからフェデレーテッドログインする場合に便利な一意の「Amazon Cognito ID」値などがあり、「リゾルバーのマッピングテンプレートのコンテキストリファレンス」で、利用可能なオプションを確認しておく必要があります。

2 つ目$util.unauthorized()は、 で応答する条件付きその他チェックは完全にオプションですが、GraphQL を設計する際のベストプラクティスとして推奨されますAPI。

ユースケース: 特定のアクセス権のハードコード

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

ユースケース: 結果リストのフィルタリング

前の例では単一の項目が返されるため、$context.result に対してチェックを直接実行することもできましたが、スキャンなどの一部のオペレーションでは $context.result.items で複数の項目が返されるため、認証フィルタを実行して、そのユーザーが確認を許可されている結果のみを返す必要があります。たとえば、今度はレコードに設定されている Amazon Cognito IdentityID が Owner フィールドにあるとすると、次のレスポンスマッピングテンプレートを使用して、そのユーザーが所有しているレコードのみを示すようにフィルタリングできます。

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

ユースケース: 複数のユーザーが読み取り可能

もう 1 つのよくある認証オプションは、ユーザーのグループがデータを読み取ることができるように許可することです。次の例の "filter":{"expression":...} では、GraphQL クエリを実行しているユーザーが 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) } } }

ユースケース: グループが読み取り可能

直前のユースケースと同様に、1 つまたは複数のグループに属するユーザーのみが、データベース内の特定の項目を読み取る権限を持っているとします。"expression": "contains()" オペレーションを使用するのは同じですが、設定されているメンバーシップに属している必要があるすべてのグループの論理 OR です。この例では、ユーザーが属している各グループに対して次の $expression ステートメントを作成し、それをフィルタに渡します。

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

データの書き込み

ミューテーションでのデータの書き込みは、常にリクエストマッピングテンプレートで制御されます。DynamoDB のデータソースの場合、key は、そのテーブル内の認証メタデータに対して検証を実行する適切な "condition":{"expression"...}" が使用されます。「セキュリティ」には、テーブル内の Author フィールドのチェックに役立つ例があります。このセクションでは、その他のユースケースを示します。

ユースケース: 複数の所有者

以前の例のテーブル図を使用した、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) } } }

ユースケース: グループが新規レコードを作成可能

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

ユースケース: グループが既存レコードを更新可能

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

パブリックレコードとプライベートレコード

条件フィルターを使用すると、データをプライベート、パブリック、またはその他のブール型チェックとしてマークすることもできます。それを認証フィルタの一部としてレスポンステンプレート内に組み込むことができます。このチェックを使用すると、グループメンバーシップを制御することなく、データを一時的に隠したり、ビューから除外したりできます。

たとえば、DynamoDB テーブル内の各項目に、yes または no のいずれかの値を持つ public という属性を追加するとします。次のレスポンステンプレートは、ユーザーがアクセス可能なグループに属し、そのデータがパブリックとしてマークANDされている場合にのみデータを表示するためのGetItem呼び出しに使用できます。

#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

また、上記のコードで論理 OR (||) を使用すると、ユーザーにレコードへのアクセス許可があるか、または、レコードがパブリックである場合に、そのユーザーに読み取りを許可できます。

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

通常、認証チェックを実行する際に、標準的な演算子 ==!=&&、および || が役立ちます。

リアルタイムデータ

クライアントがサブスクリプションを作成したときに、このドキュメントで前述したのと同じ手法で、きめ細かなアクセス制御コントロールを GraphQL のサブスクリプションに適用できます。サブスクリプションフィールドにリゾルバーをアタッチすると、そのポイントで、データソースからデータをクエリし、リクエストまたはレスポンスのいずれかのマッピングテンプレートで条件ロジックを実行できます。そのデータ構造が、GraphQL サブスクリプションで返される型と一致している限り、追加のデータ (サブスクリプションからの初期結果など) をクライアントに返すこともできます。

ユースケース: ユーザーが特定の対話のみのサブスクライブ可能

GraphQL サブスクリプションを使用したリアルタイムデータのよくあるユースケースは、メッセージングやプライベートチャットのアプリケーションを構築することです。複数のユーザーに対応したチャットアプリケーションを作成する場合、2 人または複数ユーザーの間で対話が行われます。ユーザーは、プライベートまたはパブリックの「ルーム」にグループ化されます。したがって、ユーザーにアクセス権が付与されている対話 (1 対 1 またはグループ間の) をサブスクライブする 1 人のユーザーのみを認証します。デモの目的で、以下の単純な例では、1 人のユーザーが別のユーザーにプライベートのメッセージを送信するユースケースを示します。次の 2 つの Amazon DynamoDB テーブルをセットアップします。

  • Messages テーブル: (プライマリキー) toUser、(ソートキー) id

  • Permissions テーブル: (プライマリキー) username

Messages テーブルには、GraphQL ミューテーション経由で送信される実際のメッセージが保存されます。Permissions テーブルは、クライアントの接続時に認証のために GraphQL サブスクリプションによってチェックされます。以下の例では、次の 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 }

以下では、サブスクリプションリゾルバーを示すために、一部の標準オペレーション (createUserPermissions() など) は取り上げていませんが、DynamoDB リゾルバーで標準実装されています。代わりに、リゾルバーでのサブスクリプションの認証フローを中心に説明します。ユーザー間でメッセージを送信するには、sendMessage() フィールドにリゾルバーをアタッチし、次のリクエストテンプレートを使用して Messages テーブルデータソースを選択します。

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

この例では、$context.identity.username を使用します。これにより、 AWS Identity and Access Management または Amazon Cognito ユーザーのユーザー情報が返されます。レスポンステンプレートは、$util.toJson($ctx.result) の単純なパススルーです。保存してスキーマページに戻ります。次に、newMessage()Permissions テーブルをデータソースとして使用し、次のリクエストマッピングテンプレートを使用して、 サブスクリプションにリゾルバーをアタッチします。

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

次のレスポンスマッピングテンプレートを使用し、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

この例の場合、3 つの認証チェックを実行しています。1 つ目は、結果が返されることを確認します。2 つ目は、そのユーザーが別のユーザーに対するメッセージをサブスクライブしていないことを確認しています。3 番目のチェックでは、isAuthorizedForSubscriptions として保存されている BOOL の DynamoDB 属性をチェックすることで、そのユーザーが任意のフィールドへのサブスクライブを許可されていることを確認しています。

モノをテストするには、Amazon Cognito ユーザープールと「Nadia」という名前のユーザーを使用して AWS AppSync コンソールにサインインし、次の GraphQL サブスクリプションを実行します。

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

Permissions テーブルに、Nadiausername キー属性に対して isAuthorizedForSubscriptionstrue に設定されているレコードがある場合は、正常なレスポンスが表示されます。上記の username クエリで別の newMessage() を試行すると、エラーが返されます。