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という言語で記述するもの。
これにより、「データの所有者だけにデータ更新を許可する」「全データから、リクエストしたユーザのデータのみをフィルタリングして返す」等の処理が可能となる。
もちろんバリデーション等の処理もここで行える。
つまりは、一般的なサーバサイドの処理と言って良いと思う。
クライアントサイドでいくら頑張ってもデータは改ざんされるので、一定規模のサービスになれば必ずサーバサイドの処理が必要となる。
AWS Amplifyでは、VTLの記述のみで、Lambda等を個別に設定することなくサーバサイドの処理を実現出来る。
具体的なユースケースについてはこちらの資料に詳しく記載されている。 認証のユースケース - AWS AppSync
AWS Amplifyにおけるカスタムリゾルバーの追加方法
公式のドキュメントによると以下の通り。
- AWS AppSync APIを作成後、Amplify projectのAPIフォルダに
resolvers
という空フォルダが出来ている - カスタムリゾルバーを作るには、
Query.getTodo.req.vtl
のようなファイルを上述のresolvers
フォルダに作る - 次に
amplify push
かamplify 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.vtl
と res.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に関しては記事が少なく、今回の手順の中にも間違いがあるかもしれない。ので、何かあればご指摘いただけると幸いです。