ひげぶろぐ

開発とか組織とかの話

AWS AmplifyプロジェクトにおけるCustom Resolverを用いた細かな権限管理

この記事は?

AWS Amplifyにおける、カスタムリゾルバーを用いたDynamoDBの細かいパーミッション管理について記載している。
具体的には、Cognitoのユーザプールをベースに、他人が自分のIDでデータを作ることを防ぐ仕組みを作る。
DynamoDBにおける一意性(Unique)の担保の仕方についても軽く触れている。

経緯

AWS Amplify は2019年2月にカスタムリゾルバーのサポートを発表している。

これにより、AppSyncコンソールからの作業やCloudFormation Templateの編集は不要となり、Amplifyユーザは非常に簡単な手順でカスタムリゾルバーを組み込むことが可能となった。

が、まだWeb上では利用事例が少なく、不明点も多かったので、ログとしてここに残しておく。

前提知識

カスタムリゾルバーとは

ゾルバーに関しては以下に詳しく記載されている。 docs.aws.amazon.com

ざっくり言うと、DynamoDB等へのアクセスが走る前と後の処理をVTLという言語で記述するもの。 f:id:macha_tokyo:20190628223440p:plain

これにより、「データの所有者だけにデータ更新を許可する」「全データから、リクエストしたユーザのデータのみをフィルタリングして返す」等の処理が可能となる。
もちろんバリデーション等の処理もここで行える。

つまりは、一般的なサーバサイドの処理と言って良いと思う。
クライアントサイドでいくら頑張ってもデータは改ざんされるので、一定規模のサービスになれば必ずサーバサイドの処理が必要となる。
AWS Amplifyでは、VTLの記述のみで、Lambda等を個別に設定することなくサーバサイドの処理を実現出来る。

具体的なユースケースについてはこちらの資料に詳しく記載されている。 認証のユースケース - AWS AppSync

AWS Amplifyにおけるカスタムリゾルバーの追加方法

公式のドキュメントによると以下の通り。

  1. AWS AppSync APIを作成後、Amplify projectのAPIフォルダに resolvers という空フォルダが出来ている
  2. カスタムリゾルバーを作るには、 Query.getTodo.req.vtl のようなファイルを上述の resolvers フォルダに作る
  3. 次に amplify pushamplify api gql-compile を実行したタイミングで、自動生成されたテンプレートの代わりにカスタムのテンプレートが利用される。

カスタムリゾルバを試す

実際に試してみる。
今回は以下のようなシステムを組む。

  • amplify add api によりGraphQLベースのAPIを構築
  • Cognitoユーザプールと、そこに登録されたユーザに紐づくユーザ属性テーブル User がある(DynamoDB)
  • User テーブルのIDはCognitoユーザプールのusernameと一致させ、ユーザ本人しか create できない

schema.graphql はこんなイメージ。

type User
  @model
  @auth(
    rules: [{ allow: owner, ownerField: "id", operations: [create, update] }]
  ) {
  id: ID!
  name: String!
  gender: String
  birthday: String
  image: String
}

1. resolversフォルダの確認

amplify add api した後のプロジェクトでは、自動で以下のようにフォルダ/ファイルが生成されている。

-> % tree -L 2 amplify/backend/api
amplify/backend/api
└── amplifysample
    ├── build
    ├── parameters.json
    ├── resolvers
    ├── schema.graphql
    └── stacks

この中の resolvers フォルダが今回ファイルを生成していくフォルダになる。
ちなみに build フォルダの中にも resolvers というフォルダがあるが、そちらは最終的にサーバへpushされるファイル群。

2. resolvers フォルダ以下にVTLを置く

VTLについては初めて触る人も多いかもしれない。
公式のドキュメントを一度読むとちょっとだけ分かる。

リゾルバーのマッピングテンプレートプログラミングガイド - AWS AppSync

おすすめの勉強法は build フォルダ以下に吐き出された自動生成されたVTLファイルを読む方法。
特に @auth をつけたテーブルのmutationは勉強になる。

今回はまず build/resolvers フォルダか resolvers フォルダに Mutation.createUser.req.vtl をコピーする。
build/resolvers には自動生成済みの VTL ファイルが設置されているため、それをベースに細かな権限の処理を加えていく。
req.vtlres.vtl があるが、それぞれリクエストとレスポンスに対する処理で、今回は req.vtl でバリデーションを行なう。
ファイルの内容は以下のようにする。

## [Start] Owner Authorization Checks **
#set( $isOwnerAuthorized = false )
## Authorization rule: { allow: owner, ownerField: "id", identityField: "cognito:username" } **
#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.input.id, null) )
#set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"),
$util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#if( $util.isString($allowedOwners0) )
    #if( $allowedOwners0 == $identityValue )
        #set( $isOwnerAuthorized = true )
    #end
#end
#if( $util.isNull($allowedOwners0) && (! $ctx.args.input.containsKey("id")) )
    $util.qr($ctx.args.input.put("id", $identityValue))
    #set( $isOwnerAuthorized = true )
#end
## [End] Owner Authorization Checks **

## [Start] Throw if unauthorized **
#if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized
    == true) )
    $util.unauthorized()
#end
## [End] Throw if unauthorized **

## [Start] Prepare DynamoDB PutItem Request. **
$util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt,
$util.time.nowISO8601())))
$util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt,
$util.time.nowISO8601())))
$util.qr($context.args.input.put("__typename", "User"))
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
  "id":   $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId()))
} #end,
  "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
  "condition": {
      "expression": "attribute_not_exists(#id)",
      "expressionNames": {
          "#id": "id"
    }
  }
}
## [End] Prepare DynamoDB PutItem Request. **

処理の内容としては以下の通り。

  • リクエスト送信者の cognito username を取得
  • id が指定されていたら、cognito username と比較。一致すれば isOwnerAuthorized フラグを立てる
  • id が指定されていなかったら、cognito username を id をして採用。 isOwnerAuthorized フラグを立てる
  • isOwnerAuthorized が立っていない場合、 unauthorized() を実行して処理を修了する
  • attribute_not_exists(#id) で、まだテーブルに該当idのレコードが存在しないかチェック(一意性の担保)

3. amplify push を実行

amplify push を実行する。実行後に build/resolvers/Mutation.createUser.req.vtl を見ると、カスタムした内容に置き換わっているのが分かるはず。
同様に、AppSyncのConsoleでも確認可能。

4. 実行

ログインした状態でリクエストパラメータの id を自身の username 以外で指定して createUser を実行すると、失敗するはず。
逆にレコード未作成の状態で id を指定せずに createUser を実行すれば、id に自動で username が入ったレコードが生成される。

終わりに

最初こそVTLの記法や仕組みに戸惑いはするが、作ってみれば非常に低コストにパーミッション管理が行えた印象。
まだAmplifyやVTLに関しては記事が少なく、今回の手順の中にも間違いがあるかもしれない。ので、何かあればご指摘いただけると幸いです。