この記事は「Amplify x Next.jsハマりどころ」シリーズ第3弾です。
もともとはnoteに書いていたのですが、あまり見てもらえないのとコード記述が読みにくいのでqiitaにせっせと移植中です。
ハマりどころ一覧
- postConfirmation lambda triggerでstorage(dynamoDB)にアクセスしようとすると、Circular dependency between resourcesでpushできない
-
type Query fieldに@aws_iamディレクティブをつけても、authRole(またはunauthRole)にポリシーがアタッチされない
) - type Mutation filedに@authディレクティブをつけるとOnly one resolver is allowed per fieldエラー <- 今回!
- redirect_urlを環境ごとに分けたいんですが
- S3の画像URLを署名なしで取得したいんですが
- aws-exports.jsが.gitignoreの対象になっているためビルド時にエラーになるんですが
- モノレポでamplifyバックエンドを共有したいんですが
- サブドメインでも認証を維持したいんですが
今回はこのNo.3について書いていきたいと思います。
※このシリーズでは、まずはエラーを再現し、そのエラーを修正するというチュートリアル的な流れで書いていきます。結論だけ知りたい方は「こいつの倒し方」まで読み飛ばしてください。
Versions
@aws-amplify/cli: 4.43.0
To Reproduce
今回もエラーを再現してからこいつの倒し方をご紹介したいと思います。
まず、Mutationを追加するには
- stacksを登録する
- resolverを作成する
- 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をして通ることを確認しましょう。