Open15

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」について読み解いた。
https://docs.amplify.aws/cli/graphql-transformer/auth/#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のプロパティを好きなように参照できれば…

  1. CognitoのプロパティにprojectIdを追加
  2. アプリからCognitoのプロパティの更新方法を確認
  3. カスタムディレクティブ?で同プロパティで認証制御をする
トト犬トト犬

2. アプリからCognitoのプロパティの更新方法を確認

async function updateUser() {
  const user = await Auth.currentAuthenticatedUser();
  await Auth.updateUserAttributes(user, {
    'address': '105 Main St. New York, NY 10001'
  });
}

https://docs.amplify.aws/guides/authentication/managing-user-attributes/q/platform/js/#configuring-standard-attributes

配列で持たせられるか?カスタムディレクティブから参照するときに、配列として扱ってくれるのか?

トト犬トト犬

カスタムリゾルバーを自作する

以下の2記事をマスターすれば、カスタムリゾルバーをvtlで作成するというAppSyncエンジニアに求められる能力が身につくはずです!

VTL言語のチュートリアル
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-programming-guide.html

Amplifyでカスタムリゾルバーをvtlで書く方法!
https://docs.amplify.aws/cli/graphql-transformer/resolvers/

パイプラインリゾルバーを自作する

CustomResourcesをいじることで、パイプラインリゾルバーを作成する日本語記事!!
https://thinkami.hatenablog.com/entry/2019/07/15/201356

トト犬トト犬

https://docs.amplify.aws/cli/graphql-transformer/resolvers/#overwriting-resolvers
こちらの記事を少し読み解いていきます。

※日本語で参考になる記事
https://dev.classmethod.jp/articles/amplify-tips-series-7/

カスタムリゾルバーを追加する手順

  1. スキーマ(schema.graphqlファイル)のQuery, Mutation, Subscriptionいずれかの中にカスタムクエリーを追記する。
  2. <project-root>/amplify/backend/api/<api-name>/resolversフォルダーに、新たに以下の2ファイルを作成する。これにより、クエリが呼び出されたときの処理(リゾルバー)を書く場所ができる!
    • Query.myCustomQuery.req.vtl
    • Query.myCustomQuery.res.vtl
    • ※命名規則:<TypeName>.<FieldName>.<req/res>.vlt
  3. カスタムリゾルバーを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」
https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference.html


3. AWSのリソースとして、カスタムリゾルバーを追記

記述する対象は、amplify\backend\api\chatinside\stacks\CustomResources.json

amplify/backend/api/<api-name>/stacks/下であれば、どこでもよい!
amplify push時に、stacks下の*.jsonファイルはすべてデプロイされるそうなので!

ただし、どこに書くにしてもCloudFormationの知識は必要になる。。。

最低限で頑張るには…

  • こちらのパラメータの表を見る!その意味を把握
  • 今回見ている2つの例をよく読み解く!
トト犬トト犬

AppSync → Dynamoのパターンを読み解く…

Dynamoの比較演算子諸々はこちらを参照のこと

AppSync → 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を書くための定義に過ぎない!

参考:AppSync→DynamoのVTLサンプル集