amplify-category-api
amplify-category-api copied to clipboard
AppSync GraphQL API Inconsistency After Transformer v2 Upgrade
How did you install the Amplify CLI?
yarn
If applicable, what version of Node.js are you using?
20.1.0
Amplify CLI Version
12.0.0
What operating system are you using?
macOS 13.3.1 (a)
Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.
No manual changes made
Describe the bug
We have a GraphQL API that is deployed to two different environments: staging and production. The API has been fully pushed to both environments.
For some unknown reason, the behavior between the two environments is different for at least one mutation. We have a model called UserPlan. Here is the type configuration:
type UserPlan
@model(subscriptions: { level: public })
@auth(rules: [
{ allow: owner, ownerField: "userPlanUserId", identityClaim: "username" },
{ allow: groups, groups: ["Admin"] }
]) {
id: ID!
userPlanPlanId: ID! @index(name: "byPlan")
plan: Plan! @hasOne(fields: ["userPlanPlanId"])
userPlanUserId: ID @index(name: "byUser", sortKeyFields: ["updatedAt"]) @auth(rules: [{ allow: owner, ownerField: "userPlanUserId", identityClaim: "username", operations: [create, read, delete] }, { allow: groups, groups: ["Admin"] }])
user: User! @belongsTo(fields: ["userPlanUserId"])
completedGuides: [UserCompletedGuide!]! @hasMany(indexName: "byUserPlan", fields: ["id"])
updatedAt: AWSDateTime!
status: UserPlanStatus!
}
We we create the UserPlan, we only pass the userPlanPlanId and status. The expectation is that the userPlanUserId is set automatically by the API to the current user. When we do this on production, it works correctly: the userPlanUserId is set by the API. When we do this on staging, it does not work; the userPlanUserId is not specified.
I have compared the MutationcreateUserPlanauth0Function and MutationCreateUserPlanDataResolverFn request mapping template code for both environments, and they are identical.
We have one other model with the same type of configuration (auto-set the user), and it has the same behavior: it works correctly on production but not on staging. So, at least the behavior between the two is consistent.
We recently upgraded to GraphQL Transformer v2, which I believe coincides with when this issue started happening. On transformer v1, the behavior worked on both staging and production, but after the upgrade, it only works on production, not staging.
Does anyone have any ideas on what we might try to diagnose or fix the issue? Thanks in advance!
Expected behavior
The behavior should be consistent across environments with the same schema, but it is not.
Reproduction steps
It's possible creating a schema on GraphQL transformer v1 and then upgrading to v2 might trigger it, but I have no idea why one environment is different than another 🤔
Project Identifier
Project Identifier: ba1eb772426b1ec5577639449537b5a9
Log output
# Put your logs below this line
Additional information
No response
Before submitting, please confirm:
- [X] I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
- [X] I have removed any sensitive information from my code snippets and submission.
I just tried creating a new dev environment to see what happens. The new environment has the same issue as staging where the users are not being automatically associated with newly created UserPlan records.
The only difference I've been able to identify is in the aws-exports.js. For the dev and staging environments (where it's not working), they have "federationTarget": "COGNITO_USER_POOLS". On production, where it is working, they have "federationTarget": "COGNITO_USER_AND_IDENTITY_POOLS". Could that have something to do with it? What would account for this difference? I'm not sure I can explain why production is different...
It's worth mentioning that we're testing with the same exact client code with the only change being aws-exports.js.
We are passing the jwtToken for the auth to the Apollo Client. I've checked a JWT for both production and dev environments, and I can confirm that they both have a cognito:username attribute.
Here's the relevant part of the request mapping template:
$util.qr($ctx.stash.put("hasAuth", true))
#set( $inputFields = $util.parseJson($util.toJson($ctx.args.input.keySet())) )
#set( $isAuthorized = false )
#set( $allowedFields = [] )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["us-west-2_XXX_Full-access/CognitoIdentityCredentials","us-west-2_XXX_Manage-only/CognitoIdentityCredentials"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
$util.unauthorized()
#end
#if( $util.authType() == "User Pool Authorization" )
#if( !$isAuthorized )
#set( $staticGroupRoles = [{"claim":"cognito:groups","entity":"Admin","allowedFields":["id","userPlanPlanId","plan","userPlanUserId","user","completedGuides","updatedAt","status","userUserPlansId"],"isAuthorizedOnAllFields":true}] )
#foreach( $groupRole in $staticGroupRoles )
#set( $groupsInToken = $util.defaultIfNull($ctx.identity.claims.get($groupRole.claim), []) )
#if( $groupsInToken.contains($groupRole.entity) )
#if( $groupRole.isAuthorizedOnAllFields )
#set( $isAuthorized = true )
#break
#else
$util.qr($allowedFields.addAll($groupRole.allowedFields))
#end
#end
#end
#end
#set( $ownerEntity0 = $util.defaultIfNull($ctx.args.input.userPlanUserId, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) )
#if( !$isAuthorized )
#set( $ownerClaimsList0 = [] )
#set( $ownerAllowedFields0 = ["id","userPlanPlanId","plan","userPlanUserId","user","completedGuides","updatedAt","status","userUserPlansId"] )
#set( $isAuthorizedOnAllFields0 = true )
#if( $ownerClaim0 == $ownerEntity0 || $ownerClaimsList0.contains($ownerEntity0) )
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
#end
#end
#if( $util.isNull($ownerEntity0) && !$ctx.args.input.containsKey("userPlanUserId") )
$util.qr($ctx.args.input.put("userPlanUserId", $ownerClaim0))
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
#end
#end
#end
#end
#end
All 3 environments have this set in aws-exports.js: "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS". As such, I would think the $util.authType() == "User Pool Authorization" check should always be true for all 3 environments.
I enabled AppSync verbose logging on the dev environment to see if it revealed anything useful. Here's the BeforeMapping for one of these requests (some parts redacted for privacy):
{
"logType": "BeforeMapping",
"path": [
"createUserPlan"
],
"fieldName": "createUserPlan",
"resolverArn": "arn:aws:appsync:us-west-2:XXX:apis/XXX/types/Mutation/resolvers/createUserPlan",
"requestId": "7a24124f-9387-438c-bf67-2368cddb6216",
"context": {
"arguments": {
"input": {
"userPlanPlanId": "id123",
"status": "active"
}
},
"stash": {
"authRole": "arn:aws:sts::XXX:assumed-role/amplify-XXX-dev-XXX-authRole/CognitoIdentityCredentials",
"conditions": [],
"connectionAttributes": {},
"fieldName": "createUserPlan",
"metadata": {
"dataSourceType": "AMAZON_DYNAMODB",
"apiId": "XXX"
},
"tableName": "UserPlan-XXX-dev",
"typeName": "Mutation",
"unauthRole": "arn:aws:sts::XXX:assumed-role/amplify-XXX-dev-XXX-unauthRole/CognitoIdentityCredentials"
},
"outErrors": []
},
"fieldInError": false,
"errors": [],
"parentType": "Mutation",
"graphQLAPIId": "XXX",
"transformedTemplate": "\n\n\n\n\n\n\n\n\n\n{}"
}
I compared it to a production log, and they are very similar:
{
"logType": "BeforeMapping",
"path": [
"createUserPlan"
],
"fieldName": "createUserPlan",
"resolverArn": "arn:aws:appsync:us-west-2:XXX:apis/XXX/types/Mutation/resolvers/createUserPlan",
"requestId": "8513e276-0b57-4175-be93-d2f4ca1d27ad",
"context": {
"arguments": {
"input": {
"userPlanPlanId": "id123",
"status": "active"
}
},
"stash": {
"authRole": "arn:aws:sts::XXX:assumed-role/XXX-postlive-XXX-authRole/CognitoIdentityCredentials",
"conditions": [],
"connectionAttributes": {},
"fieldName": "createUserPlan",
"metadata": {
"dataSourceType": "AMAZON_DYNAMODB",
"apiId": "XXX"
},
"tableName": "UserPlan-XXX-postlive",
"typeName": "Mutation",
"unauthRole": "arn:aws:sts::XXX:assumed-role/XXX-postlive-XXX-unauthRole/CognitoIdentityCredentials"
},
"outErrors": []
},
"fieldInError": false,
"errors": [],
"parentType": "Mutation",
"graphQLAPIId": "XXX",
"transformedTemplate": "\n\n\n\n\n\n\n\n\n\n{}"
}
Here's the RequestMapping for dev, which does not have the userPlanUserId set:
{
"logType": "RequestMapping",
"fieldName": "createUserPlan",
"resolverArn": "arn:aws:appsync:us-west-2:XXX:apis/XXX/types/Mutation/resolvers/createUserPlan",
"functionName": "MutationcreateUserPlanauth0Function",
"fieldInError": false,
"parentType": "Mutation",
"path": [
"createUserPlan"
],
"requestId": "7a24124f-9387-438c-bf67-2368cddb6216",
"context": {
"arguments": {
"input": {
"userPlanPlanId": "id123",
"status": "active"
}
},
"prev": {
"result": {}
},
"stash": {
"authRole": "arn:aws:sts::XXX:assumed-role/amplify-XXX-dev-XXX-authRole/CognitoIdentityCredentials",
"conditions": [],
"connectionAttributes": {},
"defaultValues": {
"id": "f9a7eb04-73c5-4843-9edd-d71138716013",
"createdAt": "2023-05-18T22:40:09.361Z",
"updatedAt": "2023-05-18T22:40:09.361Z"
},
"fieldName": "createUserPlan",
"hasAuth": true,
"metadata": {
"dataSourceType": "AMAZON_DYNAMODB",
"apiId": "XXX"
},
"tableName": "UserPlan-XXX-dev",
"typeName": "Mutation",
"unauthRole": "arn:aws:sts::XXX:assumed-role/amplify-XXX-dev-XXX-unauthRole/CognitoIdentityCredentials"
},
"outErrors": []
},
"errors": [],
"graphQLAPIId": "XXX",
"functionArn": "arn:aws:appsync:us-west-2:XXX:apis/XXX/functions/z4x5zq5o6zatnc7736kfh7nyd4",
"transformedTemplate": "\n \n {\"version\":\"2018-05-29\",\"payload\":{}}\n"
}
Here it is for production, where the userPlanUserId has been set:
{
"logType": "RequestMapping",
"fieldName": "createUserPlan",
"resolverArn": "arn:aws:appsync:us-west-2:XXX:apis/XXX/types/Mutation/resolvers/createUserPlan",
"functionName": "MutationcreateUserPlanauth0Function",
"fieldInError": false,
"parentType": "Mutation",
"path": [
"createUserPlan"
],
"requestId": "8513e276-0b57-4175-be93-d2f4ca1d27ad",
"context": {
"arguments": {
"input": {
"userPlanPlanId": "recYmO2nlllxYU9qN",
"status": "active",
"userPlanUserId": "73cdb831-6751-44fc-9e73-75447e6985de"
}
},
"prev": {
"result": {}
},
"stash": {
"authRole": "arn:aws:sts::XXX:assumed-role/XXX-postlive-XXX-authRole/CognitoIdentityCredentials",
"conditions": [],
"connectionAttributes": {},
"defaultValues": {
"id": "919b20b1-117c-4460-a045-7a7626c5b021",
"createdAt": "2023-05-18T22:49:16.042Z",
"updatedAt": "2023-05-18T22:49:16.042Z"
},
"fieldName": "createUserPlan",
"hasAuth": true,
"metadata": {
"dataSourceType": "AMAZON_DYNAMODB",
"apiId": "XXX"
},
"tableName": "UserPlan-XXX-postlive",
"typeName": "Mutation",
"unauthRole": "arn:aws:sts::XXX:assumed-role/XXX-postlive-XXX-unauthRole/CognitoIdentityCredentials"
},
"outErrors": []
},
"errors": [],
"graphQLAPIId": "XXX",
"functionArn": "arn:aws:appsync:us-west-2:XXX:apis/XXX/functions/it4upgtqgnbmfgwofhft2h7kly",
"transformedTemplate": "\n \n \n {\"version\":\"2018-05-29\",\"payload\":{}}\n"
}
So this confirms that this is where the issue lies, but the question is: why?? 🤯