tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.{
"command": "echo \"AWS_ENDPOINT_URL=$AWS_ENDPOINT_URL\"; echo \"ENFORCE_IAM=$ENFORCE_IAM\"; aws --version",
"description": "Check environment"
}AWS_ENDPOINT_URL=http://localstack:4566 ENFORCE_IAM= aws-cli/2.34.48 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 [stdout] AWS_ENDPOINT_URL=http://localstack:4566 ENFORCE_IAM= aws-cli/2.34.48 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24
{
"command": "aws ssm get-parameters-by-path --path /harbor/graphql --recursive --region us-east-1 --output json",
"description": "Read SSM manifest"
}{
"Parameters": [
{
"Name": "/harbor/graphql/api-id",
"Type": "String",
"Value": "39f7f68a3b784fa9b61fdb342a",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:18:24.072000+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_0607c2453cba44bb989b8ff4c2cc5bb5",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:18:24.436000+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": "lcyyhfqzgcvvtfqflw7lzvlmo6",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:18:24.835000+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:18:25.229000+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:18:25.602000+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/e5e23397-e67a-4b37-9032-e6eaa36ff13e",
"Version": 1,
"LastModifiedDate": "2026-05-16T03:18:25.976000+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:18:26.367000+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:18:26.753000+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:18:27.140000+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:18:27.505000+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": "87e2a505",
"Version": 1,
"LastModifie
…[truncated]{
"command": "aws appsync get-graphql-api --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json",
"description": "Get AppSync API details"
}{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "39f7f68a3b784fa9b61fdb342a",
"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_0607c2453cba44bb989b8ff4c2cc5bb5",
"awsRegion": "us-east-1",
"defaultAction": "ALLOW"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/39f7f68a3b784fa9b61fdb342a",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/39f7f68a3b784fa9b61fdb342a"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
},
{
"authenticationType": "AWS_IAM"
}
],
"xrayEnabled": false
}
}
[stdout]
{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "39f7f68a3b784fa9b61fdb342a",
"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_0607c2453cba44bb989b8ff4c2cc5bb5",
"awsRegion": "us-east-1",
"defaultAction": "ALLOW"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/39f7f68a3b784fa9b61fdb342a",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/39f7f68a3b784fa9b61fdb342a"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
},
{
"authenticationType": "AWS_IAM"
}
],
"xrayEnabled": false
}
}{
"command": "aws appsync list-data-sources --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json",
"description": "List data sources"
}{
"dataSources": [
{
"dataSourceArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/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]
{
"dataSources": [
{
"dataSourceArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/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 appsync list-functions --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json",
"description": "List functions"
}{
"functions": [
{
"functionId": "70867cab",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/70867cab",
"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
},
{
"functionId": "2c1e403b",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/2c1e403b",
"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]
{
"functions": [
{
"functionId": "70867cab",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/70867cab",
"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
},
{
"functionId": "2c1e403b",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/2c1e403b",
"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 list-api-keys --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json",
"description": "List API keys"
}{
"apiKeys": [
{
"id": "87e2a505",
"expires": 1810436400,
"deletes": 1815620400
}
]
}
[stdout]
{
"apiKeys": [
{
"id": "87e2a505",
"expires": 1810436400,
"deletes": 1815620400
}
]
}{
"command": "aws appsync get-introspection-schema --api-id 39f7f68a3b784fa9b61fdb342a --format SDL --region us-east-1 /tmp/schema.graphql 2>&1; cat /tmp/schema.graphql 2>/dev/null || aws appsync get-introspection-schema --api-id 39f7f68a3b784fa9b61fdb342a --format SDL --region us-east-1 /tmp/schema.txt; ls -la /tmp/",
"description": "Get 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"]) @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
}
total 16
drwxrwxrwt 1 root root 4096 May 16 03:19 .
drwxr-xr-x 1 root root 4096 May 16 03:18 ..
drwx------ 3 root root 4096 May 16 03:19 claude-0
-rw-r--r-- 1 root root 599 May 16 03:19 schema.graphql
[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
}
total 16
drwxrwxrwt 1 root root 4096 May 16 03:19 .
drwxr-xr-x 1 root root 4096 May 16 03:18 ..
drwx------ 3 root root 4096 May 16 03:19 claude-0
-rw-r--r-- 1 root root 599 May 16 03:19 schema.graphql{
"command": "aws appsync get-api-cache --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json 2>&1",
"description": "Get cache config"
}{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "FULL_REQUEST_CACHING",
"type": "SMALL"
}
}
[stdout]
{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "FULL_REQUEST_CACHING",
"type": "SMALL"
}
}{
"command": "aws appsync list-resolvers --api-id 39f7f68a3b784fa9b61fdb342a --type-name Query --region us-east-1 --output json",
"description": "List Query resolvers"
}{
"resolvers": [
{
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"2c1e403b",
"70867cab"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
]
}
[stdout]
{
"resolvers": [
{
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"2c1e403b",
"70867cab"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
]
}{
"command": "aws appsync list-resolvers --api-id 39f7f68a3b784fa9b61fdb342a --type-name Mutation --region us-east-1 --output json",
"description": "List Mutation resolvers"
}{
"resolvers": [
{
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/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": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/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 iam get-role --role-name harbor-appsync-svc-role --region us-east-1 --output json",
"description": "Get service role"
}{
"Role": {
"Path": "/",
"RoleName": "harbor-appsync-svc-role",
"RoleId": "AROAQAAAAAAAJIQZRI73Y",
"Arn": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"CreateDate": "2026-05-16T03:18:14.242760+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"RoleLastUsed": {}
}
}
[stdout]
{
"Role": {
"Path": "/",
"RoleName": "harbor-appsync-svc-role",
"RoleId": "AROAQAAAAAAAJIQZRI73Y",
"Arn": "arn:aws:iam::000000000000:role/harbor-appsync-svc-role",
"CreateDate": "2026-05-16T03:18:14.242760+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"RoleLastUsed": {}
}
}{
"command": "aws iam list-role-policies --role-name harbor-appsync-svc-role --region us-east-1 --output json; aws iam list-attached-role-policies --role-name harbor-appsync-svc-role --region us-east-1 --output json",
"description": "List svc role policies"
}{
"PolicyNames": [
"appsync-ddb-scoped"
]
}
{
"AttachedPolicies": []
}
[stdout]
{
"PolicyNames": [
"appsync-ddb-scoped"
]
}
{
"AttachedPolicies": []
}{
"command": "aws iam list-role-policies --role-name harbor-appsync-logs-role --region us-east-1 --output json; aws iam list-attached-role-policies --role-name harbor-appsync-logs-role --region us-east-1 --output json",
"description": "List logs role policies"
}{
"PolicyNames": [
"appsync-logs-scoped"
]
}
{
"AttachedPolicies": [
{
"PolicyName": "AWSAppSyncPushToCloudWatchLogs",
"PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs"
}
]
}
[stdout]
{
"PolicyNames": [
"appsync-logs-scoped"
]
}
{
"AttachedPolicies": [
{
"PolicyName": "AWSAppSyncPushToCloudWatchLogs",
"PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs"
}
]
}{
"command": "aws dynamodb describe-table --table-name harbor-things --region us-east-1 --output json",
"description": "Describe DDB table"
}{
"Table": {
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"TableName": "harbor-things",
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": "2026-05-16T03:18:12.053000+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": "60bfc046-74e2-4d90-9da8-e7fcef4d40d3",
"BillingModeSummary": {
"BillingMode": "PAY_PER_REQUEST",
"LastUpdateToPayPerRequestDateTime": "2026-05-16T03:18:12.053000+00:00"
},
"SSEDescription": {
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e"
},
"DeletionProtectionEnabled": false,
"WarmThroughput": {
"ReadUnitsPerSecond": 12000,
"WriteUnitsPerSecond": 4000,
"Status": "ACTIVE"
}
}
}
[stdout]
{
"Table": {
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"TableName": "harbor-things",
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": "2026-05-16T03:18:12.053000+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": "60bfc046-74e2-4d90-9da8-e7fcef4d40d3",
"BillingModeSummary": {
"BillingMode": "PAY_PER_REQUEST",
"LastUpdateToPayPerRequestDateTime": "2026-05-16T03:18:12.053000+00:00"
},
"SSEDescription": {
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e"
},
"DeletionProtectionEnabled": false,
"WarmThroughput": {
"ReadUnitsPerSecond": 12000,
"WriteUnitsPerSecond": 4000,
"Status": "ACTIVE"
}
}
}{
"command": "aws iam get-role-policy --role-name harbor-appsync-svc-role --policy-name appsync-ddb-scoped --region us-east-1 --output json",
"description": "Get svc role policy"
}{
"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"
}
]
}
}
[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"
}
]
}
}{
"command": "aws iam get-role-policy --role-name harbor-appsync-logs-role --policy-name appsync-logs-scoped --region us-east-1 --output json",
"description": "Get logs role policy"
}{
"RoleName": "harbor-appsync-logs-role",
"PolicyName": "appsync-logs-scoped",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/appsync/apis/harbor-graphql-api:*"
}
]
}
}
[stdout]
{
"RoleName": "harbor-appsync-logs-role",
"PolicyName": "appsync-logs-scoped",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/appsync/apis/harbor-graphql-api:*"
}
]
}
}{
"command": "aws kms describe-key --key-id arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e --region us-east-1 --output json",
"description": "Describe KMS key"
}{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "e5e23397-e67a-4b37-9032-e6eaa36ff13e",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e",
"CreationDate": "2026-05-16T03:18:07.998500+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": "bdbe79661cdf5b918a04f0db301ce09bbdbe79661cdf5b918a04f0db301ce09b"
}
}
[stdout]
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "e5e23397-e67a-4b37-9032-e6eaa36ff13e",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e",
"CreationDate": "2026-05-16T03:18:07.998500+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": "bdbe79661cdf5b918a04f0db301ce09bbdbe79661cdf5b918a04f0db301ce09b"
}
}{
"command": "aws kms get-key-policy --key-id arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e --policy-name default --region us-east-1 --output json 2>&1",
"description": "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\": \"*\"}]}"
}
[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\": \"*\"}]}"
}{
"command": "date +%s; date -d \"+6 days\" +%s; date",
"description": "Compute expiry timestamps"
}1778901806 1779420206 Sat May 16 03:23:26 UTC 2026 [stdout] 1778901806 1779420206 Sat May 16 03:23:26 UTC 2026
{
"command": "mkdir -p /tmp/harbor",
"description": "Make workspace dir"
}(Bash completed with no output)
{
"file_path": "/tmp/harbor/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!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: [\"tenants\"])\n}\n\nschema {\n query: Query\n mutation: Mutation\n}\n"
}File created successfully at: /tmp/harbor/schema.graphql (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/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\": \"KMSForEncryptedDDB\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"kms:Decrypt\",\n \"kms:Encrypt\",\n \"kms:GenerateDataKey\",\n \"kms:ReEncrypt*\",\n \"kms:DescribeKey\"\n ],\n \"Resource\": \"arn:aws:kms:us-east-1:000000000000:key/e5e23397-e67a-4b37-9032-e6eaa36ff13e\"\n }\n ]\n}\n"
}File created successfully at: /tmp/harbor/svc_policy.json (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/auth_req.vtl",
"content": "#set( $groups = $ctx.identity.claims.get(\"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}\n"
}File created successfully at: /tmp/harbor/auth_req.vtl (file state is current in your context , no need to Read it back)
{
"query": "select:TaskCreate,TaskUpdate",
"max_results": 5
}{"type": "tool_reference", "tool_name": "TaskCreate"}
{"type": "tool_reference", "tool_name": "TaskUpdate"}
{
"subject": "Fix get_thing_data function VTL",
"description": "consistentRead=false; tenant filter and error handling in response template"
}Task #1 created successfully: Fix get_thing_data function VTL
{
"subject": "Replace schema SDL",
"description": "Drop tenantId arg from addThing and remove @aws_api_key from Mutation type and field"
}Task #2 created successfully: Replace schema SDL
{
"subject": "Fix auth_check function VTL",
"description": "Use claims.get(\"cognito:groups\") for the colon-containing claim name"
}Task #3 created successfully: Fix auth_check function VTL
{
"subject": "Rotate API key into weekly window",
"description": "Set expires to ~6 days from now to fit the weekly-rotation policy"
}Task #4 created successfully: Rotate API key into weekly window
{
"subject": "Update AppSync API config",
"description": "defaultAction=DENY, xrayEnabled=true, fieldLogLevel=ALL, drop AWS_IAM from additionalAuthenticationProviders"
}Task #5 created successfully: Update AppSync API config
{
"subject": "Switch API cache to PER_RESOLVER_CACHING",
"description": "apiCachingBehavior=PER_RESOLVER_CACHING so per-resolver cachingKeys apply"
}Task #6 created successfully: Switch API cache to PER_RESOLVER_CACHING
{
"subject": "Fix Query.getThing resolver",
"description": "pipeline order [auth, data]; cachingKeys [tenant_id claim, arguments.id]"
}Task #7 created successfully: Fix Query.getThing resolver
{
"subject": "Fix Mutation.addThing resolver",
"description": "protocol version 2018-05-29; attribute_not_exists condition; tenant from claim"
}Task #8 created successfully: Fix Mutation.addThing resolver
{
"subject": "Update IAM svc role with KMS perms",
"description": "Add KMS Decrypt/Encrypt/GenerateDataKey/DescribeKey on the table CMK to harbor-appsync-svc-role inline policy"
}Task #9 created successfully: Update IAM svc role with KMS perms
{
"command": "aws iam put-role-policy --role-name harbor-appsync-svc-role --policy-name appsync-ddb-scoped --policy-document file:///tmp/harbor/svc_policy.json --region us-east-1",
"description": "Update svc role policy with KMS"
}(Bash completed with no output)
{
"command": "aws appsync update-graphql-api --api-id 39f7f68a3b784fa9b61fdb342a --name harbor-graphql-api --region us-east-1 \\\n --authentication-type AMAZON_COGNITO_USER_POOLS \\\n --user-pool-config \"userPoolId=us-east-1_0607c2453cba44bb989b8ff4c2cc5bb5,awsRegion=us-east-1,defaultAction=DENY\" \\\n --additional-authentication-providers '[{\"authenticationType\":\"API_KEY\"}]' \\\n --log-config '{\"fieldLogLevel\":\"ALL\",\"cloudWatchLogsRoleArn\":\"arn:aws:iam::000000000000:role/harbor-appsync-logs-role\",\"excludeVerboseContent\":false}' \\\n --xray-enabled --output json",
"description": "Update API: DENY default, xray on, ALL logs, drop AWS_IAM"
}{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "39f7f68a3b784fa9b61fdb342a",
"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_0607c2453cba44bb989b8ff4c2cc5bb5",
"awsRegion": "us-east-1",
"defaultAction": "DENY"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/39f7f68a3b784fa9b61fdb342a",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/39f7f68a3b784fa9b61fdb342a"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
}
],
"xrayEnabled": true
}
}
[stdout]
{
"graphqlApi": {
"name": "harbor-graphql-api",
"apiId": "39f7f68a3b784fa9b61fdb342a",
"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_0607c2453cba44bb989b8ff4c2cc5bb5",
"awsRegion": "us-east-1",
"defaultAction": "DENY"
},
"arn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a",
"uris": {
"GRAPHQL": "http://localhost.localstack.cloud:4566/graphql/39f7f68a3b784fa9b61fdb342a",
"REALTIME": "ws://localhost.localstack.cloud:4510/graphql/39f7f68a3b784fa9b61fdb342a"
},
"tags": {},
"additionalAuthenticationProviders": [
{
"authenticationType": "API_KEY"
}
],
"xrayEnabled": true
}
}{
"command": "aws appsync update-api-cache --api-id 39f7f68a3b784fa9b61fdb342a --ttl 600 --api-caching-behavior PER_RESOLVER_CACHING --type SMALL --region us-east-1 --output json",
"description": "Switch cache to PER_RESOLVER_CACHING"
}{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "PER_RESOLVER_CACHING",
"type": "SMALL"
}
}
[stdout]
{
"apiCache": {
"ttl": 600,
"apiCachingBehavior": "PER_RESOLVER_CACHING",
"type": "SMALL"
}
}{
"command": "aws appsync update-api-key --api-id 39f7f68a3b784fa9b61fdb342a --id 87e2a505 --expires 1779420206 --region us-east-1 --output json",
"description": "Rotate key expiry to +6 days"
}{
"apiKey": {
"id": "87e2a505",
"expires": 1779418800,
"deletes": 1784602800
}
}
[stdout]
{
"apiKey": {
"id": "87e2a505",
"expires": 1779418800,
"deletes": 1784602800
}
}{
"taskId": "9",
"status": "completed"
}Updated task #9 status
{
"taskId": "5",
"status": "completed"
}Updated task #5 status
{
"taskId": "6",
"status": "completed"
}Updated task #6 status
{
"taskId": "4",
"status": "completed"
}Updated task #4 status
{
"command": "aws appsync start-schema-creation --api-id 39f7f68a3b784fa9b61fdb342a --definition fileb:///tmp/harbor/schema.graphql --region us-east-1 --output json",
"description": "Start schema creation"
}{
"status": "PROCESSING"
}
[stdout]
{
"status": "PROCESSING"
}{
"command": "for i in 1 2 3 4 5; do\n status=$(aws appsync get-schema-creation-status --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json)\n echo \"$status\"\n done=$(echo \"$status\" | grep -c '\"SUCCESS\"\\|\"FAILED\"\\|\"NOT_APPLICABLE\"')\n if [ \"$done\" -gt 0 ]; then break; fi\ndone",
"description": "Poll schema status"
}{
"status": "SUCCESS",
"details": "Successfully created schema with 4 types."
}
[stdout]
{
"status": "SUCCESS",
"details": "Successfully created schema with 4 types."
}{
"command": "aws appsync get-introspection-schema --api-id 39f7f68a3b784fa9b61fdb342a --format SDL --region us-east-1 /tmp/harbor/schema_out.graphql && cat /tmp/harbor/schema_out.graphql",
"description": "Verify 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!, ownerId: 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!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"])
}
schema {
query: Query
mutation: Mutation
}{
"taskId": "2",
"status": "completed"
}Updated task #2 status
{
"file_path": "/tmp/harbor/auth_resp.vtl",
"content": "$util.toJson({})\n"
}File created successfully at: /tmp/harbor/auth_resp.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/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/harbor/data_req.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/data_resp.vtl",
"content": "#if( !$util.isNull($ctx.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n#if( !$util.isNull($ctx.result) && $ctx.result.tenantId != $ctx.identity.claims.get(\"tenant_id\") )\n $util.unauthorized()\n#end\n$util.toJson($ctx.result)\n"
}File created successfully at: /tmp/harbor/data_resp.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/add_req.vtl",
"content": "#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\": \"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/harbor/add_req.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/add_resp.vtl",
"content": "#if( !$util.isNull($ctx.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n$util.toJson($ctx.result)\n"
}File created successfully at: /tmp/harbor/add_resp.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/getthing_req.vtl",
"content": "{}\n"
}File created successfully at: /tmp/harbor/getthing_req.vtl (file state is current in your context , no need to Read it back)
{
"file_path": "/tmp/harbor/getthing_resp.vtl",
"content": "$util.toJson($ctx.result)\n"
}File created successfully at: /tmp/harbor/getthing_resp.vtl (file state is current in your context , no need to Read it back)
{
"command": "aws appsync update-function --api-id 39f7f68a3b784fa9b61fdb342a --function-id 70867cab \\\n --name auth_check --data-source-name things_table --function-version 2018-05-29 \\\n --request-mapping-template \"$(cat /tmp/harbor/auth_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/harbor/auth_resp.vtl)\" \\\n --region us-east-1 --output json",
"description": "Update auth_check function"
}{
"functionConfiguration": {
"functionId": "70867cab",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/70867cab",
"name": "auth_check",
"dataSourceName": "things_table",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"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({})",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
{
"functionConfiguration": {
"functionId": "70867cab",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/70867cab",
"name": "auth_check",
"dataSourceName": "things_table",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"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({})",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}{
"command": "aws appsync update-function --api-id 39f7f68a3b784fa9b61fdb342a --function-id 2c1e403b \\\n --name get_thing_data --data-source-name things_table --function-version 2018-05-29 \\\n --request-mapping-template \"$(cat /tmp/harbor/data_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/harbor/data_resp.vtl)\" \\\n --region us-east-1 --output json",
"description": "Update get_thing_data function"
}{
"functionConfiguration": {
"functionId": "2c1e403b",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/2c1e403b",
"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.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n#if( !$util.isNull($ctx.result) && $ctx.result.tenantId != $ctx.identity.claims.get(\"tenant_id\") )\n $util.unauthorized()\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
{
"functionConfiguration": {
"functionId": "2c1e403b",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/2c1e403b",
"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.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n#if( !$util.isNull($ctx.result) && $ctx.result.tenantId != $ctx.identity.claims.get(\"tenant_id\") )\n $util.unauthorized()\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}{
"taskId": "3",
"status": "completed"
}Updated task #3 status
{
"taskId": "1",
"status": "completed"
}Updated task #1 status
{
"command": "aws appsync update-resolver --api-id 39f7f68a3b784fa9b61fdb342a --type-name Query --field-name getThing \\\n --kind PIPELINE \\\n --pipeline-config 'functions=70867cab,2c1e403b' \\\n --request-mapping-template \"$(cat /tmp/harbor/getthing_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/harbor/getthing_resp.vtl)\" \\\n --caching-config 'ttl=600,cachingKeys=$context.identity.claims.tenant_id,cachingKeys=$context.arguments.id' \\\n --region us-east-1 --output json",
"description": "Update Query.getThing resolver"
}Exit code 252
aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--caching-config': Second instance of key "cachingKeys" encountered for input:
ttl=600,cachingKeys=$context.identity.claims.tenant_id,cachingKeys=$context.arguments.id
^
This is often because there is a preceding "," instead of a space.
[error] tool reported failure{
"command": "aws appsync update-resolver --api-id 39f7f68a3b784fa9b61fdb342a --type-name Mutation --field-name addThing \\\n --kind UNIT --data-source-name things_table \\\n --request-mapping-template \"$(cat /tmp/harbor/add_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/harbor/add_resp.vtl)\" \\\n --region us-east-1 --output json",
"description": "Update Mutation.addThing resolver"
}{
"resolver": {
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/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\": \"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": "#if( !$util.isNull($ctx.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n$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/39f7f68a3b784fa9b61fdb342a/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\": \"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": "#if( !$util.isNull($ctx.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
}{
"taskId": "8",
"status": "completed"
}Updated task #8 status
{
"command": "aws appsync update-resolver --api-id 39f7f68a3b784fa9b61fdb342a --type-name Query --field-name getThing \\\n --kind PIPELINE \\\n --pipeline-config '{\"functions\":[\"70867cab\",\"2c1e403b\"]}' \\\n --request-mapping-template \"$(cat /tmp/harbor/getthing_req.vtl)\" \\\n --response-mapping-template \"$(cat /tmp/harbor/getthing_resp.vtl)\" \\\n --caching-config '{\"ttl\":600,\"cachingKeys\":[\"$context.identity.claims.tenant_id\",\"$context.arguments.id\"]}' \\\n --region us-east-1 --output json",
"description": "Update Query.getThing resolver with JSON pipeline+cache"
}{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"70867cab",
"2c1e403b"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.identity.claims.tenant_id",
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
}
[stdout]
{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"70867cab",
"2c1e403b"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.identity.claims.tenant_id",
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
}{
"taskId": "7",
"status": "completed"
}Updated task #7 status
{
"command": "echo \"=== API ===\"; aws appsync get-graphql-api --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json | python3 -c \"import json,sys; d=json.load(sys.stdin)['graphqlApi']; print(f\\\"auth={d['authenticationType']} default={d['userPoolConfig']['defaultAction']} xray={d['xrayEnabled']} log={d['logConfig']['fieldLogLevel']} excludeVerbose={d['logConfig']['excludeVerboseContent']} addProviders={[p['authenticationType'] for p in d['additionalAuthenticationProviders']]}\\\")\"\necho \"\"\necho \"=== Cache ===\"; aws appsync get-api-cache --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json | python3 -c \"import json,sys; d=json.load(sys.stdin)['apiCache']; print(d)\"\necho \"\"\necho \"=== API key ===\"; aws appsync list-api-keys --api-id 39f7f68a3b784fa9b61fdb342a --region us-east-1 --output json | python3 -c \"\nimport json,sys,datetime\nd=json.load(sys.stdin)['apiKeys'][0]\nexp=datetime.datetime.utcfromtimestamp(d['expires'])\nnow=datetime.datetime.utcnow()\nprint(f\\\"id={d['id']} expires={exp} (in {(exp-now).days}d {(exp-now).seconds//3600}h)\\\")\"\necho \"\"\necho \"=== IAM svc role ===\"; aws iam get-role-policy --role-name harbor-appsync-svc-role --policy-name appsync-ddb-scoped --region us-east-1 --output json | python3 -c \"\nimport json,sys\nd=json.load(sys.stdin)['PolicyDocument']\nfor s in d['Statement']:\n print(f\\\"Sid={s['Sid']} actions={s['Action']}\\\")\"",
"description": "Verify API/cache/key/IAM"
}=== API ===
auth=AMAZON_COGNITO_USER_POOLS default=DENY xray=True log=ALL excludeVerbose=False addProviders=['API_KEY']
=== Cache ===
{'ttl': 600, 'apiCachingBehavior': 'PER_RESOLVER_CACHING', 'type': 'SMALL'}
=== API key ===
<string>:4: DeprecationWarning: datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.fromtimestamp(timestamp, datetime.UTC).
<string>:5: 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).
id=87e2a505 expires=2026-05-22 03:00:00 (in 5d 23h)
=== IAM svc role ===
Sid=DDBScopedActions actions=['dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:Query']
Sid=KMSForEncryptedDDB actions=['kms:Decrypt', 'kms:Encrypt', 'kms:GenerateDataKey', 'kms:ReEncrypt*', 'kms:DescribeKey']
[stdout]
=== API ===
auth=AMAZON_COGNITO_USER_POOLS default=DENY xray=True log=ALL excludeVerbose=False addProviders=['API_KEY']
=== Cache ===
{'ttl': 600, 'apiCachingBehavior': 'PER_RESOLVER_CACHING', 'type': 'SMALL'}
=== API key ===
<string>:4: DeprecationWarning: datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.fromtimestamp(timestamp, datetime.UTC).
<string>:5: 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).
id=87e2a505 expires=2026-05-22 03:00:00 (in 5d 23h)
=== IAM svc role ===
Sid=DDBScopedActions actions=['dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:Query']
Sid=KMSForEncryptedDDB actions=['kms:Decrypt', 'kms:Encrypt', 'kms:GenerateDataKey', 'kms:ReEncrypt*', 'kms:DescribeKey']{
"command": "echo \"=== Schema ===\"; cat /tmp/harbor/schema_out.graphql\necho \"\"\necho \"=== Query.getThing resolver ===\"\naws appsync get-resolver --api-id 39f7f68a3b784fa9b61fdb342a --type-name Query --field-name getThing --region us-east-1 --output json\necho \"\"\necho \"=== Mutation.addThing resolver ===\"\naws appsync get-resolver --api-id 39f7f68a3b784fa9b61fdb342a --type-name Mutation --field-name addThing --region us-east-1 --output json",
"description": "Verify schema and resolvers"
}=== 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!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"])
}
schema {
query: Query
mutation: Mutation
}
=== Query.getThing resolver ===
{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"70867cab",
"2c1e403b"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.identity.claims.tenant_id",
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
}
=== Mutation.addThing resolver ===
{
"resolver": {
"typeName": "Mutation",
"fieldName": "addThing",
"dataSourceName": "things_table",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/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\": \"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": "#if( !$util.isNull($ctx.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n$util.toJson($ctx.result)",
"kind": "UNIT",
"maxBatchSize": 0
}
}
[stdout]
=== 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!, ownerId: String!): Thing @aws_cognito_user_pools(cognito_groups: ["tenants"])
}
schema {
query: Query
mutation: Mutation
}
=== Query.getThing resolver ===
{
"resolver": {
"typeName": "Query",
"fieldName": "getThing",
"resolverArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/types/Query/resolvers/getThing",
"requestMappingTemplate": "{}",
"responseMappingTemplate": "$util.toJson($ctx.result)",
"kind": "PIPELINE",
"pipelineConfig": {
"functions": [
"70867cab",
"2c1e403b"
]
},
"cachingConfig": {
"ttl": 600,
"cachingKeys": [
"$context.identity.claims.tenant_id",
"$context.arguments.id"
]
},
"maxBatchSize": 0
}
}
=== Mutation.addThing resolver ===
{
"resolver": {
"typeName": "Mutation",
"fieldName": "addThing",
…[truncated]{
"command": "echo \"=== auth_check ===\"\naws appsync get-function --api-id 39f7f68a3b784fa9b61fdb342a --function-id 70867cab --region us-east-1 --output json\necho \"\"\necho \"=== get_thing_data ===\"\naws appsync get-function --api-id 39f7f68a3b784fa9b61fdb342a --function-id 2c1e403b --region us-east-1 --output json",
"description": "Verify functions"
}=== auth_check ===
{
"functionConfiguration": {
"functionId": "70867cab",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/70867cab",
"name": "auth_check",
"dataSourceName": "things_table",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"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({})",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
=== get_thing_data ===
{
"functionConfiguration": {
"functionId": "2c1e403b",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/2c1e403b",
"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.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n#if( !$util.isNull($ctx.result) && $ctx.result.tenantId != $ctx.identity.claims.get(\"tenant_id\") )\n $util.unauthorized()\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
[stdout]
=== auth_check ===
{
"functionConfiguration": {
"functionId": "70867cab",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/70867cab",
"name": "auth_check",
"dataSourceName": "things_table",
"requestMappingTemplate": "#set( $groups = $ctx.identity.claims.get(\"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({})",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}
=== get_thing_data ===
{
"functionConfiguration": {
"functionId": "2c1e403b",
"functionArn": "arn:aws:appsync:us-east-1:000000000000:apis/39f7f68a3b784fa9b61fdb342a/functions/2c1e403b",
"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.error) )\n $util.error($ctx.error.message, $ctx.error.type)\n#end\n#if( !$util.isNull($ctx.result) && $ctx.result.tenantId != $ctx.identity.claims.get(\"tenant_id\") )\n $util.unauthorized()\n#end\n$util.toJson($ctx.result)",
"functionVersion": "2018-05-29",
"maxBatchSize": 0
}
}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 50ms
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/twLAYlHcA9DA2qlqFwrfK/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 PASSED [ 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 PASSED [ 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 PASSED [ 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 FAILED [100%]
=================================== FAILURES ===================================
____ test_54_schema_mutation_addthing_does_not_accept_tenant_or_owner_args _____
def test_54_schema_mutation_addthing_does_not_accept_tenant_or_owner_args():
"""C13: Schema's Mutation.addThing signature must NOT accept tenantId/ownerId as arguments.
Fixing only the VTL to inject from $ctx.identity isn't enough , if the schema still
advertises `tenantId: String!` as a required arg, clients still pass it (their tooling
might rely on it) AND the field is GraphQL-valid; the broken contract just gets ignored
silently at the resolver, which is a worse failure mode than a hard schema rejection.
The audit requires the schema itself to refuse the input.
"""
sdl = _schema_sdl()
if not sdl:
return # SDL not retrievable on this tier , covered by VTL test_46
mut_block = re.search(r"type\s+Mutation\b[^{]*\{(.*?)\}", sdl, re.S)
if not mut_block:
return
body = mut_block.group(1)
if "addThing" not in body:
return
fm = re.search(r"\baddThing\s*\(([^)]*)\)", body, re.S)
assert fm, f"Mutation.addThing signature not found in SDL body: {body!r}"
args_block = fm.group(1)
# The arg names that are forbidden.
forbidden_arg = re.compile(r"\b(tenantId|ownerId)\s*:", re.I)
leaked = forbidden_arg.findall(args_block)
> assert not leaked, (
f"Mutation.addThing signature accepts client-supplied {leaked} arg(s): `addThing({args_block.strip()})`. "
f"Even if the VTL is fixed to inject tenantId/ownerId from $ctx.identity, the schema still "
f"advertises them as inputs , clients pass them and the field validates. The audit requires the "
f"schema to refuse these inputs outright (`addThing(id: ID!, name: String!): Thing`)."
)
E AssertionError: Mutation.addThing signature accepts client-supplied ['ownerId'] arg(s): `addThing(id: ID!, name: String!, ownerId: String!)`. Even if the VTL is fixed to inject tenantId/ownerId from $ctx.identity, the schema still advertises them as inputs , clients pass them and the field validates. The audit requires the schema to refuse these inputs outright (`addThing(id: ID!, name: String!): Thing`).
E assert not ['ownerId']
/tests/test_outputs.py:1053: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 148 warnings
/root/.cache/uv/archive-v0/twLAYlHcA9DA2qlqFwrfK/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/test_outputs.py::test_08_two_iam_roles_exist
PASSED ../tests/test_outputs.py::test_09_appsync_data_source_exists
PASSED ../tests/test_outputs.py::test_10_two_appsync_functions_exist
PASSED ../tests/test_outputs.py::test_11_two_resolvers_exist
PASSED ../tests/test_outputs.py::test_12_ssm_pointers_resolve
PASSED ../tests/test_outputs.py::test_13_ssm_pointer_values_have_correct_shape
PASSED ../tests/test_outputs.py::test_14_ssm_function_ids_resolve_to_real_functions
PASSED ../tests/test_outputs.py::test_15_api_authentication_type_cognito
PASSED ../tests/test_outputs.py::test_16_user_pool_config_matches_pointer
PASSED ../tests/test_outputs.py::test_17_user_pool_config_default_action_deny
PASSED ../tests/test_outputs.py::test_18_additional_auth_includes_api_key
PASSED ../tests/test_outputs.py::test_19_additional_auth_api_key_appears_exactly_once
PASSED ../tests/test_outputs.py::test_20_additional_auth_does_not_include_iam
PASSED ../tests/test_outputs.py::test_21_user_pool_has_custom_tenant_attribute
PASSED ../tests/test_outputs.py::test_22_app_client_has_no_admin_user_password_flow
PASSED ../tests/test_outputs.py::test_23_svc_role_trusts_appsync
PASSED ../tests/test_outputs.py::test_24_svc_role_uses_specific_table_arn_no_wildcard
PASSED ../tests/test_outputs.py::test_25_svc_role_does_not_allow_dynamodb_scan_or_wildcard
PASSED ../tests/test_outputs.py::test_26_svc_role_grants_kms_decrypt_on_cmk
PASSED ../tests/test_outputs.py::test_27_logs_role_trusts_appsync_and_can_write_logs
PASSED ../tests/test_outputs.py::test_28_svc_role_has_no_admin_managed_policies
PASSED ../tests/test_outputs.py::test_29_get_thing_is_pipeline_resolver
PASSED ../tests/test_outputs.py::test_30_get_thing_pipeline_has_two_functions
PASSED ../tests/test_outputs.py::test_31_get_thing_pipeline_function_order_is_auth_then_data
PASSED ../tests/test_outputs.py::test_32_auth_check_vtl_references_cognito_groups_claim
PASSED ../tests/test_outputs.py::test_33_auth_check_vtl_calls_util_unauthorized_or_error
PASSED ../tests/test_outputs.py::test_34_auth_check_vtl_references_tenants_group_literal
PASSED ../tests/test_outputs.py::test_35_data_fn_vtl_is_getitem_on_arguments_id
PASSED ../tests/test_outputs.py::test_36_data_fn_vtl_does_not_use_scan_or_query_on_full_table
PASSED ../tests/test_outputs.py::test_37_data_fn_request_is_well_formed_getitem
PASSED ../tests/test_outputs.py::test_38_api_cache_per_resolver_caching
PASSED ../tests/test_outputs.py::test_39_api_cache_type_set_and_ttl_non_zero
PASSED ../tests/test_outputs.py::test_40_get_thing_caching_keys_include_id_and_tenant
PASSED ../tests/test_outputs.py::test_41_get_thing_caching_ttl_non_trivial
PASSED ../tests/test_outputs.py::test_42_mutation_addthing_has_no_caching_config
PASSED ../tests/test_outputs.py::test_43_api_key_expiry_within_seven_d
… (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_5dc5bdcfdac64a31. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_5dc5bdcfdac64a31 · verifier authoritative; classifier explanatory.