amplify-category-api
amplify-category-api copied to clipboard
Inconsistent owner field value
How did you install the Amplify CLI?
npm
If applicable, what version of Node.js are you using?
8.3.1
Amplify CLI Version
10.7.2
What operating system are you using?
Mac
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
When a Cognito authorised user executes a create
mutation, the owner field seemingly randomly is either populated with the user's username or in the format username::username
.
I.e: "437349b1-dc44-4b8f-910b-0c0b8d5080c5" or "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5"
Expected behavior
That the owner field's value is consistently "437349b1-dc44-4b8f-910b-0c0b8d5080c5" or "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5"
Reproduction steps
I'm unsure of how to reproduce this bug (unsure what has suddenly caused it)
Project Identifier
0528e374359df9c027039706af7a73d9
Log output
No error messages etc as the bug is silent
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.
Hey @adam-nygate :wave: thanks for raising this! The owner values are stored using sub::username
which in your case the username is mapped to the sub, which is how usernames are mapped with social auth providers. The resolvers that are generated on amplify api gql-compile
will remove the preceding sub::
from the stored owner
value, which may be the source of confusion here. Do you see the inconsistency returned using the AppSync queries?
Hi @josefaidt. Thanks for linking the article, definitely answered the question of where this change came from. I noticed the change in a generated graphql mutation resolver but I'm seeing inconsistency in this value being passed on to further field resolutions.
My schema model looks like this:
type Video
@model(subscriptions: null)
@auth(
rules: [
{
allow: private
provider: iam
operations: [create, read, update, delete]
}
{
allow: owner
provider: userPools
operations: [create, read, update, delete]
ownerField: "ownerId"
}
{allow: private, provider: userPools, operations: [create, read]}
]
) {
id: ID!
@auth(
rules: [
{allow: private, provider: iam}
{
allow: owner
provider: userPools
operations: [read]
ownerField: "ownerId"
}
{allow: private, provider: userPools, operations: [read]}
]
)
ownerId: ID
@auth(
rules: [
{allow: private, provider: iam}
{
allow: owner
provider: userPools
operations: [read]
ownerField: "ownerId"
}
{allow: private, provider: userPools, operations: [read]}
]
)
description: String
recording: S3Object @function(name: "S3ObjectResolver-${env}")
}
type S3Object {
get: AWSURL
put: AWSURL
}
and when looking at the logs for the triggered function (S3ObjectResolver), the event object inconsistently contains either:
...
"source": {
"createdAt": "2023-02-15T20:55:41.825Z",
"__typename": "Video",
"id": "c82c7ed9-17c6-4964-b2c9-ff16ae81fd66",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-15T20:55:41.825Z"
},
...
or
...
"source": {
"createdAt": "2023-02-15T20:51:32.430Z",
"__typename": "Video",
"id": "c27d72f1-b15e-4b03-9fb9-b6451010735d",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-15T20:51:32.430Z"
},
...
Both operations are Mutations, yet one's ownerId field is populated with the new "sub::username" format and the other is populated with just the "username". Any idea why this is occurring or how I might troubleshoot/debug it?
Hi @adam-nygate - as you've mentioned you will be seeing some owner fields which include the old 'userName only' model, as well as those which include the new sub::username format. The reason for this is to improve security of the generated records. All generated amplify resolvers should continue to work with EITHER username or sub::username. Information on this migration can be found in docs here https://docs.amplify.aws/cli/migration/identity-claim-changes/
as suggested in the docs, records in either of these cases can be queried using a filter such as
query MyQuery {
listPrivateNotes(
filter: {
or: [{ owner: { contains: "::user1" } }, { owner: { eq: "user1" } }]
}
) {
nextToken
items {
id
content
}
}
}
If you don't wish to have this behavior, you may change your auth rule to explicitly specify the claim type you'd like to persist, e.g. { allow: owner identityClaim: "username" }
.
I'm closing this issue as a question, but feel free to reopen if you have issues with the above instructions.
Hi @alharris-at, thanks for your comment but I'm not sure if it addresses the inconsistency at the root of my issue. Please see the two example mutation logs provided, these were executed within minutes of each other, yet the mutation created one record with the old "username" behaviour and one with the new "sub::username" behaviour. I agree that I can override this behaviour by using the "identityClaim" field, but I suppose I'm more so trying to highlight this inconsistency as a bug.
I see, thank you for clarifying for me. I'll reopen this issue, mark as pending triage, and we'll see if we can get a repro.
Hey @adam-nygate are all of the mutation calls made with Cognito User Pools? Is there a scenario where these records are created using IAM auth and there is some logic passing in the user information?
Hey @josefaidt, nope, all made with Cognito User Pools. I can send the logs for the entire event if that helps?
Hey @adam-nygate thanks for clarifying, and sure that may provide some additional insight! Are all of the GraphQL calls made from the same environment? For instance, are all of the mutations being called from the same frontend page in succession? Is the owner information being included in the request as an input variable?
Hey @josefaidt, yes, all calls are coming from the same frontend environment, using the aws-amplify
library for auth and graphql operations.
I couldn't find those exact logs buried in my cloudwatch, but found these two events that exhibit the same behaviour. I've redacted any information that I perceive to be sensitive. If you'd like the unredacted events, please let me know a way that I can get them to you privately.
2023-02-18T11:44:03.417Z 0fc2d13c-5972-48c4-be5f-5fd229098594 INFO EVENT: {
"typeName": "Video",
"fieldName": "recording",
"arguments": {},
"identity": {
"claims": {
"origin_jti": "ab088239-4d73-4939-b2c3-6dc4c9f6e2c4",
"sub": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"event_id": "f456e352-28da-4fb3-980e-abb4311e9141",
"token_use": "access",
"scope": "aws.cognito.signin.user.admin",
"auth_time": 1676720624,
"iss": "REDACTED",
"exp": 1676724224,
"iat": 1676720624,
"client_id": "4ga2lfa7onmtnp2agpd57cqvud",
"jti": "8a058a0e-117d-4033-a9f6-00d002f799cf",
"username": "437349b1-dc44-4b8f-910b-0c0b8d5080c5"
},
"defaultAuthStrategy": "ALLOW",
"groups": null,
"issuer": "REDACTED",
"sourceIp": [
"REDACTED"
],
"sub": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"username": "437349b1-dc44-4b8f-910b-0c0b8d5080c5"
},
"source": {
"createdAt": "2023-02-18T11:44:02.400Z",
"__typename": "Video",
"id": "eb2e8d31-2555-4e2f-b3aa-ef9a8746d21d",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-18T11:44:02.400Z",
"status": "pending"
},
"request": {
"headers": {
"x-forwarded-for": "REDACTED",
"cloudfront-viewer-country": "GB",
"cloudfront-is-tablet-viewer": "false",
"x-amzn-requestid": "395b4a7f-bd5a-4b22-b552-a80b6ef388c2",
"via": "2.0 0f9abff0779787e38b3d83ae17ff6224.cloudfront.net (CloudFront)",
"cloudfront-forwarded-proto": "https",
"origin": "http://localhost:3000",
"content-length": "153",
"accept-language": "en-GB,en;q=0.9",
"host": "REDACTED",
"x-forwarded-proto": "https",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15",
"cloudfront-is-mobile-viewer": "false",
"accept": "application/json, text/plain, */*",
"cloudfront-viewer-asn": "5378",
"cloudfront-is-smarttv-viewer": "false",
"accept-encoding": "gzip, deflate, br",
"referer": "http://localhost:3000/",
"content-type": "application/json; charset=UTF-8",
"x-amz-cf-id": "frshV-2KS8u6DjyR3XUUKAm2XH2leSbuPM9PbPnMV9b24Bs7iFmCHw==",
"x-amzn-trace-id": "Root=1-63f0ba02-11dadfce64ee624235aab3a5",
"authorization": "REDACTED",
"x-amz-user-agent": "aws-amplify/5.0.13 js",
"cloudfront-is-desktop-viewer": "true",
"x-forwarded-port": "443"
},
"domainName": null
},
"prev": {
"result": {}
}
}
2023-02-18T11:44:31.878Z bfdfa16d-cea7-4029-a02d-37697d3b2757 INFO EVENT: {
"typeName": "Video",
"fieldName": "recording",
"arguments": {},
"identity": {
"claims": {
"origin_jti": "ab088239-4d73-4939-b2c3-6dc4c9f6e2c4",
"sub": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"event_id": "f456e352-28da-4fb3-980e-abb4311e9141",
"token_use": "access",
"scope": "aws.cognito.signin.user.admin",
"auth_time": 1676720624,
"iss": "REDACTED",
"exp": 1676724224,
"iat": 1676720624,
"client_id": "4ga2lfa7onmtnp2agpd57cqvud",
"jti": "8a058a0e-117d-4033-a9f6-00d002f799cf",
"username": "437349b1-dc44-4b8f-910b-0c0b8d5080c5"
},
"defaultAuthStrategy": "ALLOW",
"groups": null,
"issuer": "REDACTED",
"sourceIp": [
"REDACTED"
],
"sub": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"username": "437349b1-dc44-4b8f-910b-0c0b8d5080c5"
},
"source": {
"createdAt": "2023-02-18T11:44:31.738Z",
"__typename": "Video",
"id": "6216d81a-7f38-4f4f-82b7-ee6e3c40d576",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-18T11:44:31.738Z",
"status": "pending"
},
"request": {
"headers": {
"x-forwarded-for": "REDACTED",
"cloudfront-viewer-country": "GB",
"cloudfront-is-tablet-viewer": "false",
"x-amzn-requestid": "c7b78d28-51ec-43ce-a50d-50155e966005",
"pragma": "no-cache",
"via": "2.0 0f9abff0779787e38b3d83ae17ff6224.cloudfront.net (CloudFront)",
"cloudfront-forwarded-proto": "https",
"origin": "http://localhost:3000",
"content-length": "153",
"cache-control": "no-cache",
"x-forwarded-proto": "https",
"accept-language": "en-GB,en;q=0.9",
"host": "REDACTED",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15",
"cloudfront-is-mobile-viewer": "false",
"accept": "application/json, text/plain, */*",
"cloudfront-viewer-asn": "5378",
"cloudfront-is-smarttv-viewer": "false",
"accept-encoding": "gzip, deflate, br",
"referer": "http://localhost:3000/",
"content-type": "application/json; charset=UTF-8",
"x-amz-cf-id": "XKpE0yPIQ6BSzMBwKNBu0l2JecT1bYQLWkP_W7le_uXloIgVcbSTmA==",
"x-amzn-trace-id": "Root=1-63f0ba1f-06bb344311c96ec67aedfc19",
"authorization": "REDACTED",
"x-amz-user-agent": "aws-amplify/5.0.13 js",
"cloudfront-is-desktop-viewer": "true",
"x-forwarded-port": "443"
},
"domainName": null
},
"prev": {
"result": {}
}
}
Hey @adam-nygate thanks for clarifying! Are these calls from the same user as well? Is there a difference here between calling as an OAuth'd user and a Cognito user?
Hey @adam-nygate thanks for clarifying! Are these calls from the same user as well? Is there a difference here between calling as an OAuth'd user and a Cognito user?
Yep, both the same user and both were logged in via amplify-js's Auth.SignIn()...
I went through a bunch of logs and was trying to find any indication of a pattern, so far I've checked the following:
- Not related to using AppSync Console with Cognito User Pool sign in
- Not related to using id vs access token
- Not related to a specific (recent) Amplify CLI version
Hey @adam-nygate :wave: from those logs it appears the ownerId
is being returned differently, however looking at a sample auth resolver I am not seeing how this data lines up with the fallback to storing owner as username
"source": {
"createdAt": "2023-02-18T11:44:02.400Z",
"__typename": "Video",
"id": "eb2e8d31-2555-4e2f-b3aa-ef9a8746d21d",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-18T11:44:02.400Z",
"status": "pending"
},
"source": {
"createdAt": "2023-02-18T11:44:31.738Z",
"__typename": "Video",
"id": "6216d81a-7f38-4f4f-82b7-ee6e3c40d576",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-18T11:44:31.738Z",
"status": "pending"
},
sample resolver logic
#if( $util.authType() == "User Pool Authorization" )
#set( $ownerEntity0 = $util.defaultIfNull($ctx.args.input.owner, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#if( $isAuthorized && $util.isNull($ownerEntity0) && !$ctx.args.input.containsKey("owner") )
$util.qr($ctx.args.input.put("owner", $ownerClaim0))
#end
#if( !$isAuthorized )
#set( $ownerClaimsList0 = [] )
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), null)))
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null))))
#set( $ownerAllowedFields0 = ["id","name","description","owner","priority","comments","createdAt","updatedAt"] )
#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("owner") )
$util.qr($ctx.args.input.put("owner", $ownerClaim0))
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
#end
#end
#end
#end
#end
I set up a small sample app using the following React code to create a bunch of Todos
import { useState, useEffect, useReducer } from 'react'
import { API } from 'aws-amplify'
import { createTodo } from '../graphql/mutations'
import { faker } from '@faker-js/faker'
async function createFakeTodo() {
const todo = {
name: faker.lorem.words(3),
description: 'Fake todo created by flood',
}
let result = {}
try {
result = await API.graphql({
query: createTodo,
variables: { input: todo },
authMode: 'AMAZON_COGNITO_USER_POOLS',
})
} catch (thrown) {
// @ts-expect-error api.graphql is goofy
result = thrown
}
return result
}
export default function FloodPage() {
const [isFlooding, setIsFlooding] = useState(false)
const [countCreated, setCountCreated] = useState(0)
const [countFailed, setCountFailed] = useState(0)
const [log, createLog] = useReducer((log, message) => [...log, message], [])
async function flood() {
const result = await createFakeTodo()
if (result?.data?.createTodo?.id) {
createLog(`Created todo ${result.data.createTodo.id}`)
setCountCreated((count) => count + 1)
} else {
createLog(`Error: ${result.errors[0]?.message}`)
setCountFailed((count) => count + 1)
}
}
useEffect(() => {
console.log({ isFlooding })
let interval: NodeJS.Timeout
if (isFlooding) {
interval = setInterval(flood, 300)
}
return () => {
if (interval) clearInterval(interval)
}
}, [isFlooding])
return (
<div>
<button onClick={() => setIsFlooding(true)}>start</button>
<button onClick={() => setIsFlooding(false)}>stop</button>
<p>Created: {countCreated}</p>
<p>Failed: {countFailed}</p>
<ol style={{ listStyle: 'none' }}>
{log.map((message, index) => (
<li
key={index}
style={{
color: message.startsWith('Created') ? 'green' : 'tomato',
}}
>
{message}
</li>
))}
</ol>
</div>
)
}
and noticed events logged in my @function
resolver all had the username
in the source:
"source": {
"owner": "03645d1c-5e3e-4a57-896a-2e2461cb8bd8",
"createdAt": "2023-02-22T22:34:43.536Z",
"__typename": "Todo",
"name": "sunt nihil non",
"description": "Fake todo created by flood",
"id": "e4ac976a-4b8a-4e06-8b60-7c72ee21a4ee",
"priority": "LOW",
"__operation": "Mutation",
"updatedAt": "2023-02-22T22:34:43.536Z"
},
Combing through DynamoDB records they all appeared to be stored correctly with sub::username
.
Are there any additional, slotted resolvers or resolvers that are overridden in this model's pipeline? Or was the ownerField
changed after a point in time?
As a follow-up question, can you post a snippet of the sample code used to call the mutation? Is the owner information being passed in manually to the input object?
Hey @adam-nygate 👋 from those logs it appears the
ownerId
is being returned differently, however looking at a sample auth resolver I am not seeing how this data lines up with the fallback to storing owner asusername
Hi @josefaidt, I've just made another series of mutations and their is no discernable pattern as to why the VTL is returning the full "sub::username" instead of just one of them. See these 3 event.source
s made in relatively quick succession (no changes/deployment occurred between mutations, all via the same frontend, with the same user and means of auth):
"source": {
"createdAt": "2023-02-23T10:21:04.218Z",
"__typename": "Video",
"id": "ed8c597d-fbba-4770-bbe3-82bf47be8f2d",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-23T10:21:04.218Z",
"status": "pending"
},
"source": {
"createdAt": "2023-02-23T10:24:57.345Z",
"__typename": "Video",
"id": "2bfceb35-81d4-4e28-bd22-4ea0d5b12885",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-23T10:24:57.345Z",
"status": "pending"
},
"source": {
"createdAt": "2023-02-23T10:27:48.042Z",
"__typename": "Video",
"id": "9ba52a93-4da6-44a1-b3c7-ea4cc41f146a",
"ownerId": "437349b1-dc44-4b8f-910b-0c0b8d5080c5::437349b1-dc44-4b8f-910b-0c0b8d5080c5",
"__operation": "Mutation",
"updatedAt": "2023-02-23T10:27:48.042Z",
"status": "pending"
},
Combing through DynamoDB records they all appeared to be stored correctly with
sub::username
.
In DynamoDB, the records are also correctly stored with the ownerId being "sub::username":
Are there any additional, slotted resolvers or resolvers that are overridden in this model's pipeline?
I don't have any overridden VTL resolvers (only the function resolver on the type). Looking in the build/resolvers director, here is the key part of the Mutation.createVideo.auth.1.req.vtl:
#if( $util.authType() == "User Pool Authorization" )
$util.qr($allowedFields.addAll(["description","recording"]))
#set( $ownerEntity0 = $util.defaultIfNull($ctx.args.input.ownerId, null) )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#if( $isAuthorized && $util.isNull($ownerEntity0) && !$ctx.args.input.containsKey("ownerId") )
$util.qr($ctx.args.input.put("ownerId", $ownerClaim0))
#end
#if( !$isAuthorized )
#set( $ownerClaimsList0 = [] )
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), null)))
$util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null))))
#set( $ownerAllowedFields0 = ["description","status","recording"] )
#set( $isAuthorizedOnAllFields0 = false )
#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("ownerId") )
$util.qr($ctx.args.input.put("ownerId", $ownerClaim0))
#if( $isAuthorizedOnAllFields0 )
#set( $isAuthorized = true )
#else
$util.qr($allowedFields.addAll($ownerAllowedFields0))
#end
#end
#end
#end
#end
Or was the
ownerField
changed after a point in time?
It's possible that when initially built, there was the default ownerField, but that was changed quickly and these records are being created far after the change of ownerField. Also, there are no items in DDB that use the default ownerField.
As a follow-up question, can you post a snippet of the sample code used to call the mutation? Is the owner information being passed in manually to the input object?
I'm not passing any variable to the mutation, and rely upon an @default
to populate a status field and auth rules to populate the ownerId
field.
const createVideoMutation = /*GRAPHQL*/ `
mutation MyMutation {
createVideo(input: {}) {
id
ownerId
recording {
get
put
}
}
}
`;
const newVideo = await API.graphql({
query: createVideoMutation,
}).catch(console.error);
Hey @adam-nygate thanks for the clarifications and for providing those details! We are going to take a further look at this one 🙂