

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

# リクエストとレスポンスを保護するためのアクセスコントロールのユースケース
<a name="security-authorization-use-cases"></a>

「[セキュリティ](security-authz.md#aws-appsync-security)」セクションでは、API を保護するためのさまざまな認可モードについて説明し、きめ細かな認可メカニズムの概念と流れについて紹介します。 AWS AppSync では、GraphQL Resolver [Mapping テンプレート](resolver-mapping-template-reference-overview.md#aws-appsync-resolver-mapping-template-reference-overview)を使用してデータに対してロジックフルオペレーションを実行できるため、ユーザー ID、条件、データインジェクションの組み合わせを使用して、読み取りまたは書き込み時のデータを非常に柔軟に保護できます。

 AWS AppSync Resolver の編集に慣れていない場合は、[プログラミングガイド](resolver-mapping-template-reference-programming-guide.md#aws-appsync-resolver-mapping-template-reference-programming-guide)を参照してください。

## 概要:
<a name="overview"></a>

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

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

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

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

## データの読み込み
<a name="reading-data"></a>

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

![\[DynamoDB table with ID, Data, PeopleCanAccess, GroupsCanAccess, and Owner columns.\]](http://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/images/auth.png)


プライマリキーは `id` であり、アクセスするデータは `Data` です。その他の列は、認可のために実行できるチェックの例です。「[DynamoDB のリゾルバーのマッピングテンプレートリファレンス](resolver-mapping-template-reference-dynamodb.md#aws-appsync-resolver-mapping-template-reference-dynamodb)」に概説されている通り、`Owner` は `String` になり、`PeopleCanAccess` と `GroupsCanAccess` は `String Sets` になります。

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

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

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

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

### ユースケース: 所有者が読み取り可能
<a name="use-case-owner-can-read"></a>

上記のテーブルを使用して、個々の読み取りオペレーション (`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」値などがあり、「[リゾルバーのマッピングテンプレートのコンテキストリファレンス](resolver-context-reference.md#aws-appsync-resolver-mapping-template-context-reference)」で、利用可能なオプションを確認しておく必要があります。

次に、`$util.unauthorized()` に対応する条件付き else チェックは、完全に省略可能ですが、GraphQL API を設計する際のベストプラクティスとして、指定することお勧めします。

### ユースケース: 特定のアクセス権のハードコード
<a name="use-case-hardcode-specific-access"></a>

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

### ユースケース: 結果リストのフィルタリング
<a name="use-case-filtering-a-list-of-results"></a>

前の例では単一の項目が返されるため、`$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)
```

### ユースケース: 複数のユーザーが読み取り可能
<a name="use-case-multiple-people-can-read"></a>

もう 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)
        }
    }
}
```

### ユースケース: グループが読み取り可能
<a name="use-case-group-can-read"></a>

直前のユースケースと同様に、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)
    }
}
```

## データの書き込み
<a name="writing-data"></a>

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

### ユースケース: 複数の所有者
<a name="use-case-multiple-owners"></a>

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

### ユースケース: グループが新規レコードを作成可能
<a name="use-case-group-can-create-new-record"></a>

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

### ユースケース: グループが既存レコードを更新可能
<a name="use-case-group-can-update-existing-record"></a>

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

## パブリックレコードとプライベートレコード
<a name="public-and-private-records"></a>

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

たとえば、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
```

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

## リアルタイムデータ
<a name="security-real-time-data"></a>

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

### ユースケース: ユーザーが特定の対話のみのサブスクライブ可能
<a name="use-case-user-can-subscribe-to-specific-conversations-only"></a>

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()` を試行すると、エラーが返されます。