AWS Amplifyでマルチテナント型サービスを作る その2
前回はAWS Amplifyでマルチテナント型サービスのためのアクセス制御を同実装するかという話を書きました。前回はモデルを取得する(Query)ケースについて説明しましたが、今回はさらに実装を進めて、モデルの作成、更新、削除(Mutation)の場合について書こうと思います。
Amplify CLIによる自動生成の話なので、Amplify CLIのバージョンによっては挙動が変わる可能性があります。よってAmplify CLIのバージョンを書いておきます。
Amplify CLI version 12.7.1
モデルのMutationの場合もQueryのときと同じくProjectUserテーブルにプロジェクトとユーザーの紐づけがあるかどうかを確認すればよいのですが、以下のモデル定義だと、Mutationクエリの入力には主キーであるidしか生成されません。(projectIdは必須定義のためCreateTaskInputには必須で入ってきます。)
type Task @model @auth(rules: [{allow: private}]) {
id: ID!
projectId: String!
project: Project @hasOne
name: String!
startDate: String
endDate: String
manDays: Float
description: String
category: Category @hasOne
priority: Priority! @hasOne
status: Status! @hasOne
order: Float!
creator: User! @hasOne
assignees: [User] @manyToMany(relationName: "TaskUser")
attachmentFiles: [AttachmentFile] @hasMany(indexName: "byTask", fields: ["id"])
}
上記のモデルから生成されるupdateTaskInputとdeleteTaskInput。projectIdが必須で含まれていません。(projectId自体は必須定義なのでcreateTaskInputでは必須になっています。)
export type UpdateTaskInput = {
id: string,
projectId?: string | null,
name?: string | null,
startDate?: string | null,
endDate?: string | null,
manDays?: number | null,
description?: string | null,
order?: number | null,
taskProjectId?: string | null,
taskCategoryId?: string | null,
taskPriorityId?: string | null,
taskStatusId?: string | null,
taskCreatorId?: string | null,
};
export type DeleteTaskInput = {
id: string,
};
この時生成されるリゾルバーの読み解いてみます。
リゾルバーを全てここに記載すると長くなってしまうので簡単に概要を書くにとどめます。例えばupdateTaskのパイプラインリゾルバーは以下の様になりました。
Mutation.updateTask.init.1.req.vtl
更新日にデフォルト値を入れています。
Mutation.updateTask.auth.1.req.vtl
引数のidをキーにしてGetItemしています。
Mutation.updateTask.auth.1.res.vtl
メインの認証処理です。reqで取得したTaskの結果確認などしています。
Mutation.updateTask.postAuth.1.req.vtl
auth.1.resの結果を再度確認しています。(あまり意味はないかも。)
Mutation.updateTask.req|res.vtl
UpdateItemしています。
さて、上記のフローのどこかにProjectUserの確認を挟みたいのですが、以下の問題があります。
リクエスト引数にTask.idは必ず含まれますが、Task.projectIdは必須ではないため含まれない場合があります。
Mutation.updateTask.auth.1.res.vtlでGetItemしていますが、結果をstashしていないため、auth.2リゾルバーを作ってもせっかくGetItemで取得したTask情報からprojectIdを得ることができません。OverrideしてGetItemの結果をstashすれば良いですが自動生成テンプレートはできれば触りたくないのとファイル数が増えていくので、Overrideはしたくありません。
シンプルにprojectIdがリクエストに必ず含まれるようにすれば良さそうです。
生成されるMutationクエリーを書き換えてもよさそうですが、できるだけ自動生成に任せたいです。
そこでモデルの主キーにソートキーを指定する方法=複合主キーにする方法を採ってみます。
ソートキーを指定すると、生成されるMutationクエリーを書き換えるよりもスマートに実装できます。以下の2点がメリットです。
生成されるMutationクエリーにprojectIdが強制される。
DynamoDBのテーブルがidとprojectIdで複合主キーになるため、update/deleteの場合、生成されるauth.1リゾルバーで行うGetItem時にidとprojectIdを使うようになる。これで対象のTask.idが対象のProject.idに所属しているかどうかのチェックもできる。
では、idに@primaryKey(sortKeyFields:["projectId"])としてソートキーにprojectIdを設定します。
type Task @model @auth(rules: [{allow: private}]) {
id: ID! @primaryKey(sortKeyFields:["projectId"])
projectId: String!
project: Project @hasOne
...略
}
するとUpdateTaskInputにprojectIdが強制されました。DeleteTaskInputも同様です。
export type UpdateTaskInput = {
id: string,
projectId: string,
name?: string | null,
startDate?: string | null,
endDate?: string | null,
manDays?: number | null,
description?: string | null,
order?: number | null,
taskProjectId?: string | null,
taskCategoryId?: string | null,
taskPriorityId?: string | null,
taskStatusId?: string | null,
taskCreatorId?: string | null,
};
export type DeleteTaskInput = {
id: string,
projectId: string,
};
これで、Mutationリゾルバーの$ctx.args.inputにはprojectIdが必ず含まれることになるため、Queryのときと同じくpreAuthでProjectUserをScanし、auth.2でチェックができます。
ただし、sortKeyFieldsを設定すると、preAuht.1スロットにMutation.updateTask.preAuth.1.req.vtlというリゾルバーが生成されます。sortKeyFieldsが指定されるとDynamoDBのテーブルにもSortKeyが設定されます。SortKeyはMutation.updateTask.auth.1.req.vtlで行われるGetItem時に指定しなければいけないため、preAuth.1ではその処理を行っています。
なので、自動生成されたpreAuth.1を上書きしない様にProjectUserのScanはpreAuth.2スロットで行います。
注意点としては、Taskモデルの主キーにソートキーを作ると、TaskTableは作り直しになります。その点に関してはこの方法のデメリットと言えるかもしれません。自分の場合はそのままamplify pushしてもエラーがでTaskTableの更新ができなかったので、一度Taskモデルの定義を消してamplify pushをし、TaskTableが消えた後、再度Taskモデルの定義を戻してamplify pushをすることで無事にデプロイできました。
既にTaskTableにデータがある場合は、データも消えてしまうので、データをエクスポートしてバックアップを取っておきます。S3へのエクスポート/インポートは既存テーブルへのインポートができなかったので、CSVでエクスポートし、インポートは専用のプログラムを作るなどして対応が必要そうです。
今回はマルチテナント型のアクセス制御にソートキーを活用する方法についてでした。ソートキーをつけることでリレーションモデルへの影響などもあるのですがその辺りの話はまた別の機会に。
10月中にα版を公開したかったですが、モデルのアクセス制御を色々検証していたら間に合わなくなってしまいました…。地道に実装を進めようと思います。
それではまた次の記事で。
もしこの記事があなたのお役に立てたなら幸いです。 よろしければサポートをお願いします。今後の制作資金にさせていただきます!