AWS Amplify フレームワークの使い方Part6〜GraphQL Transform @auth編〜

# はじめに
Amplifyでの肝の部分であるAPI(AppSync = GraphQL)のスキーマ設計の第二弾として、権限設定の`@auth`の解説を行っていきます。

# 定義
公式ドキュメントに掲載されている定義には、どんな設定ができるのかが詰まっているので、要チェックです。(そして気づいたら更新されていたりするので注意！！)
この定義を使いこなすことで、より細かい権限設定が可能となります。`@auth`についてこの記事で初めて知る方は、この定義を読むとわかりにくいので、ここはとばして次の解説から読んでもらった後にこの定義を読むことをお勧めします。

```
directive @auth(rules: [AuthRule!]!) on OBJECT, FIELD_DEFINITION
input AuthRule {
    allow: AuthStrategy!
    provider: AuthProvider
    ownerField: String # defaults to "owner" when using owner auth
    identityClaim: String # defaults to "username" when using owner auth
    groupClaim: String # defaults to "cognito:groups" when using Group auth
    groups: [String]  # Required when using Static Group auth
    groupsField: String # defaults to "groups" when using Dynamic Group auth
    operations: [ModelOperation] # Required for finer control

    # The following arguments are deprecated. It is encouraged to use the 'operations' argument.
    queries: [ModelQuery]
    mutations: [ModelMutation]
}
enum AuthStrategy { owner groups private public }
enum AuthProvider { apiKey iam oidc userPools }
enum ModelOperation { create update delete read }

# The following objects are deprecated. It is encouraged to use ModelOperations.
enum ModelQuery { get list }
enum ModelMutation { create update delete }
```
## allow：権限設定の対象
現状の製作中のアプリでは、ownerとgroupsを多用しています。最近(2019年中頃？)、privateとpublicが追加され、まだ利用はしていませんが、活用の幅が広がっています。

- owner  
データの所有者(Cognitoの認証ユーザー)に対しての権限設定
- groups  
データのgroup(Cognito内のグループ)に属するユーザーとそれ以外に対しての権限設定
- private  
Cognitoの認証ユーザーに対しての権限設定(調査していないので、おそらく)
- public  
Cognitoの未認証ユーザー調に対しての権限設定。(調査していないので、おそらく)


## provider：認証の方法
以下の4パターンがあるようですが、現在作成中のアプリではとくに設定したことがないです。APIの初期設定時に、userPoolsでの認証をデフォルトにしているので、おそらく各`@auth`ごとに設定しなくても自動的にuserPoolsになっているものと思われます。

- apiKey：APIキー
- iam：iamユーザー
- oidc：Open ID Connect
- userPools：Cognito User Pools

## ownerFiled：owner情報を入れるカラム指定
権限設定の対象をownerにした場合に宣言し、対象のレコードの所有者のid情報が入るカラムを指定します。指定しない場合は、ownerが自動で入ります。

## groups：ユーザープールのグループの指定
権限設定の対象をgroupsにした場合に宣言し、ユーザープールに作成されているグループを指定します。デフォルト値はないので必ず指定が必要です。

## groupsFiled：groups情報を入れるカラム指定
権限設定の対象をgroupsに設定し、かつ動的なグループ設定をしたい場合に宣言します。指定したカラムに、ユーザープールのグループを配列で書き込むことで、動的にグループに権限を付与することができます。

## identityClaim：owner認証のための属性情報を指定(cognito)
ownerFiledに書き込まれたidをどのユーザープールの情報を使って認証するかを指定します。デフォルト値はusernameです。例えば、cognitoに登録したuserameが`tanaka`の場合、ownerFiledに指定したカラムに`tanaka`の文字列が入っている時のみ、指定された権限を有することができます。

## groupClaim：groups認証のための情報を指定(cognito)
デフォルトは`cognito:groups`になっており、cognitoのグループを見に行くようになっています。詳しく調べていませんが、指定ができるということは、cognitoのグループ機能以外もつかうことができる、ということだと思うので、色々使える用途はありそうです。

## operations：対象者のCRUDの制限を指定
CRUDの権限指定である肝の部分です。若干癖があるので、ここでは簡単に概要を説明し、後ほど細かくご説明します。指定しない場合は、デフォルトでcreate、update、delete、readのすべてが指定されます。

- read：対象者のみ読み取り可能にする
- create：ownerFiledの値にログインユーザーの情報を自動入力してくれる。(groupsの場合は、対象のgroupに属する場合のみcreateが可能)
- update：対象者のみが更新可能にする
- dalate：対象者のみが削除可能にする


# 解説
公式ドキュメントの内容をもとに、記事情報(Post)を参考事例として、各動作を解説していきます。

## 所有者のみがCRUDできる
```
// ①シンプルバージョン
type Post @model @auth(rules: [{allow: owner}]) {
  id: ID!
  title: String!
}

// ②詳細バージョン
type Post
  @model
  @auth(rules: [{allow: owner, ownerField: "owner", operations: [read, create, update, delete]}])
{
  id: ID!
  title: String!
  owner: String
}
```
①と②は基本的には同じ動作になります。これだけで、所有者のみがCRUDできる権限を付与できます。create時に自動で、ownerカラムに作成者のcognitoのusernameが書き込みされ、そのuserしかread、update、deleteができません。

## 所有者を指定①
```
type Post
  @model
  @auth(rules: [{allow: owner, ownerField: "userId", operations: [read, create, update, delete]}])
{
  id: ID!
  title: String!
  userId: String
}
```
ownerFieldを指定することで、ownerカラムではなく、別のカラム(userId)に自動書き込みをすることができます。また、create時に、userIdに値を渡してcreateすると、自動書込ではなく、その値が書き込まれます。(用途あまりないですが、第三者に所有権をはじめから与えることができます。)

## 複数の所有者を指定
```
type Post
  @model
  @auth(rules: [{allow: owner, ownerField: "editors", operations: [read, create, update, delete]}])
{
  id: ID!
  title: String!
  editors: [String]
}
```
ownerFieldにlist型を指定することで、そのlistに含まれるすべてのユーザーに権限を付与することができます。

## 所有者のみread可能

```
type Post
  @model
  @auth(rules: [{allow: owner}, operations: [read] }])
{
  id: ID!
  title: String!
  owner: String
}
```
上記の記載方法だと、readはもちろ所有者のみですが、updateやdeleteを制限していないため、だれでもupdateとdeleteができてしまう、という歪な状況になります。


## 所有者のみupdateとdeleteが可能、readは誰でもOK
```
type Post
  @model
  @auth(rules: [{allow: owner}, operations: [create, update, delete] }])
{
  id: ID!
  title: String!
  owner: String
}
```
これが一番良く使うパターンの権限設定の1つです。

## 所有者ごとに権限を分ける
```
type Post
  @model
  @auth(rules: [
    {allow: owner}, operations: [read, create, update, delete] },
    {allow: owner, ownerField: "editors", operations: [read, update]}
  ])
{
  id: ID!
  title: String!
  owner: Stirng
  editors: [String]
}
```
上記の例では、owner(作成者)は自由にCRUDができますが、editors権限のユーザーはreadとupdateしかできません。

## Cognitoのグループを使った権限設定
```
type Post
  @model
  @auth(rules: [
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groups: ["Viewer"], operations: [read]}
  ])
{
  id: ID!
  title: String!
}
```
CognitoのユーザープールでAdminグループに所属しているユーザーは、すべてのCRUD処理が行なえますが、Viewerに所属しているユーザーは、readしかできません。また、どちらにも含まれていないユーザーは、この情報には一切触れることができません。

## 動的なグループ設定
```
type Post
  @model
  @auth(rules: [
    {allow: groups, groupsField: "groups", operations: [read, create, update, delete]}
  ])
{
  id: ID!
  title: String!
  groups:[String]
}
```
groupsFieldを設定することで、groupsに書き込みしたグループに所属するユーザーにCRUDの権限が付与されます。

## public認証
```
type Post @model @auth(rules: [{allow: public}]) {
  id: ID!
  title: String!
}
```
Cognito User Poolによって認証されていないユーザーもCRUDできるようになる。ただしAPI Keyによる認証が必要。認証方式を追加するには[こちらのissue](https://github.com/aws-amplify/amplify-cli/issues/3802#issuecomment-718372138)が参考になる。

## private認証
```
type Post @model @auth(rules: [{allow: private}]) {
  id: ID!
  title: String!
}
```
これで、認証ユーザー(ログイン済み)しかCRUDができないようになる？らしい。APIを認証モードで設定している場合は、おそらくこれがデフォルトになっているため、`@auth`を何も宣言しなくても、認証ユーザーしかCRUDができない仕様になっていると勝手に思っています。

## フィールドレベルの認証
```
type Post
  @model
{
  id: ID!
  title: String!
  owner: String
  description: String @auth(rules: [{allow: owner}, operations: [read] }])
}
```
こんな感じでフィールドレベルでの制限も可能です。上記の例では、descriptionのみownerしかreadすることができません。それ以外は、全ユーザーがread可能です。

## レコードレベル認証✕フィールドレベル認証
```
type Post
  @model
  @auth(rules: [{allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}])
{
  id: ID!
  title: String!
  owner: String
  description: String
    @auth(rules: [{allow: owner, ownerField: "owner", operations: [read}])
}
```
これまでのレコードレベル認証とフィールドレベル認証を掛け合わすことも可能です。
上記の例では、レコードレベルではAdminグループに所属するユーザーがreadができますが、descriptionについては、ownerのみread権限に設定しているため、AdminグループのユーザーがgetPostでdescriptionを取得しようとするとエラーになります。
逆にownerが、Adminグループに所属していない場合もエラーになります。

# ポイント
スキーマ設計において、重要なポイントについてお伝えします。
## レコードレベル認証とフィールドレベル認証
上記でも解説しましたが、レコードレベル認証とフィールドレベル認証は同時に設定することができ、かつそれぞれ複数設定することができます。
1つのテーブルでレコードレベル認証だけ、フィールドレベル認証だけを複数設定した場合は、そのどれかに当てはまればOKですが、レコードレベル認証とフィールドレベル認証が同時に存在する場合は、レコードレベル認証に当てはまるかつフィールドレベル認証にも当てはまる必要があります。

```
// レコードレベル認証3つ R1,R2,R3
// フィールドレベル認証3つ F1,F2,F3

// 権限の適用はこんな感じ
(R1 || R2 || R3) && (F1 || F2 || F3)
```
色々と試しましたが、レコードに対しての`@auth`には全カラムに共通する権限を記載し、フィールドレベルの`@auth`でより厳しく権限を制限していく形に落ち着いています。

## ownerのcreateとgroupsのcreate制限の違い
ownerのcreateは権限制限ではなく、指定のカラムへのidの自動書込だが、groupによるcreateはそのグループに属するものだけがcreateできる権限を持つことができる。

## エラーを見る
`$ amplify push`後に、スキーマ設定で何かしらのミスがあればエラーが大量に出てきますが、基本的にはその１つ目のエラーが原因のテーブルの可能性が高いので、1つ目のエラー内容を確認してください。

# 追記
## フィールドレベルのread権限がいつの間にか変わった？(2020/12/19)
以前は、以下のような記述で、オーナーとadminグループ所属のユーザーのみ取得と更新ができて、titleだけはadminグループに所属していないと更新ができない、という権限設定ができていました。

```
type Todo
  @model
  @auth(rules: [
    { allow: groups, groups: ["admin"], operations: [read, create, update, delete] },
    { allow: owner, ownerField: "userId", operations: [read, update] }
  ]) {
  id: ID!
  userId: String
  title: String @auth(rules: [{ allow: groups, groups: ["admin"], operations: [update] }])
}
```

が、気づいたら上記の書き方だとread権限がないと言われ、データが取得できなくなりました。
なので、read権限を記載する必要があります。

```
type Todo
  @model
  @auth(rules: [
    { allow: groups, groups: ["admin"], operations: [read, create, update, delete] },
    { allow: owner, ownerField: "userId", operations: [read, update] }
  ]) {
  id: ID!
  userId: String
  title: String @auth(rules: [
    { allow: groups, groups: ["admin"], operations: [read, update] },
    { allow: owner, ownerField: "userId", operations: [read] },
  ])
}
```

古いプロジェクトは最初の記述で問題なく動いて入るので、AmplifyCLIを更新するなかで、どこかからは使用が変わったようです。
個人的にスッキリしませんが、とりあえず今はこういうものと受け入れています。

# おわりに
ややボリューミーな内容になりましたが、amplifyで権限設定をしようと思うと最低でもこの記事の内容の理解は必須です。まだ深堀りできていない部分も多いので、ご存知な情報があれば是非コメントお願いします。

# 参考
[GraphQL Transform(公式ドキュメント)](https://aws-amplify.github.io/docs/cli-toolchain/graphql?sdk=js) 

# 関連記事
[AWS amplify フレームワークの使い方Part1〜Auth設定編〜](https://qiita.com/too/items/fc961283dcbef3aafdeb)
[AWS Amplify フレームワークの使い方Part2〜Auth実践編〜](https://qiita.com/too/items/54992bb871fc1a2ab101)
[AWS Amplify フレームワークの使い方Part3〜API設定編〜](https://qiita.com/too/items/c0ce14d16772b9f71223)
[AWS Amplify フレームワークの使い方Part4〜API実践編〜](https://qiita.com/too/items/f9078803ea394c35df7d)
[AWS Amplify フレームワークの使い方Part5〜GraphQL Transform @model編〜](https://qiita.com/too/items/ab6b313da9f9e34be4a7)
[AWS Amplify フレームワークの使い方Part7〜GraphQL Transform @key編〜]
(https://qiita.com/too/items/cb1dfb4f44536a3e9855)
[AWS Amplify フレームワークの使い方Part8〜GraphQL Transform @connection編〜](https://qiita.com/too/items/c77f1e5487d2497b5061)
[AWS Amplify フレームワークの使い方Part9〜Function 基礎編〜](https://qiita.com/too/items/d2732b3ec625011f4567)
[AWS Amplify フレームワークの使い方Part10〜Storage編〜](https://qiita.com/too/items/361bc10b6d27f133e9f8)
[AWS Amplify フレームワークの使い方Part11〜Function 権限管理編〜](https://qiita.com/too/items/b34cbfd7e96a01e33f23)
[AWS Amplify フレームワークの使い方Part12〜ENV編〜](https://qiita.com/too/items/5c1e28cf95fc6229547e)
[AWS Amplify フレームワークの使い方Part13〜Auth 設定更新編〜]
(https://qiita.com/too/items/52f35860bcb5bdf5e667)
[AWS Amplify フレームワークの使い方Part14〜Lambda レイヤー編〜]
(https://qiita.com/too/items/54de781085bd9a3a66d0)