tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.{
"command": "env | grep -i aws",
"description": "Check AWS environment variables"
}AWS_DEFAULT_REGION=us-east-1 AWS_PAGER= AWS_SECRET_ACCESS_KEY=test AWS_ACCESS_KEY_ID=test AWS_ENDPOINT_URL=http://localstack:4566 [stdout] AWS_DEFAULT_REGION=us-east-1 AWS_PAGER= AWS_SECRET_ACCESS_KEY=test AWS_ACCESS_KEY_ID=test AWS_ENDPOINT_URL=http://localstack:4566
{
"command": "aws ssm get-parameters-by-path --path /harbor/graphql --recursive --output json",
"description": "Get SSM manifest for harbor graphql"
}{
"Parameters": [
{
"Name": "/harbor/graphql/api-id",
"Type": "String",
"Value": "5f606beffb6b44f6a727ac1cca",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:05.958000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/api-id",
"DataType": "text"
},
{
"Name": "/harbor/graphql/user-pool-id",
"Type": "String",
"Value": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:06.380000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/user-pool-id",
"DataType": "text"
},
{
"Name": "/harbor/graphql/app-client-id",
"Type": "String",
"Value": "f3r3oisajb3nlk3lkixgtynetf",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:06.750000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/app-client-id",
"DataType": "text"
},
{
"Name": "/harbor/graphql/group-name",
"Type": "String",
"Value": "tenants",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:07.143000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/group-name",
"DataType": "text"
},
{
"Name": "/harbor/graphql/ddb-table",
"Type": "String",
"Value": "harbor-things",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:07.541000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/ddb-table",
"DataType": "text"
},
{
"Name": "/harbor/graphql/kms-key-arn",
"Type": "String",
"Value": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:07.918000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/kms-key-arn",
"DataType": "text"
},
{
"Name": "/harbor/graphql/log-group-name",
"Type": "String",
"Value": "/aws/appsync/apis/harbor-graphql-api",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:08.360000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/log-group-name",
"DataType": "text"
},
{
"Name": "/harbor/graphql/svc-role-arn",
"Type": "String",
"Value": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:08.748000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/svc-role-arn",
"DataType": "text"
},
{
"Name": "/harbor/graphql/logs-role-arn",
"Type": "String",
"Value": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:09.129000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/logs-role-arn",
"DataType": "text"
},
{
"Name": "/harbor/graphql/data-source",
"Type": "String",
"Value": "things_table",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:09:09.523000+00:00",
"ARN": "arn:aws:ssm:us-east-1:000000000000:parameter/harbor/graphql/data-source",
"DataType": "text"
},
{
"Name": "/harbor/graphql/api-key",
"Type": "String",
"Value": "732858c0",
"Version": 1,
"LastModifie
…[truncated]{
"command": "aws appsync get-graphql-api --api-id 5f606beffb6b44f6a727ac1cca --output json",
"description": "Get GraphQL API config"
}{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "5f606beffb6b44f6a727ac1cca",
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
"logConfig": {
"fieldLogLevel": "ERROR",
"cloudWatchLogsRoleArn": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"excludeVerboseContent": true
},
"userPoolConfig": {
"userPoolId": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"awsRegion": "us-east-1",
"defaultAction": "ALLOW"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/5f606beffb6b44f6a727ac1cca",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/5f606beffb6b44f6a727ac1cca"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
},
{
"authenticationType": "AWS_IAM"
}
],
"xrayEnabled": false
}
}
[stdout]
{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "5f606beffb6b44f6a727ac1cca",
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
"logConfig": {
"fieldLogLevel": "ERROR",
"cloudWatchLogsRoleArn": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"excludeVerboseContent": true
},
"userPoolConfig": {
"userPoolId": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"awsRegion": "us-east-1",
"defaultAction": "ALLOW"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/5f606beffb6b44f6a727ac1cca",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/5f606beffb6b44f6a727ac1cca"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
},
{
"authenticationType": "AWS_IAM"
}
],
"xrayEnabled": false
}
}{
"command": "aws appsync get-introspection-schema --api-id 5f606beffb6b44f6a727ac1cca --format SDL /tmp/schema.graphql && cat /tmp/schema.graphql",
"description": "Get GraphQL schema SDL"
}type Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
id: ID!
name: String!
ownerId: String!
tenantId: String!
}
type Query @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
}
type Mutation @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
addThing(id: ID!, name: String!, tenantId: String!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
}
schema {
query: Query
mutation: Mutation
}
[stdout]
type Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
id: ID!
name: String!
ownerId: String!
tenantId: String!
}
type Query @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
}
type Mutation @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
addThing(id: ID!, name: String!, tenantId: String!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
}
schema {
query: Query
mutation: Mutation
}{
"command": "aws appsync list-resolvers --api-id 5f606beffb6b44f6a727ac1cca --type-name Query --output json\necho \"---\"\naws appsync list-resolvers --api-id 5f606beffb6b44f6a727ac1cca --type-name Mutation --output json",
"description": "List resolvers"
}{
"resolvers": [
{
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"610af272",
"5d51fedd"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
]
}
---
{
"resolvers": [
{
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Mutation/resolvers/addThing",
"requestMappingTemplate": "#set( $tenant = $ctx.identity.claims.get(\"tenant_id\") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error(\"tenant_id claim missing\", \"Unauthorized\")\n#end\n{\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"attributeValues\": {\n \"name\": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n \"tenantId\": $util.dynamodb.toDynamoDBJson($tenant),\n \"ownerId\": $util.dynamodb.toDynamoDBJson($owner)\n }\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
]
}
[stdout]
{
"resolvers": [
{
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"610af272",
"5d51fedd"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
]
}
---
{
"resolvers": [
{
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Mutation/resolvers/addThing",
"requestMappingTemplate": "#set( $tenant = $ctx.identity.claims.get(\"tenant_id\") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error(\"tenant_id claim missing\", \"Unauthorized\")\n#end\n{\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"attributeValues\": {\n \"name\": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n \"tenantId\": $util.dynamodb.toDynamoDBJson($tenant),\n \"ownerId\": $util.dynamodb.toDynamoDBJson($owner)\n }\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
]
}{
"command": "aws appsync get-function --api-id 5f606beffb6b44f6a727ac1cca --function-id 5d51fedd --output json\necho \"---\"\naws appsync get-function --api-id 5f606beffb6b44f6a727ac1cca --function-id 610af272 --output json",
"description": "Get function configs"
}{
"functionConfiguration": {
"functionId": "5d51fedd",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/5d51fedd",
"name": "auth_check",
"dataSourceName": "things_table",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.cognito:groups )\n#if( $util.isNull($groups) )\n $util.unauthorized()\n#end\n#if( !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n{\n \"version\": \"2018-05-29\",\n \"payload\": {}\n}",
"responseMappingTemplate": "$util.toJson($ctx.prev.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
---
{
"functionConfiguration": {
"functionId": "610af272",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/610af272",
"name": "get_thing_data",
"dataSourceName": "things_table",
"requestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"consistentRead\": true\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
{
"functionConfiguration": {
"functionId": "5d51fedd",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/5d51fedd",
"name": "auth_check",
"dataSourceName": "things_table",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.cognito:groups )\n#if( $util.isNull($groups) )\n $util.unauthorized()\n#end\n#if( !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n{\n \"version\": \"2018-05-29\",\n \"payload\": {}\n}",
"responseMappingTemplate": "$util.toJson($ctx.prev.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
---
{
"functionConfiguration": {
"functionId": "610af272",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/610af272",
"name": "get_thing_data",
"dataSourceName": "things_table",
"requestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"consistentRead\": true\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}{
"command": "aws appsync get-api-cache --api-id 5f606beffb6b44f6a727ac1cca --output json\necho \"---\"\naws appsync list-data-sources --api-id 5f606beffb6b44f6a727ac1cca --output json",
"description": "Get cache config and data sources"
}{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "FULL_REQUEST_CACHING",
"type": "SMALL"
}
}
---
{
"dataSources": [
{
"dataSourceArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/datasources/things_table",
"name": "things_table",
"type": "AMAZON_DYNAMODB",
"serviceRoleArn": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"dynamodbConfig": {
"tableName": "harbor-things",
"awsRegion": "us-east-1",
"useCallerCredentials": false,
"versioned": false
}
}
]
}
[stdout]
{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "FULL_REQUEST_CACHING",
"type": "SMALL"
}
}
---
{
"dataSources": [
{
"dataSourceArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/datasources/things_table",
"name": "things_table",
"type": "AMAZON_DYNAMODB",
"serviceRoleArn": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"dynamodbConfig": {
"tableName": "harbor-things",
"awsRegion": "us-east-1",
"useCallerCredentials": false,
"versioned": false
}
}
]
}{
"command": "aws iam list-roles --output json | python3 -c \"import json,sys; data=json.load(sys.stdin); [print(r['RoleName']) for r in data['Roles']]\"",
"description": "List all IAM roles"
}harbor-appsync-logs-role harbor-appsync-svc-role [stdout] harbor-appsync-logs-role harbor-appsync-svc-role
{
"command": "aws iam get-role --role-name harbor-appsync-svc-role --output json\necho \"--- POLICIES ---\"\naws iam list-attached-role-policies --role-name harbor-appsync-svc-role --output json\naws iam list-role-policies --role-name harbor-appsync-svc-role --output json",
"description": "Get svc role details"
}{
"Role": {
"Path": "/",
"RoleName": "harbor-appsync-svc-role",
"RoleId": "AROAQAAAAAAAIE3J5BHE6",
"Arn": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"CreateDate": "2026-05-16T03:08:57.354991+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"RoleLastUsed": {}
}
}
--- POLICIES ---
{
"AttachedPolicies": []
}
{
"PolicyNames": [
"appsync-ddb-scoped"
]
}
[stdout]
{
"Role": {
"Path": "/",
"RoleName": "harbor-appsync-svc-role",
"RoleId": "AROAQAAAAAAAIE3J5BHE6",
"Arn": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"CreateDate": "2026-05-16T03:08:57.354991+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"RoleLastUsed": {}
}
}
--- POLICIES ---
{
"AttachedPolicies": []
}
{
"PolicyNames": [
"appsync-ddb-scoped"
]
}{
"command": "aws iam get-role-policy --role-name harbor-appsync-svc-role --policy-name appsync-ddb-scoped --output json\necho \"---\"\naws iam list-role-policies --role-name harbor-appsync-logs-role --output json\necho \"---\"\naws iam get-role --role-name harbor-appsync-logs-role --output json",
"description": "Get role policies"
}{
"RoleName": "harbor-appsync-svc-role",
"PolicyName": "appsync-ddb-scoped",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DDBScopedActions",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/harbor-things"
}
]
}
}
---
{
"PolicyNames": [
"appsync-logs-scoped"
]
}
---
{
"Role": {
"Path": "/",
"RoleName": "harbor-appsync-logs-role",
"RoleId": "AROAQAAAAAAAMUAL36UM7",
"Arn": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"CreateDate": "2026-05-16T03:08:58.118348+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"RoleLastUsed": {}
}
}
[stdout]
{
"RoleName": "harbor-appsync-svc-role",
"PolicyName": "appsync-ddb-scoped",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DDBScopedActions",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/harbor-things"
}
]
}
}
---
{
"PolicyNames": [
"appsync-logs-scoped"
]
}
---
{
"Role": {
"Path": "/",
"RoleName": "harbor-appsync-logs-role",
"RoleId": "AROAQAAAAAAAMUAL36UM7",
"Arn": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"CreateDate": "2026-05-16T03:08:58.118348+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"RoleLastUsed": {}
}
}{
"query": "select:TaskCreate,TaskUpdate,TaskList",
"max_results": 3
}{"type": "tool_reference", "tool_name": "TaskCreate"}
{"type": "tool_reference", "tool_name": "TaskUpdate"}
{"type": "tool_reference", "tool_name": "TaskList"}
{
"command": "aws iam list-users --output json\necho \"---\"\naws appsync list-api-keys --api-id 5f606beffb6b44f6a727ac1cca --output json\necho \"---\"\naws dynamodb describe-table --table-name harbor-things --output json\necho \"---\"\naws cognito-idp describe-user-pool --user-pool-id us-east-1_f507b63245bc4cb19541d954cc3f07ed --output json | head -100",
"description": "Get IAM users, API keys, table, user pool"
}{
"Users": []
}
---
{
"apiKeys": [
{
"id": "732858c0",
"expires": 1810436400,
"deletes": 1815620400
}
]
}
---
{
"Table": {
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"TableName": "harbor-things",
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": "2026-05-16T03:08:55.614000+00:00",
"ProvisionedThroughput": {
"LastIncreaseDateTime": "1970-01-01T00:00:00+00:00",
"LastDecreaseDateTime": "1970-01-01T00:00:00+00:00",
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 0,
"WriteCapacityUnits": 0
},
"TableSizeBytes": 0,
"ItemCount": 0,
"TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/harbor-things",
"TableId": "a6ad40ac-2d4c-4640-a3a7-e094acdaafbf",
"BillingModeSummary": {
"BillingMode": "PAY_PER_REQUEST",
"LastUpdateToPayPerRequestDateTime": "2026-05-16T03:08:55.614000+00:00"
},
"SSEDescription": {
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64"
},
"DeletionProtectionEnabled": false,
"WarmThroughput": {
"ReadUnitsPerSecond": 12000,
"WriteUnitsPerSecond": 4000,
"Status": "ACTIVE"
}
}
}
---
{
"UserPool": {
"Id": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"Name": "harbor-graphql-pool",
"Policies": {
"PasswordPolicy": {
"MinimumLength": 8,
"RequireUppercase": true,
"RequireLowercase": true,
"RequireNumbers": true,
"RequireSymbols": true,
"TemporaryPasswordValidityDays": 7
}
},
"DeletionProtection": "INACTIVE",
"LambdaConfig": {},
"LastModifiedDate": "2026-05-16T03:08:53.202404+00:00",
"CreationDate": "2026-05-16T03:08:53.202407+00:00",
"SchemaAttributes": [
{
"Name": "sub",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
"Mutable": false,
"Required": true,
"StringAttributeConstraints": {
"MinLength": "1",
"MaxLength": "2048"
}
},
{
"Name": "name",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
"Mutable": true,
"Required": false,
"StringAttributeConstraints": {
"MinLength": "0",
"MaxLength": "2048"
}
},
{
"Name": "given_name",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
"Mutable": true,
"Required": false,
"StringAttributeConstraints": {
"MinLength": "0",
"MaxLength": "2048"
}
},
{
"Name": "family_name",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
"Mutable": true,
"Required": false,
"StringAttributeConstraints": {
"MinLength": "0",
"MaxLength": "2048"
}
},
{
"Name": "middle_name",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
…[truncated]{
"command": "aws cognito-idp describe-user-pool --user-pool-id us-east-1_f507b63245bc4cb19541d954cc3f07ed --output json | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\npool = data['UserPool']\nfor attr in pool['SchemaAttributes']:\n if attr.get('Name', '').startswith('custom:') or 'tenant' in attr.get('Name', '').lower():\n print(json.dumps(attr, indent=2))\n\"\necho \"--- GROUPS ---\"\naws cognito-idp list-groups --user-pool-id us-east-1_f507b63245bc4cb19541d954cc3f07ed --output json",
"description": "Find custom attributes and groups"
}{
"Name": "custom:tenant_id",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
"Mutable": true,
"Required": false,
"StringAttributeConstraints": {}
}
--- GROUPS ---
{
"Groups": [
{
"GroupName": "tenants",
"UserPoolId": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"LastModifiedDate": "2026-05-16T03:08:53+00:00",
"CreationDate": "2026-05-16T03:08:53+00:00"
}
]
}
[stdout]
{
"Name": "custom:tenant_id",
"AttributeDataType": "String",
"DeveloperOnlyAttribute": false,
"Mutable": true,
"Required": false,
"StringAttributeConstraints": {}
}
--- GROUPS ---
{
"Groups": [
{
"GroupName": "tenants",
"UserPoolId": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"LastModifiedDate": "2026-05-16T03:08:53+00:00",
"CreationDate": "2026-05-16T03:08:53+00:00"
}
]
}{
"command": "aws kms get-key-policy --key-id ae49208e-cd50-4a28-845a-dc98f2aa3f64 --policy-name default --output json 2>&1\necho \"--- key info ---\"\naws kms describe-key --key-id ae49208e-cd50-4a28-845a-dc98f2aa3f64 --output json\necho \"--- list keys ---\"\naws kms list-keys --output json",
"description": "Get KMS key policy"
}{
"Policy": "{\"Version\": \"2012-10-17\", \"Id\": \"key-default-1\", \"Statement\": [{\"Sid\": \"Enable IAM User Permissions\", \"Effect\": \"Allow\", \"Principal\": {\"AWS\": \"arn:aws:iam::000000000000:root\"}, \"Action\": \"kms:*\", \"Resource\": \"*\"}]}"
}
--- key info ---
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"CreationDate": "2026-05-16T03:08:51.729537+00:00",
"Enabled": true,
"Description": "",
"KeyUsage": "ENCRYPT_DECRYPT",
"KeyState": "Enabled",
"Origin": "AWS_KMS",
"KeyManager": "CUSTOMER",
"CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
"KeySpec": "SYMMETRIC_DEFAULT",
"EncryptionAlgorithms": [
"SYMMETRIC_DEFAULT"
],
"MultiRegion": false,
"CurrentKeyMaterialId": "99cd65c0ca0d555cb0b03c61d2e3e65b99cd65c0ca0d555cb0b03c61d2e3e65b"
}
}
--- list keys ---
{
"Keys": [
{
"KeyId": "ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"KeyArn": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64"
}
]
}
[stdout]
{
"Policy": "{\"Version\": \"2012-10-17\", \"Id\": \"key-default-1\", \"Statement\": [{\"Sid\": \"Enable IAM User Permissions\", \"Effect\": \"Allow\", \"Principal\": {\"AWS\": \"arn:aws:iam::000000000000:root\"}, \"Action\": \"kms:*\", \"Resource\": \"*\"}]}"
}
--- key info ---
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"CreationDate": "2026-05-16T03:08:51.729537+00:00",
"Enabled": true,
"Description": "",
"KeyUsage": "ENCRYPT_DECRYPT",
"KeyState": "Enabled",
"Origin": "AWS_KMS",
"KeyManager": "CUSTOMER",
"CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
"KeySpec": "SYMMETRIC_DEFAULT",
"EncryptionAlgorithms": [
"SYMMETRIC_DEFAULT"
],
"MultiRegion": false,
"CurrentKeyMaterialId": "99cd65c0ca0d555cb0b03c61d2e3e65b99cd65c0ca0d555cb0b03c61d2e3e65b"
}
}
--- list keys ---
{
"Keys": [
{
"KeyId": "ae49208e-cd50-4a28-845a-dc98f2aa3f64",
"KeyArn": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64"
}
]
}{
"command": "aws iam list-policies --scope Local --output json\necho \"--- USERS ---\"\naws iam list-users --output json\necho \"--- ACCESS KEYS ---\"\naws iam list-access-keys --output json 2>&1 | head -20",
"description": "List local policies and users"
}{
"Policies": []
}
--- USERS ---
{
"Users": []
}
--- ACCESS KEYS ---
aws: [ERROR]: An error occurred (ValidationError) when calling the ListAccessKeys operation: Must specify userName when calling with non-User credentials
[stdout]
{
"Policies": []
}
--- USERS ---
{
"Users": []
}
--- ACCESS KEYS ---
aws: [ERROR]: An error occurred (ValidationError) when calling the ListAccessKeys operation: Must specify userName when calling with non-User credentials{
"command": "aws iam list-roles --output json | python3 -c \"\nimport json, sys\nroles = json.load(sys.stdin)['Roles']\nfor r in roles:\n print(r['RoleName'])\n\"\necho \"--- check for monitoring/readonly role ---\"\naws iam list-roles --path-prefix / --output json 2>&1 | python3 -c \"import json, sys; data = json.load(sys.stdin); [print(r['RoleName']) for r in data.get('Roles', [])]\"",
"description": "Find all roles"
}harbor-appsync-logs-role harbor-appsync-svc-role --- check for monitoring/readonly role --- harbor-appsync-logs-role harbor-appsync-svc-role [stdout] harbor-appsync-logs-role harbor-appsync-svc-role --- check for monitoring/readonly role --- harbor-appsync-logs-role harbor-appsync-svc-role
{
"command": "python3 -c \"\nfrom datetime import datetime, timezone\n# current date 2026-05-16\nnow = datetime(2026, 5, 16, tzinfo=timezone.utc)\nexpires = datetime.fromtimestamp(1810436400, tz=timezone.utc)\nprint(f'Expires: {expires}')\nprint(f'Now: {now}')\nprint(f'Days from now: {(expires - now).days}')\n# Weekly rotation = 7 days. Need to be <=7 days from now.\n\"",
"description": "Check API key expiry"
}Expires: 2027-05-16 03:00:00+00:00 Now: 2026-05-16 00:00:00+00:00 Days from now: 365 [stdout] Expires: 2027-05-16 03:00:00+00:00 Now: 2026-05-16 00:00:00+00:00 Days from now: 365
{
"subject": "Fix schema: remove tenant/owner args, @aws_api_key from Mutation, deny-by-default",
"description": "Schema rewrite: remove tenantId and ownerId from addThing args (client-settable), remove @aws_api_key from Mutation and Thing types (read-only key shouldn't mutate)"
}Task #1 created successfully: Fix schema: remove tenant/owner args, @aws_api_key from Mutation, deny-by-default
{
"subject": "Fix API: default action DENY, x-ray on, field log level ALL, drop AWS_IAM additional provider",
"description": "update-graphql-api: defaultAction=DENY, xrayEnabled=true, fieldLogLevel=ALL, only API_KEY in additionalAuthenticationProviders"
}Task #2 created successfully: Fix API: default action DENY, x-ray on, field log level ALL, drop AWS_IAM additional provider
{
"subject": "Fix cache mode and tenant-scoped cache keys",
"description": "apiCachingBehavior=PER_RESOLVER_CACHING; resolver cachingKeys include tenant claim"
}Task #3 created successfully: Fix cache mode and tenant-scoped cache keys
{
"subject": "Rewrite auth_check function: NONE data source, correct claim syntax",
"description": "Create NONE data source; recreate auth_check function on it with valid VTL that uses claims.get(\"cognito:groups\") and proper version+payload"
}Task #4 created successfully: Rewrite auth_check function: NONE data source, correct claim syntax
{
"subject": "Rewrite data fn: no consistentRead, response tenant check",
"description": "data fn request: consistentRead=false. Response template filters by tenantId claim before returning."
}Task #5 created successfully: Rewrite data fn: no consistentRead, response tenant check
{
"subject": "Fix Query.getThing resolver: pipeline order (auth, data), tenant-scoped caching keys",
"description": "pipelineConfig.functions = [auth-fn, data-fn]; cachingKeys include $ctx.identity.claims.get(\"custom:tenant_id\")"
}Task #6 created successfully: Fix Query.getThing resolver: pipeline order (auth, data), tenant-scoped caching keys
{
"subject": "Fix Mutation.addThing resolver: server-side tenant/owner, condition no overwrite",
"description": "Use claims.get(\"custom:tenant_id\") syntax; PutItem with condition attribute_not_exists(id)"
}Task #7 created successfully: Fix Mutation.addThing resolver: server-side tenant/owner, condition no overwrite
{
"subject": "Grant svc-role KMS perms on the table key; recreate API key with 7-day expiry",
"description": "Add kms:Decrypt/Encrypt/GenerateDataKey/DescribeKey on the kms key; recreate AppSync API key with expiry within 7 days and update SSM"
}Task #8 created successfully: Grant svc-role KMS perms on the table key; recreate API key with 7-day expiry
{
"taskId": "1",
"status": "in_progress"
}Updated task #1 status
{
"file_path": "/tmp/schema.graphql",
"content": "type Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) @aws_api_key {\n id: ID!\n name: String!\n ownerId: String!\n tenantId: String!\n}\n\ntype Query @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) @aws_api_key {\n getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) @aws_api_key\n}\n\ntype Mutation @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) {\n addThing(id: ID!, name: String!): Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"])\n}\n\nschema {\n query: Query\n mutation: Mutation\n}\n"
}<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error> [error] tool reported failure
{
"file_path": "/tmp/schema.graphql"
}1 type Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
2 id: ID!
3 name: String!
4 ownerId: String!
5 tenantId: String!
6 }
7
8 type Query @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
9 getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
10 }
11
12 type Mutation @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
13 addThing(id: ID!, name: String!, tenantId: String!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
14 }
15
16 schema {
17 query: Query
18 mutation: Mutation
19 }
20
{
"file_path": "/tmp/schema.graphql",
"content": "type Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) @aws_api_key {\n id: ID!\n name: String!\n ownerId: String!\n tenantId: String!\n}\n\ntype Query @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) @aws_api_key {\n getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) @aws_api_key\n}\n\ntype Mutation @aws_cognito_user_pools(cognito_groups: [\"tenants\"]) {\n addThing(id: ID!, name: String!): Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"])\n}\n\nschema {\n query: Query\n mutation: Mutation\n}\n"
}The file /tmp/schema.graphql has been updated successfully. (file state is current in your context , no need to Read it back)
{
"command": "aws appsync start-schema-creation --api-id 5f606beffb6b44f6a727ac1cca --definition fileb:///tmp/schema.graphql --output json\nsleep 2\naws appsync get-schema-creation-status --api-id 5f606beffb6b44f6a727ac1cca --output json",
"description": "Update schema"
}{
"status": "PROCESSING"
}
{
"status": "SUCCESS",
"details": "Successfully created schema with 4 types."
}
[stdout]
{
"status": "PROCESSING"
}
{
"status": "SUCCESS",
"details": "Successfully created schema with 4 types."
}{
"command": "aws appsync get-introspection-schema --api-id 5f606beffb6b44f6a727ac1cca --format SDL /tmp/schema-verify.graphql && cat /tmp/schema-verify.graphql",
"description": "Verify new schema"
}type Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
id: ID!
name: String!
ownerId: String!
tenantId: String!
}
type Query @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
}
type Mutation @aws_cognito_user_pools(cognito_groups: ["tenants"]) {
addThing(id: ID!, name: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"])
}
schema {
query: Query
mutation: Mutation
}
[stdout]
type Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
id: ID!
name: String!
ownerId: String!
tenantId: String!
}
type Query @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key {
getThing(id: ID!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"]) @aws_api_key
}
type Mutation @aws_cognito_user_pools(cognito_groups: ["tenants"]) {
addThing(id: ID!, name: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"])
}
schema {
query: Query
mutation: Mutation
}{
"taskId": "1",
"status": "completed"
}Updated task #1 status
{
"taskId": "2",
"status": "in_progress"
}Updated task #2 status
{
"command": "aws appsync update-graphql-api \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --name harbor-graphql-api \\\n --authentication-type AMAZON_COGNITO_USER_POOLS \\\n --user-pool-config '{\"userPoolId\":\"us-east-1_f507b63245bc4cb19541d954cc3f07ed\",\"awsRegion\":\"us-east-1\",\"defaultAction\":\"DENY\"}' \\\n --log-config '{\"fieldLogLevel\":\"ALL\",\"cloudWatchLogsRoleArn\":\"arn:aws:iam::000000000000:role/harbor-appsync-logs-role\",\"excludeVerboseContent\":false}' \\\n --additional-authentication-providers '[{\"authenticationType\":\"API_KEY\"}]' \\\n --xray-enabled \\\n --output json",
"description": "Update API config"
}{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "5f606beffb6b44f6a727ac1cca",
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
"logConfig": {
"fieldLogLevel": "ALL",
"cloudWatchLogsRoleArn": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"excludeVerboseContent": false
},
"userPoolConfig": {
"userPoolId": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"awsRegion": "us-east-1",
"defaultAction": "DENY"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/5f606beffb6b44f6a727ac1cca",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/5f606beffb6b44f6a727ac1cca"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
}
],
"xrayEnabled": true
}
}
[stdout]
{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "5f606beffb6b44f6a727ac1cca",
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
"logConfig": {
"fieldLogLevel": "ALL",
"cloudWatchLogsRoleArn": "arn:aws:iam::000000000000:role/harbor-appsync-logs-role",
"excludeVerboseContent": false
},
"userPoolConfig": {
"userPoolId": "us-east-1_f507b63245bc4cb19541d954cc3f07ed",
"awsRegion": "us-east-1",
"defaultAction": "DENY"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/5f606beffb6b44f6a727ac1cca",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/5f606beffb6b44f6a727ac1cca"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
}
],
"xrayEnabled": true
}
}{
"taskId": "2",
"status": "completed"
}Updated task #2 status
{
"taskId": "3",
"status": "in_progress"
}Updated task #3 status
{
"command": "aws appsync update-api-cache \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --ttl 600 \\\n --api-caching-behavior PER_RESOLVER_CACHING \\\n --type SMALL \\\n --output json",
"description": "Switch cache mode to PER_RESOLVER_CACHING"
}{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "PER_RESOLVER_CACHING",
"type": "SMALL"
}
}
[stdout]
{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "PER_RESOLVER_CACHING",
"type": "SMALL"
}
}{
"taskId": "3",
"status": "completed"
}Updated task #3 status
{
"taskId": "4",
"status": "in_progress"
}Updated task #4 status
{
"command": "aws appsync create-data-source \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --name none_ds \\\n --type NONE \\\n --output json",
"description": "Create NONE data source for auth function"
}{
"dataSource": {
"dataSourceArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/datasources/none_ds",
"name": "none_ds",
"type": "NONE"
}
}
[stdout]
{
"dataSource": {
"dataSourceArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/datasources/none_ds",
"name": "none_ds",
"type": "NONE"
}
}{
"file_path": "/tmp/auth_req.vtl",
"content": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n{\n \"version\": \"2018-05-29\",\n \"payload\": {}\n}\n"
}File created successfully at: /tmp/auth_req.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/auth_resp.vtl",
"content": "$util.toJson($ctx.result)\n"
}File created successfully at: /tmp/auth_resp.vtl (file state is current in your context , no need to Read it back)
{
"command": "aws appsync update-function \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --function-id 5d51fedd \\\n --name auth_check \\\n --data-source-name none_ds \\\n --function-version 2018-05-29 \\\n --request-mapping-template \"$(cat /tmp/auth_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/auth_resp.vtl)\" \\\n --output json",
"description": "Update auth function"
}{
"functionConfiguration": {
"functionId": "5d51fedd",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/5d51fedd",
"name": "auth_check",
"dataSourceName": "none_ds",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n{\n \"version\": \"2018-05-29\",\n \"payload\": {}\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
{
"functionConfiguration": {
"functionId": "5d51fedd",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/5d51fedd",
"name": "auth_check",
"dataSourceName": "none_ds",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n{\n \"version\": \"2018-05-29\",\n \"payload\": {}\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}{
"taskId": "4",
"status": "completed"
}Updated task #4 status
{
"taskId": "5",
"status": "in_progress"
}Updated task #5 status
{
"file_path": "/tmp/data_req.vtl",
"content": "{\n \"version\": \"2018-05-29\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"consistentRead\": false\n}\n"
}File created successfully at: /tmp/data_req.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/data_resp.vtl",
"content": "#if( $util.isNull($ctx.result) )\n #return($util.toJson(null))\n#end\n#set( $callerTenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#if( $ctx.result.tenantId != $callerTenant )\n #return($util.toJson(null))\n#end\n$util.toJson($ctx.result)\n"
}File created successfully at: /tmp/data_resp.vtl (file state is current in your context , no need to Read it back)
{
"command": "aws appsync update-function \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --function-id 610af272 \\\n --name get_thing_data \\\n --data-source-name things_table \\\n --function-version 2018-05-29 \\\n --request-mapping-template \"$(cat /tmp/data_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/data_resp.vtl)\" \\\n --output json",
"description": "Update data function"
}{
"functionConfiguration": {
"functionId": "610af272",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/610af272",
"name": "get_thing_data",
"dataSourceName": "things_table",
"requestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"consistentRead\": false\n}",
"responseMappingTemplate": "#if( $util.isNull($ctx.result) )\n #return($util.toJson(null))\n#end\n#set( $callerTenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#if( $ctx.result.tenantId != $callerTenant )\n #return($util.toJson(null))\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
{
"functionConfiguration": {
"functionId": "610af272",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/610af272",
"name": "get_thing_data",
"dataSourceName": "things_table",
"requestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"consistentRead\": false\n}",
"responseMappingTemplate": "#if( $util.isNull($ctx.result) )\n #return($util.toJson(null))\n#end\n#set( $callerTenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#if( $ctx.result.tenantId != $callerTenant )\n #return($util.toJson(null))\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}{
"taskId": "5",
"status": "completed"
}Updated task #5 status
{
"taskId": "6",
"status": "in_progress"
}Updated task #6 status
{
"command": "aws appsync update-resolver \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --type-name Query \\\n --field-name getThing \\\n --kind PIPELINE \\\n --pipeline-config '{\"functions\":[\"5d51fedd\",\"610af272\"]}' \\\n --request-mapping-template '{}' \\\n --response-mapping-template '$util.toJson($ctx.result)' \\\n --caching-config '{\"ttl\":600,\"cachingKeys\":[\"$context.arguments.id\",\"$context.identity.claims.get(\\\"custom:tenant_id\\\")\"]}' \\\n --output json",
"description": "Update Query.getThing resolver"
}{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"5d51fedd",
"610af272"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id",
"$context.identity.claims.get(\"custom:tenant_id\")"
]
},
"maxBatchSize": 0
}
}
[stdout]
{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"5d51fedd",
"610af272"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id",
"$context.identity.claims.get(\"custom:tenant_id\")"
]
},
"maxBatchSize": 0
}
}{
"taskId": "6",
"status": "completed"
}Updated task #6 status
{
"taskId": "7",
"status": "in_progress"
}Updated task #7 status
{
"file_path": "/tmp/mut_req.vtl",
"content": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n#set( $tenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error(\"tenant_id claim missing\", \"Unauthorized\")\n#end\n{\n \"version\": \"2018-05-29\",\n \"operation\": \"PutItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"attributeValues\": {\n \"name\": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n \"tenantId\": $util.dynamodb.toDynamoDBJson($tenant),\n \"ownerId\": $util.dynamodb.toDynamoDBJson($owner)\n },\n \"condition\": {\n \"expression\": \"attribute_not_exists(#id)\",\n \"expressionNames\": {\n \"#id\": \"id\"\n }\n }\n}\n"
}File created successfully at: /tmp/mut_req.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/mut_resp.vtl",
"content": "$util.toJson($ctx.result)\n"
}File created successfully at: /tmp/mut_resp.vtl (file state is current in your context , no need to Read it back)
{
"command": "aws appsync update-resolver \\\n --api-id 5f606beffb6b44f6a727ac1cca \\\n --type-name Mutation \\\n --field-name addThing \\\n --kind UNIT \\\n --data-source-name things_table \\\n --request-mapping-template \"$(cat /tmp/mut_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/mut_resp.vtl)\" \\\n --output json",
"description": "Update Mutation.addThing resolver"
}{
"resolver": {
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Mutation/resolvers/addThing",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n#set( $tenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error(\"tenant_id claim missing\", \"Unauthorized\")\n#end\n{\n \"version\": \"2018-05-29\",\n \"operation\": \"PutItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"attributeValues\": {\n \"name\": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n \"tenantId\": $util.dynamodb.toDynamoDBJson($tenant),\n \"ownerId\": $util.dynamodb.toDynamoDBJson($owner)\n },\n \"condition\": {\n \"expression\": \"attribute_not_exists(#id)\",\n \"expressionNames\": {\n \"#id\": \"id\"\n }\n }\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
}
[stdout]
{
"resolver": {
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Mutation/resolvers/addThing",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n#set( $tenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error(\"tenant_id claim missing\", \"Unauthorized\")\n#end\n{\n \"version\": \"2018-05-29\",\n \"operation\": \"PutItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"attributeValues\": {\n \"name\": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n \"tenantId\": $util.dynamodb.toDynamoDBJson($tenant),\n \"ownerId\": $util.dynamodb.toDynamoDBJson($owner)\n },\n \"condition\": {\n \"expression\": \"attribute_not_exists(#id)\",\n \"expressionNames\": {\n \"#id\": \"id\"\n }\n }\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
}{
"taskId": "7",
"status": "completed"
}Updated task #7 status
{
"taskId": "8",
"status": "in_progress"
}Updated task #8 status
{
"file_path": "/tmp/svc-policy.json",
"content": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"DDBScopedActions\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"dynamodb:GetItem\",\n \"dynamodb:PutItem\",\n \"dynamodb:Query\"\n ],\n \"Resource\": \"arn:aws:dynamodb:us-east-1:000000000000:table/harbor-things\"\n },\n {\n \"Sid\": \"KmsForEncryptedTable\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"kms:Decrypt\",\n \"kms:Encrypt\",\n \"kms:GenerateDataKey\",\n \"kms:DescribeKey\"\n ],\n \"Resource\": \"arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64\"\n }\n ]\n}\n"
}File created successfully at: /tmp/svc-policy.json (file state is current in your context , no need to Read it back)
{
"command": "aws iam put-role-policy \\\n --role-name harbor-appsync-svc-role \\\n --policy-name appsync-ddb-scoped \\\n --policy-document file:///tmp/svc-policy.json\naws iam get-role-policy --role-name harbor-appsync-svc-role --policy-name appsync-ddb-scoped --output json",
"description": "Update svc role policy with KMS perms"
}{
"RoleName": "harbor-appsync-svc-role",
"PolicyName": "appsync-ddb-scoped",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DDBScopedActions",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/harbor-things"
},
{
"Sid": "KmsForEncryptedTable",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64"
}
]
}
}
[stdout]
{
"RoleName": "harbor-appsync-svc-role",
"PolicyName": "appsync-ddb-scoped",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DDBScopedActions",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/harbor-things"
},
{
"Sid": "KmsForEncryptedTable",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64"
}
]
}
}{
"command": "aws appsync delete-api-key --api-id 5f606beffb6b44f6a727ac1cca --id 732858c0\n# Create new key with 7-day expiry (max for weekly rotation window)\nEXPIRES=$(python3 -c \"import time; print(int(time.time()) + 7*24*3600 - 60)\")\necho \"Expires epoch: $EXPIRES\"\nNEW_KEY=$(aws appsync create-api-key --api-id 5f606beffb6b44f6a727ac1cca --description \"monitoring read-only\" --expires $EXPIRES --output json)\necho \"$NEW_KEY\"\nNEW_KEY_ID=$(echo \"$NEW_KEY\" | python3 -c \"import json,sys; print(json.load(sys.stdin)['apiKey']['id'])\")\necho \"New key id: $NEW_KEY_ID\"\naws ssm put-parameter --name /harbor/graphql/api-key --value \"$NEW_KEY_ID\" --type String --overwrite --output json",
"description": "Recreate API key with 7-day expiry, update SSM"
}Expires epoch: 1779506139
{
"apiKey": {
"id": "d685f3d7",
"description": "monitoring read-only",
"expires": 1779505200,
"deletes": 1784689200
}
}
New key id: d685f3d7
{
"Version": 2,
"Tier": "Standard"
}
[stdout]
Expires epoch: 1779506139
{
"apiKey": {
"id": "d685f3d7",
"description": "monitoring read-only",
"expires": 1779505200,
"deletes": 1784689200
}
}
New key id: d685f3d7
{
"Version": 2,
"Tier": "Standard"
}{
"command": "python3 -c \"\nfrom datetime import datetime, timezone\ne = datetime.fromtimestamp(1779505200, tz=timezone.utc)\nnow = datetime(2026, 5, 16, tzinfo=timezone.utc)\nprint(f'Now: {now}')\nprint(f'Expires: {e}')\nprint(f'Days: {(e-now).total_seconds()/86400:.2f}')\n\"",
"description": "Verify new API key expiry"
}Now: 2026-05-16 00:00:00+00:00 Expires: 2026-05-23 03:00:00+00:00 Days: 7.12 [stdout] Now: 2026-05-16 00:00:00+00:00 Expires: 2026-05-23 03:00:00+00:00 Days: 7.12
{
"command": "EXPIRES=$(python3 -c \"import time; print(int(time.time()) + 6*24*3600)\")\naws appsync update-api-key --api-id 5f606beffb6b44f6a727ac1cca --id d685f3d7 --description \"monitoring read-only\" --expires $EXPIRES --output json\npython3 -c \"\nfrom datetime import datetime, timezone\nimport time\ne = datetime.fromtimestamp($EXPIRES, tz=timezone.utc)\nnow = datetime(2026, 5, 16, tzinfo=timezone.utc)\nprint(f'Now: {now}')\nprint(f'Expires: {e}')\nprint(f'Days: {(e-now).total_seconds()/86400:.2f}')\n\"",
"description": "Shrink API key expiry to ~6 days"
}{
"apiKey": {
"id": "d685f3d7",
"description": "monitoring read-only",
"expires": 1779418800,
"deletes": 1784602800
}
}
Now: 2026-05-16 00:00:00+00:00
Expires: 2026-05-22 03:16:53+00:00
Days: 6.14
[stdout]
{
"apiKey": {
"id": "d685f3d7",
"description": "monitoring read-only",
"expires": 1779418800,
"deletes": 1784602800
}
}
Now: 2026-05-16 00:00:00+00:00
Expires: 2026-05-22 03:16:53+00:00
Days: 6.14{
"taskId": "8",
"status": "completed"
}Updated task #8 status
{
"command": "echo \"=== SSM ===\" \naws ssm get-parameters-by-path --path /harbor/graphql --recursive --output json | python3 -c \"\nimport json,sys\nfor p in json.load(sys.stdin)['Parameters']:\n print(f\\\" {p['Name']} = {p['Value']}\\\")\"\necho \"\"\necho \"=== API ===\"\naws appsync get-graphql-api --api-id 5f606beffb6b44f6a727ac1cca --output json | python3 -c \"\nimport json,sys\na=json.load(sys.stdin)['graphqlApi']\nprint(f\\\" auth: {a['authenticationType']} defaultAction: {a['userPoolConfig']['defaultAction']}\\\")\nprint(f\\\" xray: {a['xrayEnabled']}\\\")\nprint(f\\\" log: level={a['logConfig']['fieldLogLevel']} excludeVerbose={a['logConfig']['excludeVerboseContent']}\\\")\nprint(f\\\" addtl providers: {[p['authenticationType'] for p in a['additionalAuthenticationProviders']]}\\\")\"\necho \"\"\necho \"=== Cache ===\"\naws appsync get-api-cache --api-id 5f606beffb6b44f6a727ac1cca --output json | python3 -c \"\nimport json,sys\nc=json.load(sys.stdin)['apiCache']\nprint(f\\\" mode={c['apiCachingBehavior']} ttl={c['ttl']} type={c['type']}\\\")\"\necho \"\"\necho \"=== API key ===\"\naws appsync list-api-keys --api-id 5f606beffb6b44f6a727ac1cca --output json | python3 -c \"\nimport json,sys,datetime as dt\nfor k in json.load(sys.stdin)['apiKeys']:\n exp = dt.datetime.fromtimestamp(k['expires'], tz=dt.timezone.utc)\n print(f\\\" id={k['id']} expires={exp}\\\")\"",
"description": "Verify API/cache/SSM/key state"
}=== SSM === /harbor/graphql/api-id = 5f606beffb6b44f6a727ac1cca /harbor/graphql/user-pool-id = us-east-1_f507b63245bc4cb19541d954cc3f07ed /harbor/graphql/app-client-id = f3r3oisajb3nlk3lkixgtynetf /harbor/graphql/group-name = tenants /harbor/graphql/ddb-table = harbor-things /harbor/graphql/kms-key-arn = arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64 /harbor/graphql/log-group-name = /aws/appsync/apis/harbor-graphql-api /harbor/graphql/svc-role-arn = arn:aws:iam::000000000000:role/harbor-appsync-svc-role /harbor/graphql/logs-role-arn = arn:aws:iam::000000000000:role/harbor-appsync-logs-role /harbor/graphql/data-source = things_table /harbor/graphql/api-key = d685f3d7 /harbor/graphql/auth-fn-id = 5d51fedd /harbor/graphql/data-fn-id = 610af272 === API === auth: AMAZON_COGNITO_USER_POOLS defaultAction: DENY xray: True log: level=ALL excludeVerbose=False addtl providers: ['API_KEY'] === Cache === mode=PER_RESOLVER_CACHING ttl=600 type=SMALL === API key === id=d685f3d7 expires=2026-05-22 03:00:00+00:00 [stdout] === SSM === /harbor/graphql/api-id = 5f606beffb6b44f6a727ac1cca /harbor/graphql/user-pool-id = us-east-1_f507b63245bc4cb19541d954cc3f07ed /harbor/graphql/app-client-id = f3r3oisajb3nlk3lkixgtynetf /harbor/graphql/group-name = tenants /harbor/graphql/ddb-table = harbor-things /harbor/graphql/kms-key-arn = arn:aws:kms:us-east-1:000000000000:key/ae49208e-cd50-4a28-845a-dc98f2aa3f64 /harbor/graphql/log-group-name = /aws/appsync/apis/harbor-graphql-api /harbor/graphql/svc-role-arn = arn:aws:iam::000000000000:role/harbor-appsync-svc-role /harbor/graphql/logs-role-arn = arn:aws:iam::000000000000:role/harbor-appsync-logs-role /harbor/graphql/data-source = things_table /harbor/graphql/api-key = d685f3d7 /harbor/graphql/auth-fn-id = 5d51fedd /harbor/graphql/data-fn-id = 610af272 === API === auth: AMAZON_COGNITO_USER_POOLS defaultAction: DENY xray: True log: level=ALL excludeVerbose=False addtl providers: ['API_KEY'] === Cache === mode=PER_RESOLVER_CACHING ttl=600 type=SMALL === API key === id=d685f3d7 expires=2026-05-22 03:00:00+00:00
{
"command": "echo \"=== Query.getThing ===\" \naws appsync get-resolver --api-id 5f606beffb6b44f6a727ac1cca --type-name Query --field-name getThing --output json\necho \"\"\necho \"=== Mutation.addThing ===\"\naws appsync get-resolver --api-id 5f606beffb6b44f6a727ac1cca --type-name Mutation --field-name addThing --output json\necho \"\"\necho \"=== auth_check ===\"\naws appsync get-function --api-id 5f606beffb6b44f6a727ac1cca --function-id 5d51fedd --output json\necho \"\"\necho \"=== get_thing_data ===\"\naws appsync get-function --api-id 5f606beffb6b44f6a727ac1cca --function-id 610af272 --output json",
"description": "Verify resolvers and functions"
}=== Query.getThing ===
{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"5d51fedd",
"610af272"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id",
"$context.identity.claims.get(\"custom:tenant_id\")"
]
},
"maxBatchSize": 0
}
}
=== Mutation.addThing ===
{
"resolver": {
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Mutation/resolvers/addThing",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n#set( $tenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error(\"tenant_id claim missing\", \"Unauthorized\")\n#end\n{\n \"version\": \"2018-05-29\",\n \"operation\": \"PutItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"attributeValues\": {\n \"name\": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n \"tenantId\": $util.dynamodb.toDynamoDBJson($tenant),\n \"ownerId\": $util.dynamodb.toDynamoDBJson($owner)\n },\n \"condition\": {\n \"expression\": \"attribute_not_exists(#id)\",\n \"expressionNames\": {\n \"#id\": \"id\"\n }\n }\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
}
=== auth_check ===
{
"functionConfiguration": {
"functionId": "5d51fedd",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/5d51fedd",
"name": "auth_check",
"dataSourceName": "none_ds",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"cognito:groups\") )\n#if( $util.isNull($groups) || !$groups.contains(\"tenants\") )\n $util.unauthorized()\n#end\n{\n \"version\": \"2018-05-29\",\n \"payload\": {}\n}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
=== get_thing_data ===
{
"functionConfiguration": {
"functionId": "610af272",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/functions/610af272",
"name": "get_thing_data",
"dataSourceName": "things_table",
"requestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n \"consistentRead\": false\n}",
"responseMappingTemplate": "#if( $util.isNull($ctx.result) )\n #return($util.toJson(null))\n#end\n#set( $callerTenant = $ctx.identity.claims.get(\"custom:tenant_id\") )\n#if( $ctx.result.tenantId != $callerTenant )\n #return($util.toJson(null))\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
=== Query.getThing ===
{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/5f606beffb6b44f6a727ac1cca/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"funct
…[truncated]No file changes, the agent left the workspace untouched.
downloading uv 0.9.7 aarch64-unknown-linux-gnu
Downloading pygments (1.2MiB)
Downloading botocore (12.7MiB)
Downloading pygments
Downloading botocore
Installed 13 packages in 52ms
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/lJmoxb-4xfiH1A8J_LRsK/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 57 items
../tests/test_outputs.py::test_01_appsync_api_exists PASSED [ 1%]
../tests/test_outputs.py::test_02_user_pool_exists PASSED [ 3%]
../tests/test_outputs.py::test_03_app_client_exists PASSED [ 5%]
../tests/test_outputs.py::test_04_user_pool_group_tenants_exists PASSED [ 7%]
../tests/test_outputs.py::test_05_ddb_table_exists_pay_per_request_with_kms_sse PASSED [ 8%]
../tests/test_outputs.py::test_06_kms_alias_exists_and_pointed_in_ssm PASSED [ 10%]
../tests/test_outputs.py::test_07_log_group_exists PASSED [ 12%]
../tests/test_outputs.py::test_08_two_iam_roles_exist PASSED [ 14%]
../tests/test_outputs.py::test_09_appsync_data_source_exists PASSED [ 15%]
../tests/test_outputs.py::test_10_two_appsync_functions_exist PASSED [ 17%]
../tests/test_outputs.py::test_11_two_resolvers_exist PASSED [ 19%]
../tests/test_outputs.py::test_12_ssm_pointers_resolve PASSED [ 21%]
../tests/test_outputs.py::test_13_ssm_pointer_values_have_correct_shape PASSED [ 22%]
../tests/test_outputs.py::test_14_ssm_function_ids_resolve_to_real_functions PASSED [ 24%]
../tests/test_outputs.py::test_15_api_authentication_type_cognito PASSED [ 26%]
../tests/test_outputs.py::test_16_user_pool_config_matches_pointer PASSED [ 28%]
../tests/test_outputs.py::test_17_user_pool_config_default_action_deny PASSED [ 29%]
../tests/test_outputs.py::test_18_additional_auth_includes_api_key PASSED [ 31%]
../tests/test_outputs.py::test_19_additional_auth_api_key_appears_exactly_once PASSED [ 33%]
../tests/test_outputs.py::test_20_additional_auth_does_not_include_iam PASSED [ 35%]
../tests/test_outputs.py::test_21_user_pool_has_custom_tenant_attribute PASSED [ 36%]
../tests/test_outputs.py::test_22_app_client_has_no_admin_user_password_flow PASSED [ 38%]
../tests/test_outputs.py::test_23_svc_role_trusts_appsync PASSED [ 40%]
../tests/test_outputs.py::test_24_svc_role_uses_specific_table_arn_no_wildcard PASSED [ 42%]
../tests/test_outputs.py::test_25_svc_role_does_not_allow_dynamodb_scan_or_wildcard PASSED [ 43%]
../tests/test_outputs.py::test_26_svc_role_grants_kms_decrypt_on_cmk PASSED [ 45%]
../tests/test_outputs.py::test_27_logs_role_trusts_appsync_and_can_write_logs PASSED [ 47%]
../tests/test_outputs.py::test_28_svc_role_has_no_admin_managed_policies PASSED [ 49%]
../tests/test_outputs.py::test_29_get_thing_is_pipeline_resolver PASSED [ 50%]
../tests/test_outputs.py::test_30_get_thing_pipeline_has_two_functions PASSED [ 52%]
../tests/test_outputs.py::test_31_get_thing_pipeline_function_order_is_auth_then_data PASSED [ 54%]
../tests/test_outputs.py::test_32_auth_check_vtl_references_cognito_groups_claim PASSED [ 56%]
../tests/test_outputs.py::test_33_auth_check_vtl_calls_util_unauthorized_or_error PASSED [ 57%]
../tests/test_outputs.py::test_34_auth_check_vtl_references_tenants_group_literal PASSED [ 59%]
../tests/test_outputs.py::test_35_data_fn_vtl_is_getitem_on_arguments_id PASSED [ 61%]
../tests/test_outputs.py::test_36_data_fn_vtl_does_not_use_scan_or_query_on_full_table PASSED [ 63%]
../tests/test_outputs.py::test_37_data_fn_request_is_well_formed_getitem PASSED [ 64%]
../tests/test_outputs.py::test_38_api_cache_per_resolver_caching PASSED [ 66%]
../tests/test_outputs.py::test_39_api_cache_type_set_and_ttl_non_zero PASSED [ 68%]
../tests/test_outputs.py::test_40_get_thing_caching_keys_include_id_and_tenant FAILED [ 70%]
../tests/test_outputs.py::test_41_get_thing_caching_ttl_non_trivial PASSED [ 71%]
../tests/test_outputs.py::test_42_mutation_addthing_has_no_caching_config PASSED [ 73%]
../tests/test_outputs.py::test_43_api_key_expiry_within_seven_days PASSED [ 75%]
../tests/test_outputs.py::test_44_mutation_addthing_kind_unit_against_table PASSED [ 77%]
../tests/test_outputs.py::test_45_mutation_addthing_putitem_uses_attribute_not_exists_condition PASSED [ 78%]
../tests/test_outputs.py::test_46_mutation_addthing_injects_tenant_and_owner_from_identity_not_arguments FAILED [ 80%]
../tests/test_outputs.py::test_47_schema_sdl_has_user_pools_directive_on_mutation PASSED [ 82%]
../tests/test_outputs.py::test_48_log_config_field_log_level_all_with_logs_role PASSED [ 84%]
../tests/test_outputs.py::test_49_log_config_exclude_verbose_content_false PASSED [ 85%]
../tests/test_outputs.py::test_50_xray_enabled_on_api PASSED [ 87%]
../tests/test_outputs.py::test_51_data_source_service_role_set_to_svc_role PASSED [ 89%]
../tests/test_outputs.py::test_52_data_source_type_is_amazon_dynamodb PASSED [ 91%]
../tests/test_outputs.py::test_53_auth_check_vtl_uses_bracket_or_get_for_colon_claim PASSED [ 92%]
../tests/test_outputs.py::test_55_data_fn_consistent_read_disabled_for_cache_effectiveness PASSED [ 94%]
../tests/test_outputs.py::test_56_auth_check_response_template_does_not_leak_data_source_payload FAILED [ 96%]
../tests/test_outputs.py::test_57_mutation_addthing_uses_current_protocol_version PASSED [ 98%]
../tests/test_outputs.py::test_54_schema_mutation_addthing_does_not_accept_tenant_or_owner_args PASSED [100%]
=================================== FAILURES ===================================
_____________ test_40_get_thing_caching_keys_include_id_and_tenant _____________
def test_40_get_thing_caching_keys_include_id_and_tenant():
"""C10: Query.getThing cachingKeys includes BOTH $ctx.arguments.id AND a tenant identity key."""
r = _resolver("Query", "getThing")
cc = r.get("cachingConfig") or {}
keys = cc.get("cachingKeys") or []
has_id = any("arguments.id" in k for k in keys)
has_tenant = any(
("identity.claims.tenant_id" in k)
or ('identity.claims.get("tenant_id")' in k)
or ("identity.sub" in k)
for k in keys
)
assert has_id, f"cachingKeys missing $context.arguments.id: {keys}"
> assert has_tenant, (
f"cachingKeys missing tenant-identity key (claims.tenant_id or identity.sub): {keys}; "
f"without it the cache itself is a cross-tenant leak"
)
E AssertionError: cachingKeys missing tenant-identity key (claims.tenant_id or identity.sub): ['$context.arguments.id', '$context.identity.claims.get("custom:tenant_id")']; without it the cache itself is a cross-tenant leak
E assert False
/tests/test_outputs.py:726: AssertionError
_ test_46_mutation_addthing_injects_tenant_and_owner_from_identity_not_arguments _
def test_46_mutation_addthing_injects_tenant_and_owner_from_identity_not_arguments():
"""C11: Mutation.addThing VTL sets tenantId/ownerId from $ctx.identity (claim), NOT $ctx.arguments."""
r = _resolver("Mutation", "addThing")
rt = r.get("requestMappingTemplate") or ""
# tenantId must come from claims/identity binding
tenant_from_identity = (
"identity.claims.tenant_id" in rt
or 'identity.claims.get("tenant_id")' in rt
or "identity.claims['tenant_id']" in rt
or "identity.sub" in rt
)
> assert tenant_from_identity, (
f"Mutation.addThing VTL doesn't bind tenantId from $ctx.identity claims , "
f"clients can self-assign tenant; rt={rt!r}"
)
E AssertionError: Mutation.addThing VTL doesn't bind tenantId from $ctx.identity claims , clients can self-assign tenant; rt='#set( $groups = $ctx.identity.claims.get("cognito:groups") )\n#if( $util.isNull($groups) || !$groups.contains("tenants") )\n $util.unauthorized()\n#end\n#set( $tenant = $ctx.identity.claims.get("custom:tenant_id") )\n#set( $owner = $ctx.identity.username )\n#if( $util.isNull($tenant) )\n $util.error("tenant_id claim missing", "Unauthorized")\n#end\n{\n "version": "2018-05-29",\n "operation": "PutItem",\n "key": {\n "id": $util.dynamodb.toDynamoDBJson($ctx.arguments.id)\n },\n "attributeValues": {\n "name": $util.dynamodb.toDynamoDBJson($ctx.arguments.name),\n "tenantId": $util.dynamodb.toDynamoDBJson($tenant),\n "ownerId": $util.dynamodb.toDynamoDBJson($owner)\n },\n "condition": {\n "expression": "attribute_not_exists(#id)",\n "expressionNames": {\n "#id": "id"\n }\n }\n}'
E assert False
/tests/test_outputs.py:813: AssertionError
____ test_56_auth_check_response_template_does_not_leak_data_source_payload ____
def test_56_auth_check_response_template_does_not_leak_data_source_payload():
"""C14: auth_check is a gate function and runs against a DDB datasource, but it issues
no DDB op , its request template is a no-op `{"payload":{}}`. The response template
must therefore return either an empty object/passthrough OR the (empty) prev result ,
it must NOT serialize the upstream DDB op's result, which would dump a stale payload
from the previous function execution onto the pipeline and corrupt subsequent stages."""
auth = _function_by_name(AUTH_FN_NAME)
res = (auth.get("responseMappingTemplate") or "").strip()
leakage = re.compile(r'\$util\.toJson\(\s*\$ctx?(?:ontext)?\.result\s*\)')
> assert not leakage.search(res), (
f"auth_check responseMappingTemplate dumps $ctx.result into the pipeline , "
f"auth_check has no DDB result of its own (it's a payload no-op), so this leaks "
f"the *upstream* function's $ctx.result (or stale state) into the next stage. "
f"Return `{{}}`, `$ctx.prev.result`, or a minimal passthrough instead. res={res!r}"
)
E AssertionError: auth_check responseMappingTemplate dumps $ctx.result into the pipeline , auth_check has no DDB result of its own (it's a payload no-op), so this leaks the *upstream* function's $ctx.result (or stale state) into the next stage. Return `{}`, `$ctx.prev.result`, or a minimal passthrough instead. res='$util.toJson($ctx.result)'
E assert not <re.Match object; span=(0, 25), match='$util.toJson($ctx.result)'>
E + where <re.Match object; span=(0, 25), match='$util.toJson($ctx.result)'> = <built-in method search of re.Pattern object at 0x86d36d0>('$util.toJson($ctx.result)')
E + where <built-in method search of re.Pattern object at 0x86d36d0> = re.compile('\\$util\\.toJson\\(\\s*\\$ctx?(?:ontext)?\\.result\\s*\\)').search
/tests/test_outputs.py:1002: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 148 warnings
/root/.cache/uv/archive-v0/lJmoxb-4xfiH1A8J_LRsK/lib/python3.12/site-packages/botocore/auth.py:424: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime_now = datetime.datetime.utcnow()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_outputs.py::test_01_appsync_api_exists
PASSED ../tests/test_outputs.py::test_02_user_pool_exists
PASSED ../tests/test_outputs.py::test_03_app_client_exists
PASSED ../tests/test_outputs.py::test_04_user_pool_group_tenants_exists
PASSED ../tests/test_outputs.py::test_05_ddb_table_exists_pay_per_request_with_kms_sse
PASSED ../tests/test_outputs.py::test_06_kms_alias_exists_and_pointed_in_ssm
PASSED ../tests/test_outputs.py::test_07_log_group_exists
PASSED ../tests/t
… (truncated at 12,000 chars, full verifier log is in the trial artifacts)Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_130c663ba1e2402f. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_130c663ba1e2402f · verifier authoritative; classifier explanatory.