Amplify×AppSyncでリクエストに制限を設ける
ユーザーが所属するプロジェクト外にアクセスしないようにしたい。
そのために使えそうな知見をここにストックしていきます。
各データについて、projectIdのようなものを付けるイメージ。
各プロジェクトには、以下が設定される。
- アクセス可能なリソース
- アクセスを許可するユーザー
「マルチテナント」という発想と同じかもしれない。
テナントは、ログイン時に入る先を一意に特定するが、
今回はログインしているユーザーは、ログイン時以外にもテナント(プロジェクト)を切り替えることが可能だ。
そのためCognitoにテナント情報を持たせて参照するようなことは難しい…かもしれない。
×没案
自動生成される「src\graphql\mutations.ts」を見てみると
以下のように$condition
という引数がありました。
これは香ばしいですね~
export const updatePersona = /* GraphQL */ `
mutation UpdatePersona(
$input: UpdatePersonaInput!
$condition: ModelPersonaConditionInput
) {
updatePersona(input: $input, condition: $condition) {
認証についてのディレクティブ「@auth」について読み解いた。
いつ | 項目 | 詳細 |
---|---|---|
- | allow | 唯一の必須項目 |
選択肢: { owner groups private public } | ||
provider | 選択肢: { apiKey iam oidc userPools } | |
owner認証で使用? | ownerField: String | 初期値: owner |
identityClaim: String | 初期値: username | |
group認証で使用? | groupClaim: String | 初期値: cognito:groups |
groups: [String] | 必須項目 | |
groupsField: String | 初期値: groups | |
動的グループ認証の時 | ||
- | operations: [選択肢] | より詳細な制御が必要な時… |
選択肢: { create update delete read } |
@auth(rules: [{ allow: owner }])
を設定すると、これまでのデータを参照できなくなってしまった…
どうやら、データの「owner」が自分でないと参照できないようだ。
[試したこと]
- ↑のディレクティブを設定後に、データ作成をしたら勝手にownerカラム付きでデータが更新された。
- これまでのデータにもownerカラムを自分のidで設定したら、参照できた
未設定はデフォルトで全部指定になる
以下のパターン1とパターン2は同じ内容を示す。
// パターン1
@auth(rules: [{ allow: owner }])
// パターン2
@auth(
rules: [
{ allow: owner, ownerField: "owner", operations: [create, update, delete, read] },
])
ownerFieldとは?
ownerFieldとは、該当データをCreateする際に付与されるカラム名を設定するもの。
デフォルトでは「owner」になっている。
Create時に、これを付与させて取得時に検証するためには以下のいずれかが必要。
- 明示的にoperationsにcreateを記載する
- operationsを指定しないことで、暗示的にcreateを指定する
operationsに指定するとは…
基本的にauthルールに指定されていない行動は拒否されます。
それを念頭に置くと理解しやすいです。
パターン1
type Todo @model
@auth(rules: [{ allow: owner }]) {
id: ID!
updatedAt: AWSDateTime!
content: String!
}
この場合、暗示的にoperations: [create, update, delete, read]
が指定されます。
そのため、ownerだけにこれらの動作が許容されるため…
「ownerではない認証済みユーザー」は、createしかできません。
(create自体は、該当データの所有者が誰か…ということは問題にならないので。なんならこれから作るユーザーが所有所ですし)
パターン2
type Todo @model
@auth(rules: [{ allow: owner, operations: [create, delete, update] }]) {
id: ID!
updatedAt: AWSDateTime!
content: String!
}
ownerだけにread以外の権限が独占されます。
そのため…
「ownerではない認証済みユーザー」は、「create, read」しかできません。
パターン3
@auth(rules: [
# Defaults to use the "owner" field.
{ allow: owner },
# Authorize the update mutation and both queries.
{ allow: owner, ownerField: "editors", operations: [update, read] }
])
今度は複数条件です。
推測するに、認証条件は以下のようなイメージなんではないかと思われます。
req: ユーザーからのリクエスト
rules: AppSyncでかけられる認証ルール
data: DynamoDBに格納されたデータ
const getAllowedOperations = (data, req, rules) => {
// 認証されてない
if (!req.isAuthenticated) return [];
// operationsとownerFieldのデフォルト値
rules.map((rule) => {
if (rule.operations === undefined)
rule.operations = ["create", "read", "update", "delete"];
if (rule.ownerField === undefined) rule.ownerField = "owner";
return rule;
});
// アクセスするユーザーがいずれかのownerFieldに所属する
if (rules.map((rule) => data[rule.ownerField].includes(req.user))) {
// ownerFieldに自分が存在する条件
const myRules = rules.filter(
(rule) => data[rule.ownerField] === req.user
);
// ルールごとに許可されている操作を統合
const myOperations = myRules
.reduce((acc, rule) => [...acc, ...rule.operations], [])
.push("create");
return Array.from(new Set(myOperations));
} else {
const restrictedOperations = rules.reduce((acc, rule) => {
acc.push(...rule.operations);
}, []);
return ["read", "update", "delete"]
.filter((operation) => !restrictedOperations.includes(operation))
.push("create");
}
};
同じようにownerではなく、所属グループで認証することもできる。
ここでのグループとは、Cognitoに設定されたプロパティと推測する。
publicは誰でもアクセスできる
privateは
参照できるデータはCognitoのデータ!
owner: username
group: groupname
ディレクティブではここが限界なのか?
Cognitoのプロパティを好きなように参照できれば…
- CognitoのプロパティにprojectIdを追加
- アプリからCognitoのプロパティの更新方法を確認
- カスタムディレクティブ?で同プロパティで認証制御をする
2. アプリからCognitoのプロパティの更新方法を確認
async function updateUser() {
const user = await Auth.currentAuthenticatedUser();
await Auth.updateUserAttributes(user, {
'address': '105 Main St. New York, NY 10001'
});
}
配列で持たせられるか?カスタムディレクティブから参照するときに、配列として扱ってくれるのか?
この記事はドンピシャなので、読み解く!
カスタムリゾルバーを自作する
以下の2記事をマスターすれば、カスタムリゾルバーをvtlで作成するというAppSyncエンジニアに求められる能力が身につくはずです!
VTL言語のチュートリアル
Amplifyでカスタムリゾルバーをvtlで書く方法!
パイプラインリゾルバーを自作する
CustomResourcesをいじることで、パイプラインリゾルバーを作成する日本語記事!!
こちらの記事を少し読み解いていきます。
※日本語で参考になる記事
カスタムリゾルバーを追加する手順
- スキーマ(schema.graphqlファイル)のQuery, Mutation, Subscriptionいずれかの中にカスタムクエリーを追記する。
-
<project-root>/amplify/backend/api/<api-name>/resolvers
フォルダーに、新たに以下の2ファイルを作成する。これにより、クエリが呼び出されたときの処理(リゾルバー)を書く場所ができる!- Query.myCustomQuery.req.vtl
- Query.myCustomQuery.res.vtl
- ※命名規則:<TypeName>.<FieldName>.<req/res>.vlt
- カスタムリゾルバーをAWSのリソースとして具現化するため、
amplify/backend/api/<api-name>/stacks/CustomResources.json
に追記する。
1. カスタムクエリーを追記する
2. VTLファイル(カスタムリゾルバー)ファイルの追加
req / res の2種類あるのは、リクエスト / レスポンスを意味しており、以下のような住み分けをしています。
template | templateに記載する処理 |
---|---|
リクエストテンプレート | データソースへのアクセス。事前処理。 |
レスポンステンプレート | データソースからの返却と、変換処理等。 |
左右2種類あるのは、リゾルバには2種類の処理体系を選択できるためです。
詳しくはこちら
そして、VTLを書く際には基本的にamplify/backend/api/<api-name>/build/resolvers/Mutation.updateChat.req.vtl
のようにAmplifyが自動で生成してくれたファイルを参照するのが手軽!
勘違いしていたが、vtl自体がresolverではない!
「GraphQLのリクエストを、リゾルバーが解釈できるjsonに変換するのがvtl」
3. AWSのリソースとして、カスタムリゾルバーを追記
記述する対象は、amplify\backend\api\chatinside\stacks\CustomResources.json
amplify/backend/api/<api-name>/stacks/
下であれば、どこでもよい!
amplify push
時に、stacks下の*.jsonファイル
はすべてデプロイされるそうなので!
ただし、どこに書くにしてもCloudFormationの知識は必要になる。。。
最低限で頑張るには…
AppSync → Dynamoのパターンを読み解く…
Dynamoの比較演算子諸々はこちらを参照のこと
これでなんとか頑張って読み解くのだー!
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
},
"update" : {
"expression" : "SET author = :author, title = :title, content = :content, #url = :url ADD version :one",
"expressionNames": {
"#url" : "url"
},
"expressionValues": {
":author" : $util.dynamodb.toDynamoDBJson($context.arguments.author),
":title" : $util.dynamodb.toDynamoDBJson($context.arguments.title),
":content" : $util.dynamodb.toDynamoDBJson($context.arguments.content),
":url" : $util.dynamodb.toDynamoDBJson($context.arguments.url),
":one" : { "N": 1 }
}
}
}
expressionNamesやexpressionValuesは、expressionを書くための定義に過ぎない!