0
0

More than 3 years have passed since last update.

【Amplify api】Mutationフィールドに@authディレクティブをつけるとonly one resolver is allowed per fieldエラー

Posted at

この記事は「Amplify x Next.jsハマりどころ」シリーズ第3弾です。
もともとはnoteに書いていたのですが、あまり見てもらえないのとコード記述が読みにくいのでqiitaにせっせと移植中です。

ハマりどころ一覧

  1. postConfirmation lambda triggerでstorage(dynamoDB)にアクセスしようとすると、Circular dependency between resourcesでpushできない
  2. type Query fieldに@aws_iamディレクティブをつけても、authRole(またはunauthRole)にポリシーがアタッチされない )
  3. type Mutation filedに@authディレクティブをつけるとOnly one resolver is allowed per fieldエラー <- 今回!
  4. redirect_urlを環境ごとに分けたいんですが
  5. S3の画像URLを署名なしで取得したいんですが
  6. aws-exports.jsが.gitignoreの対象になっているためビルド時にエラーになるんですが
  7. モノレポでamplifyバックエンドを共有したいんですが
  8. サブドメインでも認証を維持したいんですが

今回はこのNo.3について書いていきたいと思います。

※このシリーズでは、まずはエラーを再現し、そのエラーを修正するというチュートリアル的な流れで書いていきます。結論だけ知りたい方は「こいつの倒し方」まで読み飛ばしてください。

Versions

@aws-amplify/cli: 4.43.0

To Reproduce

今回もエラーを再現してからこいつの倒し方をご紹介したいと思います。

まず、Mutationを追加するには

  1. stacksを登録する
  2. resolverを作成する
  3. schema.graphqlにtype Mutationフィールドを追加する

という3つのステップが必要になります。

今回はCounterというモデルがあり、Counter.countをインクリメントするincrementCountというMutationを作成してみることにします。


type Counter @model @auth(rules: [{ allow: owner }, { allow: private, provider: iam }]){
 id: ID!
 count: Int!
}

type Mutation {
 incrementCount(id: String): Int
   @auth(rules: [{ allow: private, provider: iam }])
}

1. stacksを追加する

cloudFormationでAppSyncにMutationとcustom resolverを登録させるため、./amplify/backend/api/[:apiname]/stacksというディレクトリの中のCustomResources.jsonというファイルを開き、"Resources"フィールドに以下を追記します。

"MutateIncrementCount": {
  "Type": "AWS::AppSync::Resolver",
  "Properties": {
    "ApiId": {
      "Ref": "AppSyncApiId"
    },
    "DataSourceName": "CounterTable",
    "TypeName": "Mutation",
    "FieldName": "incrementCount",
    "RequestMappingTemplateS3Location": {
      "Fn::Sub": [
        "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
        {
          "S3DeploymentBucket": {
            "Ref": "S3DeploymentBucket"
          },
          "S3DeploymentRootKey": {
            "Ref": "S3DeploymentRootKey"
          },
          "ResolverFileName": {
            "Fn::Join": [
              ".",
              [
                "Mutation",
                "incrementCount",
                "req",
                "vtl"
              ]
            ]
          }
        }
      ]
    },
    "ResponseMappingTemplateS3Location": {
      "Fn::Sub": [
        "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
        {
          "S3DeploymentBucket": {
            "Ref": "S3DeploymentBucket"
          },
          "S3DeploymentRootKey": {
            "Ref": "S3DeploymentRootKey"
          },
          "ResolverFileName": {
            "Fn::Join": [
              ".",
              [
                "Mutation",
                "incrementCount",
                "res",
                "vtl"
              ]
            ]
          }
        }
      ]
    }
  }
}

2. resolverを作成する

1で指定したファイル(今回はMutation.incrementCount.req.vtlとMutation.incrementCount.res.vtl)を、resolversディレクトリ内に作成して以下のように書きます。基本的に、buildディレクトリに自動で作成されたresolverをコピーして、必要な処理だけ書き換えればいいと思います。

## Mutation.incrementCount.req.vtl

## [Start] Determine request authentication mode **
#if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) )
 #set( $authMode = "userPools" )
#end
## [End] Determine request authentication mode **
## [Start] Check authMode and execute owner/group checks **
#if( $authMode == "userPools" )
 ## No Static Group Authorization Rules **


 #if( ! $isStaticGroupAuthorized )
   ## No dynamic group authorization rules **


   ## [Start] Owner Authorization Checks **
   #set( $ownerAuthExpressions = [] )
   #set( $ownerAuthExpressionValues = {} )
   #set( $ownerAuthExpressionNames = {} )
   ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **
   $util.qr($ownerAuthExpressions.add("#owner0 = :identity0"))
   $util.qr($ownerAuthExpressionNames.put("#owner0", "owner"))
   $util.qr($ownerAuthExpressionValues.put(":identity0", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")))))
   ## [End] Owner Authorization Checks **


   ## [Start] Collect Auth Condition **
   #set( $authCondition = $util.defaultIfNull($authCondition, {
 "expression": "",
 "expressionNames": {},
 "expressionValues": {}
}) )
   #set( $totalAuthExpression = "" )
   ## Add dynamic group auth conditions if they exist **
   #if( $groupAuthExpressions )
     #foreach( $authExpr in $groupAuthExpressions )
       #set( $totalAuthExpression = "$totalAuthExpression $authExpr" )
       #if( $foreach.hasNext )
         #set( $totalAuthExpression = "$totalAuthExpression OR" )
       #end
     #end
   #end
   #if( $groupAuthExpressionNames )
     $util.qr($authCondition.expressionNames.putAll($groupAuthExpressionNames))
   #end
   #if( $groupAuthExpressionValues )
     $util.qr($authCondition.expressionValues.putAll($groupAuthExpressionValues))
   #end
   ## Add owner auth conditions if they exist **
   #if( $totalAuthExpression != "" && $ownerAuthExpressions && $ownerAuthExpressions.size() > 0 )
     #set( $totalAuthExpression = "$totalAuthExpression OR" )
   #end
   #if( $ownerAuthExpressions )
     #foreach( $authExpr in $ownerAuthExpressions )
       #set( $totalAuthExpression = "$totalAuthExpression $authExpr" )
       #if( $foreach.hasNext )
         #set( $totalAuthExpression = "$totalAuthExpression OR" )
       #end
     #end
   #end
   #if( $ownerAuthExpressionNames )
     $util.qr($authCondition.expressionNames.putAll($ownerAuthExpressionNames))
   #end
   #if( $ownerAuthExpressionValues )
     $util.qr($authCondition.expressionValues.putAll($ownerAuthExpressionValues))
   #end
   ## Set final expression if it has changed. **
   #if( $totalAuthExpression != "" )
     #if( $util.isNullOrEmpty($authCondition.expression) )
       #set( $authCondition.expression = "($totalAuthExpression)" )
     #else
       #set( $authCondition.expression = "$authCondition.expression AND ($totalAuthExpression)" )
     #end
   #end
   ## [End] Collect Auth Condition **
 #end


 ## [Start] Throw if unauthorized **
 #if( !($isStaticGroupAuthorized == true || ($totalAuthExpression != "")) )
   $util.unauthorized()
 #end
 ## [End] Throw if unauthorized **
#end
## [End] Check authMode and execute owner/group checks **

#if( $authCondition && $authCondition.expression != "" )
 #set( $condition = $authCondition )
 #if( $modelObjectKey )
   #foreach( $entry in $modelObjectKey.entrySet() )
     $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)"))
     $util.qr($condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key"))
   #end
 #else
   $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#id)"))
   $util.qr($condition.expressionNames.put("#id", "id"))
 #end
#else
 #if( $modelObjectKey )
   #set( $condition = {
 "expression": "",
 "expressionNames": {},
 "expressionValues": {}
} )
   #foreach( $entry in $modelObjectKey.entrySet() )
     #if( $velocityCount == 1 )
       $util.qr($condition.put("expression", "attribute_exists(#keyCondition$velocityCount)"))
     #else
       $util.qr($condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)"))
     #end
     $util.qr($condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key"))
   #end
 #else
   #set( $condition = {
 "expression": "attribute_exists(#id)",
 "expressionNames": {
     "#id": "id"
 },
 "expressionValues": {}
} )
 #end
#end

#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
## ここでインクリメントの処理を記述してます
$util.qr($expSet.put("#count", "#count + :x"))
$util.qr($expNames.put("#count", "count"))
$util.qr($expValues.put(":x", $util.dynamodb.toDynamoDB(1)))

#set( $expression = "" )
#if( !$expSet.isEmpty() )
 #set( $expression = "SET" )
 #foreach( $entry in $expSet.entrySet() )
   #set( $expression = "$expression $entry.key = $entry.value" )
   #if( $foreach.hasNext() )
     #set( $expression = "$expression," )
   #end
 #end
#end

#set( $update = {} )
$util.qr($update.put("expression", "$expression"))
#if( !$expNames.isEmpty() )
 $util.qr($update.put("expressionNames", $expNames))
#end
#if( !$expValues.isEmpty() )
 $util.qr($update.put("expressionValues", $expValues))
#end
{
 "version": "2018-05-29",
 "operation": "UpdateItem",
 "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else {
 "id": {
     "S": $util.toJson($context.args.id)
 }
} #end,
 "update": $util.toJson($update),
 "condition": $util.toJson($condition)
}
## Mutation.incrementCount.res.vtl

#if( $ctx.error )
$util.error($ctx.error.message, $ctx.error.type)
#else
$util.toJson($ctx.result.count)
#end

3. schema.graphqlにtype Mutationフィールドを追加する

# 再掲
type Counter @model @auth(rules: [{ allow: owner }, { allow: private, provider: iam }]){
 id: ID!
 count: Int!
}

type Mutation {
 incrementCount(id: String): Int
   @auth(rules: [{ allow: private, provider: iam }])
}

以上でエラーを再現する準備が出来ました。amplify push --yを実行して確認してください。(もしかすると、Mutation.incrementCountの作成と@auth追加を同時に実行すると、AuthRolePolicy追加時にincrementCountなんて見付からない、的な別のエラーが出るかもしれません。その場合、一旦@authディレクティブをコメントアウトしてからpushし、その後@authディレクティブを加えて再pushしてみてください。)

こいつの倒し方

前回の復習ですが、@authディレクティブは、@aws_iamなどAppSyncで提供されるディレクティブを自動で付与してくれるのと同時に、IAM認証で必要なAuthRole(やUnauthRole)にQueryやMutationを実行するためのpolicyを追加してくれるのでした。なので@authを付けたかったんですね。でもエラーになっちゃうというのが今回のテーマです。

この問題についてStackoverflowで探してみると以下のようdiscussionがありました。

https://github.com/aws-amplify/amplify-cli/issues/5362#issuecomment-756340169

The comment by attilah implies that this should be supported, but currently it only seems to work for a Query field backed by @function at this time according to this comment by yuth

これとその参照先のコメントによると、トップレベルのQueryやMutationフィールドに@authを付与してCLIが良しなにやってくれるのは@functionを使う場合だけで、今回のようにそれ以外のcustom resolverを使うケースだと、CLIがnullを返すresolverを勝手に作成してincrementCountに割り当てようとするため、マニュアルで作ったresolverと衝突して今回のようなエラーが発生するとのことです。

ただし、実際に確認してみたところ、@functionを使わないフィールドの場合でも、type Queryなら問題なく動くようでした。CLIのバージョンの差異かもしれません。

さて、type Mutationフィールドに@authが使えないようなので、@authがやってくれていた二つのこと、つまり、@aws_iamディレクティブを@authの代わりに使い、custom CloudFormation templateでAuthRoleにincrementCountを実行するpolicyを書き足してやる必要があります。

順番に、まずschema.graphqlを以下のように直します。

type Counter @model @auth(rules: [{ allow: owner }, { allow: private, provider: iam }]){
 id: ID!
 count: Int!
}

type Mutation {
 incrementCount(id: String): Int
   @aws_iam
}

そして、上述したCustomResources.jsonの"Resources"フィールドに以下を足します。

"AuthRolePolicy": {
  "Type": "AWS::IAM::Policy",
  "Properties": {
    "PolicyName": {
      "Fn::Sub": [
        "amplify-${AppSyncApiName}-${env}-AuthCustomPolicy",
        {
          "AppSyncApiName": {
            "Ref": "AppSyncApiName"
          },
          "env": {
            "Ref": "env"
          }
        }
      ]
    },
    "Roles": [
      {
        "Ref": "authRoleName"
      }
    ],
    "PolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "appsync:GraphQL"
          ],
          "Resource": [
            {
              "Fn::Sub": [
                "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/fields/${fieldName}",
                {
                  "apiId": {
                    "Ref": "AppSyncApiId"
                  },
                  "typeName": "Mutation",
                  "fieldName": "incrementCount"
                }
              ]
            }
          ]
        }
      ]
    }
  }
}

今回はAuthRoleのみを対象にしましたが、UnauthRoleでもincrementCount実行したい場合は同様に書き加えてください。

最後にamplify pushをして通ることを確認しましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0