Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active March 15, 2024 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhw/3aa9d4f85d9709fc7cb44e886ba0808f to your computer and use it in GitHub Desktop.
Save jhw/3aa9d4f85d9709fc7cb44e886ba0808f to your computer and use it in GitHub Desktop.
Test to see if apigateway POST Model validation is working (or not)
env
*.pyc
__pycache__
tmp
setenv-priv.sh
AppName=apigw-post-model-demo
#!/usr/bin/env bash
. app.props
aws cloudformation delete-stack --stack-name $AppName
#!/usr/bin/env bash
. app.props
echo "DomainName: $DOMAIN_NAME"
echo "CertificateArn: $CERTIFICATE_ARN"
aws cloudformation deploy --stack-name $AppName --template-file stack.json --capabilities CAPABILITY_NAMED_IAM --parameter-overrides DomainName=$DOMAIN_NAME CertificateArn=$CERTIFICATE_ARN

I have a Cloudformation template for an apigw HTTP POST endpoint, which validates the POST request body using a Model -

{
  "Outputs": {
    "AppRestApi": {
      "Value": {
        "Ref": "AppRestApi"
      }
    }
  },
  "Parameters": {
    "CertificateArn": {
      "Type": "String"
    },
    "DomainName": {
      "Type": "String"
    }
  },
  "Resources": {
    "AppBasePathMapping": {
      "DependsOn": [
        "AppDomainName"
      ],
      "Properties": {
        "DomainName": {
          "Ref": "DomainName"
        },
        "RestApiId": {
          "Ref": "AppRestApi"
        },
        "Stage": "prod"
      },
      "Type": "AWS::ApiGateway::BasePathMapping"
    },
    "AppDeployment": {
      "DependsOn": [
        "AppHelloPostPublicLambdaProxyMethod"
      ],
      "Properties": {
        "RestApiId": {
          "Ref": "AppRestApi"
        }
      },
      "Type": "AWS::ApiGateway::Deployment"
    },
    "AppDomainName": {
      "Properties": {
        "CertificateArn": {
          "Ref": "CertificateArn"
        },
        "DomainName": {
          "Ref": "DomainName"
        }
      },
      "Type": "AWS::ApiGateway::DomainName"
    },
    "AppHelloPostFunction": {
      "Properties": {
        "Code": {
          "ZipFile": "import json\ndef handler(event, context):\n    body=json.loads(event[\"body\"])\n    message=body[\"message\"]\n    return {\"statusCode\": 200,\n            \"headers\": {\"Content-Type\": \"text/plain\",\n                        \"Access-Control-Allow-Origin\": \"*\",\n                        \"Access-Control-Allow-Headers\": \"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent\",\n                        \"Access-Control-Allow-Methods\": \"OPTIONS,POST\"},\n            \"body\": f\"you sent '{message}' via POST\"}"
        },
        "Handler": "index.handler",
        "MemorySize": 512,
        "Role": {
          "Fn::GetAtt": [
            "AppHelloPostRole",
            "Arn"
          ]
        },
        "Runtime": "python3.10",
        "Timeout": 5
      },
      "Type": "AWS::Lambda::Function"
    },
    "AppHelloPostModel": {
      "Properties": {
        "ContentType": "application/json",
        "Name": "AppHelloPostModel",
        "RestApiId": {
          "Ref": "AppRestApi"
        },
        "Schema": {
          "$schema": "http://json-schema.org/draft-04/schema#",
          "additionalProperties": false,
          "properties": {
            "message": {
              "type": "string"
            }
          },
          "required": [
            "message"
          ],
          "type": "object"
        }
      },
      "Type": "AWS::ApiGateway::Model"
    },
    "AppHelloPostPermission": {
      "Properties": {
        "Action": "lambda:InvokeFunction",
        "FunctionName": {
          "Ref": "AppHelloPostFunction"
        },
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppRestApi}/${AppStage}/POST/hello-post"
        }
      },
      "Type": "AWS::Lambda::Permission"
    },
    "AppHelloPostPublicLambdaProxyMethod": {
      "Properties": {
        "AuthorizationType": "NONE",
        "HttpMethod": "POST",
        "Integration": {
          "IntegrationHttpMethod": "POST",
          "Type": "AWS_PROXY",
          "Uri": {
            "Fn::Sub": [
              "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
              {
                "arn": {
                  "Fn::GetAtt": [
                    "AppHelloPostFunction",
                    "Arn"
                  ]
                }
              }
            ]
          }
        },
        "RequestModels": {
          "application/json": "AppHelloPostModel"
        },
        "RequestValidatorId": {
          "Ref": "AppHelloPostSchemaRequestValidator"
        },
        "ResourceId": {
          "Ref": "AppHelloPostResource"
        },
        "RestApiId": {
          "Ref": "AppRestApi"
        }
      },
      "Type": "AWS::ApiGateway::Method"
    },
    "AppHelloPostResource": {
      "Properties": {
        "ParentId": {
          "Fn::GetAtt": [
            "AppRestApi",
            "RootResourceId"
          ]
        },
        "PathPart": "hello-post",
        "RestApiId": {
          "Ref": "AppRestApi"
        }
      },
      "Type": "AWS::ApiGateway::Resource"
    },
    "AppHelloPostRole": {
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": [
                "sts:AssumeRole"
              ],
              "Effect": "Allow",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "Policies": [
          {
            "PolicyDocument": {
              "Statement": [
                {
                  "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Effect": "Allow",
                  "Resource": "*"
                }
              ],
              "Version": "2012-10-17"
            },
            "PolicyName": {
              "Fn::Sub": "app-hello-post-role-policy-${AWS::StackName}"
            }
          }
        ]
      },
      "Type": "AWS::IAM::Role"
    },
    "AppHelloPostSchemaRequestValidator": {
      "Properties": {
        "RestApiId": {
          "Ref": "AppRestApi"
        },
        "ValidateRequestBody": true,
        "ValidateRequestParameters": false
      },
      "Type": "AWS::ApiGateway::RequestValidator"
    },
    "AppRecordSet": {
      "Properties": {
        "AliasTarget": {
          "DNSName": {
            "Fn::GetAtt": [
              "AppDomainName",
              "DistributionDomainName"
            ]
          },
          "EvaluateTargetHealth": false,
          "HostedZoneId": {
            "Fn::GetAtt": [
              "AppDomainName",
              "DistributionHostedZoneId"
            ]
          }
        },
        "HostedZoneName": {
          "Fn::Sub": [
            "${prefix}.${suffix}.",
            {
              "prefix": {
                "Fn::Select": [
                  1,
                  {
                    "Fn::Split": [
                      ".",
                      {
                        "Ref": "DomainName"
                      }
                    ]
                  }
                ]
              },
              "suffix": {
                "Fn::Select": [
                  2,
                  {
                    "Fn::Split": [
                      ".",
                      {
                        "Ref": "DomainName"
                      }
                    ]
                  }
                ]
              }
            }
          ]
        },
        "Name": {
          "Ref": "DomainName"
        },
        "Type": "A"
      },
      "Type": "AWS::Route53::RecordSet"
    },
    "AppRestApi": {
      "Properties": {
        "Name": {
          "Fn::Sub": "app-rest-api-${AWS::StackName}"
        }
      },
      "Type": "AWS::ApiGateway::RestApi"
    },
    "AppStage": {
      "Properties": {
        "DeploymentId": {
          "Ref": "AppDeployment"
        },
        "RestApiId": {
          "Ref": "AppRestApi"
        },
        "StageName": "prod"
      },
      "Type": "AWS::ApiGateway::Stage"
    }
  }
}

This works fine in the happy path case -

(env) jhw@Justins-Air  % curl -i -X POST https://apigwmodelpostdemo.spaas.link/hello-post -d "{\"message\": \"Hello World\!\"}"
HTTP/2 200 
content-type: text/plain
content-length: 32
date: Fri, 15 Mar 2024 06:14:57 GMT
x-amzn-requestid: 7f7ad199-6bb7-4ea9-bcab-9046d5c8651e
access-control-allow-origin: *
access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
x-amz-apigw-id: UqEXOEnZjoEEYmA=
access-control-allow-methods: OPTIONS,POST
x-amzn-trace-id: Root=1-65f3e761-2bf936e212c5205716168100;Parent=3218d4aa6fbde101;Sampled=0;lineage=3282c475:0
x-cache: Miss from cloudfront
via: 1.1 22e421a47e59010b5e8eb6ae4d4bd7e4.cloudfront.net (CloudFront)
x-amz-cf-pop: LHR61-P2
x-amz-cf-id: -fpNsR5L4xGvFf-GwO-aDw4D8TkfA1cc0B6fnoLLFDZC0gP_2vcFLQ==

you sent 'Hello World!' via POST%

but when I send it with what should be an invalid request body it returns HTTP 5XX not HTTP 4XX -

(env) jhw@Justins-Air  % curl -i -X POST https://apigwmodelpostdemo.spaas.link/hello-post -d "{\"_message\": \"Hello World\!\"}"
HTTP/2 502 
content-type: application/json
content-length: 36
date: Fri, 15 Mar 2024 06:15:02 GMT
x-amzn-requestid: 9113503e-79c7-4948-bb3d-9cb3698d2ff9
x-amzn-errortype: InternalServerErrorException
x-amz-apigw-id: UqEYCFd2joEED1Q=
x-cache: Error from cloudfront
via: 1.1 8ba281782b2b20f7db8f5372bc06a3a2.cloudfront.net (CloudFront)
x-amz-cf-pop: LHR61-P2
x-amz-cf-id: UpUa11Il2x7lEdu7l7SboDyuB9TekNAsnY_fo8INHX7LDlgal81nDw==

{"message": "Internal server error"}%

This error is coming from the Lambda function, as I can see the error in the Lambda logs. Which suggests that the apigw Model is not being called, or if it is being called, it is not configured to return a 4XX response.

Why is that and what needs to be changed, so that if the endpoint is called with an invalid request body, apigw will return 4XX because of Model validation failure, and the request will never hit the Lambda function?

I have tested that the schema is formatted properly -

import json, jsonschema

if __name__ == "__main__":
    template=json.loads(open("template.json").read())
    schema=template["Resources"]["AppHelloPostModel"]["Properties"]["Schema"]
    for instance in [{"message": "hello"},
                     {"_message": "hello"}]:
        print(f"--- {instance} ---")
        try:        
            jsonschema.validate(instance=instance,
                                schema=schema)
            print("OK")
        except jsonschema.exceptions.ValidationError as error:
            print(str(error))

env) jhw@Justins-Air  % python test_model_validation.py 
--- {'message': 'hello'} ---
OK
--- {'_message': 'hello'} ---
Additional properties are not allowed ('_message' was unexpected)

Failed validating 'additionalProperties' in schema:
    {'$schema': 'http://json-schema.org/draft-04/schema#',
     'additionalProperties': False,
     'properties': {'message': {'type': 'string'}},
     'required': ['message'],
     'type': 'object'}

On instance:
    {'_message': 'hello'}

Please let me know what I need to change, to make sure the Model is validating the incoming request body properly.

#!/usr/bin/env bash
. app.props
aws cloudformation describe-stack-events --stack-name $AppName --query "StackEvents[].{\"1.Timestamp\":Timestamp,\"2.Id\":LogicalResourceId,\"3.Type\":ResourceType,\"4.Status\":ResourceStatus,\"5.Reason\":ResourceStatusReason}"
#!/usr/bin/env bash
. app.props
aws cloudformation describe-stacks --stack-name $AppName --query 'Stacks[0].Outputs' --output table
#!/usr/bin/env bash
. app.props
aws cloudformation describe-stack-resources --stack-name $AppName --query "StackResources[].{\"1.Timestamp\":Timestamp,\"2.LogicalId\":LogicalResourceId,\"3.PhysicalId\":PhysicalResourceId,\"4.Type\":ResourceType,\"5.Status\":ResourceStatus}"
#!/usr/bin/env bash
aws cloudformation describe-stacks --query "Stacks[].{\"1.Name\":StackName,\"2.Status\":StackStatus}"
awscli
boto3
botocore
jsonschema
pyyaml
#!/usr/bin/env bash
export AWS_DEFAULT_OUTPUT=table
export AWS_PROFILE=#{your-aws-profile-here}
export CERTIFICATE_ARN=#{your-certificate-arn-here}
export DOMAIN_NAME=#{your-fully-qualified-domain-name-here}
{
"Outputs": {
"AppRestApi": {
"Value": {
"Ref": "AppRestApi"
}
}
},
"Parameters": {
"CertificateArn": {
"Type": "String"
},
"DomainName": {
"Type": "String"
}
},
"Resources": {
"AppBasePathMapping": {
"DependsOn": [
"AppDomainName"
],
"Properties": {
"DomainName": {
"Ref": "DomainName"
},
"RestApiId": {
"Ref": "AppRestApi"
},
"Stage": "prod"
},
"Type": "AWS::ApiGateway::BasePathMapping"
},
"AppDeployment": {
"DependsOn": [
"AppHelloPostPublicLambdaProxyMethod"
],
"Properties": {
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Deployment"
},
"AppDomainName": {
"Properties": {
"CertificateArn": {
"Ref": "CertificateArn"
},
"DomainName": {
"Ref": "DomainName"
}
},
"Type": "AWS::ApiGateway::DomainName"
},
"AppHelloPostFunction": {
"Properties": {
"Code": {
"ZipFile": "import json\ndef handler(event, context):\n body=json.loads(event[\"body\"])\n message=body[\"message\"]\n return {\"statusCode\": 200,\n \"headers\": {\"Content-Type\": \"text/plain\",\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Headers\": \"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent\",\n \"Access-Control-Allow-Methods\": \"OPTIONS,POST\"},\n \"body\": f\"you sent '{message}' via POST\"}"
},
"Handler": "index.handler",
"MemorySize": 512,
"Role": {
"Fn::GetAtt": [
"AppHelloPostRole",
"Arn"
]
},
"Runtime": "python3.10",
"Timeout": 5
},
"Type": "AWS::Lambda::Function"
},
"AppHelloPostModel": {
"Properties": {
"ContentType": "application/json",
"Name": "AppHelloPostModel",
"RestApiId": {
"Ref": "AppRestApi"
},
"Schema": {
"$schema": "http://json-schema.org/draft-04/schema#",
"additionalProperties": false,
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
},
"Type": "AWS::ApiGateway::Model"
},
"AppHelloPostPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "AppHelloPostFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppRestApi}/${AppStage}/POST/hello-post"
}
},
"Type": "AWS::Lambda::Permission"
},
"AppHelloPostPublicLambdaProxyMethod": {
"Properties": {
"AuthorizationType": "NONE",
"HttpMethod": "POST",
"Integration": {
"IntegrationHttpMethod": "POST",
"Type": "AWS_PROXY",
"Uri": {
"Fn::Sub": [
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
{
"arn": {
"Fn::GetAtt": [
"AppHelloPostFunction",
"Arn"
]
}
}
]
}
},
"RequestModels": {
"application/json": "AppHelloPostModel"
},
"RequestValidatorId": {
"Ref": "AppHelloPostSchemaRequestValidator"
},
"ResourceId": {
"Ref": "AppHelloPostResource"
},
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Method"
},
"AppHelloPostResource": {
"Properties": {
"ParentId": {
"Fn::GetAtt": [
"AppRestApi",
"RootResourceId"
]
},
"PathPart": "hello-post",
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Resource"
},
"AppHelloPostRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-hello-post-role-policy-${AWS::StackName}"
}
}
]
},
"Type": "AWS::IAM::Role"
},
"AppHelloPostSchemaRequestValidator": {
"Properties": {
"RestApiId": {
"Ref": "AppRestApi"
},
"ValidateRequestBody": true,
"ValidateRequestParameters": false
},
"Type": "AWS::ApiGateway::RequestValidator"
},
"AppRecordSet": {
"Properties": {
"AliasTarget": {
"DNSName": {
"Fn::GetAtt": [
"AppDomainName",
"DistributionDomainName"
]
},
"EvaluateTargetHealth": false,
"HostedZoneId": {
"Fn::GetAtt": [
"AppDomainName",
"DistributionHostedZoneId"
]
}
},
"HostedZoneName": {
"Fn::Sub": [
"${prefix}.${suffix}.",
{
"prefix": {
"Fn::Select": [
1,
{
"Fn::Split": [
".",
{
"Ref": "DomainName"
}
]
}
]
},
"suffix": {
"Fn::Select": [
2,
{
"Fn::Split": [
".",
{
"Ref": "DomainName"
}
]
}
]
}
}
]
},
"Name": {
"Ref": "DomainName"
},
"Type": "A"
},
"Type": "AWS::Route53::RecordSet"
},
"AppRestApi": {
"Properties": {
"Name": {
"Fn::Sub": "app-rest-api-${AWS::StackName}"
}
},
"Type": "AWS::ApiGateway::RestApi"
},
"AppStage": {
"Properties": {
"DeploymentId": {
"Ref": "AppDeployment"
},
"RestApiId": {
"Ref": "AppRestApi"
},
"StageName": "prod"
},
"Type": "AWS::ApiGateway::Stage"
}
}
}
import json, jsonschema
if __name__ == "__main__":
template=json.loads(open("stack.json").read())
schema=template["Resources"]["AppHelloPostModel"]["Properties"]["Schema"]
for instance in [{"message": "hello"},
{"_message": "hello"}]:
print(f"--- {instance} ---")
try:
jsonschema.validate(instance=instance,
schema=schema)
print("OK")
except jsonschema.exceptions.ValidationError as error:
print(str(error))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment