リクエストとレスポンスを保護するためのアクセスコントロールのユースケース
「セキュリティ」セクションでは、API を保護するためのさまざまな認可モードについて説明し、きめ細かな認可メカニズムの概念と流れについて紹介します。AWS AppSync では、GraphQL リゾルバーの マッピングテンプレート を使用して、ユーザーがデータに対して完全なオペレーションを実行できるため、ユーザー ID、条件、およびデータインジェクションを組み合わせて使用して非常に柔軟な方法で、データの読み取り時または書き込み時にデータを保護できます。
AWS AppSync のリゾルバーの編集に慣れていない場合は、「プログラミングガイド」を参照してください。
概要
システム内のデータへのアクセスの付与は、従来、行 (リソース) と列 (ユーザーまたはロール) の交点が付与されるアクセス許可である アクセスコントロールマトリックス
AWS AppSync では、お客様自身のアカウント内のリソースを使用し、ID (ユーザーまたはロール) 情報を GraphQL のリクエストおよびレスポンスにコンテキストオブジェクトとして渡すため、それをリゾルバーで使用できます。つまり、リゾルバーのロジックに基づいて、書き込みまたは読み取りのオペレーションに対して適切にアクセス権限を付与できます。例えば、そのロジックがリソースレベルである場合は、特定の名前のユーザーまたはグループのみが、「認可メタデータ」が保存される必要がある特定のデータベース行に対して読み取りおよび書き込みできます。AWSAppSync ではデータは一切保存されないため、ユーザーは、アクセス権限を計算できるようにリソースを使用してその認可メタデータを保存する必要があります。認可メタデータは通常、DynamoDB テーブル内の属性 (列) であり、所有者、ユーザーまたはグループのリストなどです。たとえば、Readers と Writers 属性があります。
ハイレベルでは、データソースから個々の項目を読み取っている場合、リゾルバーがデータソースから読み取った後に、レスポンステンプレートで条件ステートメント #if () ... #end
を実行します。そのチェックでは通常、読み取りオペレーションから返された認可メタデータに対するメンバーシップ確認のために、$context.identity
の user または group の値が使用されます。複数のレコードがある (テーブルの Scan
や Query
で返されるリストなど) 場合は、同様の user または group の値を使用して、データソースに対するオペレーションの一部として条件チェックを送信します。
同様に、データを書き込む場合は、アクション (ミューテーションを作成するユーザーやグループにアクセス権限があるかどうかを確認する PutItem
や UpdateItem
など) に対して条件ステートメントを適用します。この条件チェックでは、$context.identity
内の値を使用して何度も行われ、リソースの認可メタデータと比較されます。リクエストテンプレートとレスポンステンプレートの両方で、クライアントからのカスタムヘッダーを使用して検証チェックを実行することもできます。
データの読み込み
前述のように、チェックを実行するための認可メタデータは、リソースに保存されているかまたは GraphQL リクエスト (ID、ヘッダーなど) で渡される必要があります。その例を示すために、次の DynamoDB テーブルがあるとします。
プライマリキーは id
であり、アクセスするデータは Data
です。その他の列は、認可のために実行できるチェックの例です。「DynamoDB のリゾルバーのマッピングテンプレートリファレンス」に概説されている通り、Owner
は String
になり、PeopleCanAccess
と GroupsCanAccess
は String Sets
になります。
「リゾルバーのマッピングテンプレートの概要」の図に示しているように、レスポンステンプレートには、コンテキストオブジェクトだけでなくデータソースからの結果も含まれています。個々の項目に対する GraphQL クエリでは、レスポンステンプレートを使用して、そのユーザーが結果を確認することを許可されているかどうかをチェックし、そうでない場合は認可エラーメッセージを返すことができます。これは、「認可フィルタ」と呼ばれることもあります。Scan または Query を使用してリストを返す GraphQL クエリでは、リクエストテンプレートでチェックを実行し、認可条件が満たされている場合にのみデータを返すのが、より効率的です。次のように実装します。
-
GetItem - 個々のレコードに対する認可チェック。
#if() ... #end
ステートメントを使用して行われます。 -
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
ここで、以降のセクションで再利用する点について説明しておきます。まず、チェックで使用される $context.identity.username
は、Amazon Cognito ユーザープールが使用されている場合は分かりやすいユーザーサインアップ名であり、IAM (Amazon Cognito フェデレーテッドアイデンティティも含む) が使用されている場合はユーザー ID です。所有者に対して保存されるその他の値として、複数のロケーションからフェデレーテッドログインする場合に便利な一意の「Amazon Cognito ID」値などがあり、「リゾルバーのマッピングテンプレートのコンテキストリファレンス」で、利用可能なオプションを確認しておく必要があります。
次に、$util.unauthorized()
に対応する条件付き else チェックは、完全に省略可能ですが、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
という属性を追加するとします。次のレスポンステンプレートを 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 テーブルに、Nadia
の username
キー属性に対して isAuthorizedForSubscriptions
が true
に設定されているレコードがある場合は、正常なレスポンスが表示されます。上記の username
クエリで別の newMessage()
を試行すると、エラーが返されます。