Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active March 28, 2024 20:40
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/a0bba03627b6210acec2dc3d018bc2e0 to your computer and use it in GitHub Desktop.
Save jhw/a0bba03627b6210acec2dc3d018bc2e0 to your computer and use it in GitHub Desktop.
AWS ApiGatewayV2 web api demo
env
*.pyc
__pycache__
tmp
setenv-priv.sh
AppName=apigwv2-webapi-demo
AllowedOrigins=http://localhost:3000
<!-- this file needs to be served from a server in order to test CORS; server needs to be specified as an allowed origin; python3 -m http.server 3000 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CORS 200</title>
<script>
window.onload = function() {
fetch('https://apigwv2demo.spaaseu.link/public-get?message=Hello%20World')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
document.getElementById('response').textContent = data;
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
document.getElementById('response').textContent = 'Error: Could not retrieve data. Check console for details.';
});
};
</script>
</head>
<body>
<div id="response">Loading...</div>
</body>
</html>
<!-- this file needs to be served from a server in order to test CORS; server needs to be specified as an allowed origin; python3 -m http.server 3000 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CORS 400</title>
<script>
window.onload = function() {
fetch('https://apigwv2demo.spaaseu.link/public-get?whatevs=Hello%20World')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
document.getElementById('response').textContent = data;
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
document.getElementById('response').textContent = 'Error: Could not retrieve data. Check console for details.';
});
};
</script>
</head>
<body>
<div id="response">Loading...</div>
</body>
</html>
from botocore.exceptions import ClientError
import boto3, os, re, sys, yaml
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
if len(sys.argv) < 3:
raise RuntimeError("please enter email, password")
email, password = sys.argv[1:3]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
userpoolkey=hungarorise("app-user-pool")
if userpoolkey not in outputs:
raise RuntimeError("userpool not found")
userpool=outputs[userpoolkey]
clientkey=hungarorise("app-user-pool-client")
if clientkey not in outputs:
raise RuntimeError("client not found")
client=outputs[clientkey]
cognito=boto3.client("cognito-idp")
resp0=cognito.sign_up(ClientId=client,
Username=email,
Password=password)
print (yaml.safe_dump(resp0,
default_flow_style=False))
resp1=cognito.admin_confirm_sign_up(UserPoolId=userpool,
Username=email)
print (yaml.safe_dump(resp1,
default_flow_style=False))
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
#!/usr/bin/env bash
. app.props
aws cloudformation delete-stack --stack-name $AppName
from botocore.exceptions import ClientError
import boto3, os, re, sys, yaml
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
if len(sys.argv) < 2:
raise RuntimeError("please enter email")
email=sys.argv[1]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
userpoolkey=hungarorise("app-user-pool")
if userpoolkey not in outputs:
raise RuntimeError("userpool not found")
userpool=outputs[userpoolkey]
cognito=boto3.client("cognito-idp")
resp=cognito.admin_delete_user(UserPoolId=userpool,
Username=email)
print (yaml.safe_dump(resp,
default_flow_style=False))
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
#!/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 AllowedOrigins=$AllowedOrigins
from botocore.exceptions import ClientError
import boto3, os, re, sys, time
def fetch_log_events(logs, kwargs):
events, token = [], None
while True:
if token:
kwargs["nextToken"]=token
resp=logs.filter_log_events(**kwargs)
events+=resp["events"]
if "nextToken" in resp:
token=resp["nextToken"]
else:
break
return sorted(events,
key=lambda x: x["timestamp"])
if __name__=="__main__":
try:
if len(sys.argv) < 3:
raise RuntimeError("please enter lambda name, window")
lambdaname, window = sys.argv[1:3]
if not re.search("^\\d+$", window):
raise RuntimeError("window is invalid")
window=int(window)
logs=boto3.client("logs")
starttime=int(1000*(time.time()-window))
loggroupname="/aws/lambda/%s" % lambdaname
kwargs={"logGroupName": loggroupname,
"startTime": starttime,
"interleaved": True}
events=fetch_log_events(logs, kwargs)
for event in events:
msg=re.sub("\\r|\\n", "", event["message"])
print (msg)
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
import json, os
if __name__=="__main__":
if not os.path.exists("tmp"):
os.mkdir("tmp")
struct=json.loads(open("stack.json").read())
with open("tmp/stack.json", 'w') as f:
f.write(json.dumps(struct,
indent=2))
#!/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}"
from botocore.exceptions import ClientError
import boto3, os, re, sys, yaml
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
userpoolkey=hungarorise("app-user-pool")
if userpoolkey not in outputs:
raise RuntimeError("userpool not found")
userpool=outputs[userpoolkey]
cognito=boto3.client("cognito-idp")
resp=cognito.list_users(UserPoolId=userpool)
print (yaml.safe_dump(resp,
default_flow_style=False))
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
from botocore.exceptions import ClientError
import boto3, json, os, re, requests, sys, urllib.parse
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
if len(sys.argv) < 4:
raise RuntimeError("please enter email, password, message")
email, password, message = sys.argv[1:4]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
userpoolkey=hungarorise("app-user-pool")
if userpoolkey not in outputs:
raise RuntimeError("userpool not found")
userpool=outputs[userpoolkey]
clientkey=hungarorise("app-user-pool-client")
if clientkey not in outputs:
raise RuntimeError("client not found")
client=outputs[clientkey]
cg=boto3.client("cognito-idp")
authresp=cg.admin_initiate_auth(UserPoolId=userpool,
ClientId=client,
AuthFlow='ADMIN_NO_SRP_AUTH',
AuthParameters={"USERNAME": email,
"PASSWORD": password})
token=authresp["AuthenticationResult"]["IdToken"]
print (token)
url="https://apigwv2demo.spaaseu.link/private-get?message=%s" % urllib.parse.quote(message)
headers={"Origin": "http://localhost:3000",
"Authorization": "Bearer %s" % token,
"Accept": "application/json"}
resp=requests.get(url,
headers=headers)
print ("%s > %s" % (resp.status_code,
resp.text))
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
from botocore.exceptions import ClientError
import boto3, json, os, re, requests, sys, urllib.parse
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
if len(sys.argv) < 4:
raise RuntimeError("please enter email, password, message")
email, password, message = sys.argv[1:4]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
userpoolkey=hungarorise("app-user-pool")
if userpoolkey not in outputs:
raise RuntimeError("userpool not found")
userpool=outputs[userpoolkey]
clientkey=hungarorise("app-user-pool-client")
if clientkey not in outputs:
raise RuntimeError("client not found")
client=outputs[clientkey]
cg=boto3.client("cognito-idp")
authresp=cg.admin_initiate_auth(UserPoolId=userpool,
ClientId=client,
AuthFlow='ADMIN_NO_SRP_AUTH',
AuthParameters={"USERNAME": email,
"PASSWORD": password})
token=authresp["AuthenticationResult"]["IdToken"]
print (token)
url="https://apigwv2demo.spaaseu.link/private-post"
headers={"Origin": "http://localhost:3000",
"Authorization": "Bearer %s" % token,
"Content-Type": "application/json"}
struct={"message": message}
resp=requests.post(url,
headers=headers,
data=json.dumps(struct))
print ("%s > %s" % (resp.status_code,
resp.text))
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
import requests, sys, urllib.parse
if __name__=="__main__":
try:
if len(sys.argv) < 2:
raise RuntimeError("please enter message")
message=sys.argv[1]
url="https://apigwv2demo.spaaseu.link/public-get?message=%s" % urllib.parse.quote(message)
headers={"Origin": "http://localhost:3000"}
resp=requests.get(url,
headers=headers)
print ("%s > %s" % (resp.status_code,
resp.text))
except RuntimeError as error:
print ("Error: %s" % str(error))
import json, requests, sys
if __name__=="__main__":
try:
if len(sys.argv) < 2:
raise RuntimeError("please enter message")
message=sys.argv[1]
url="https://apigwv2demo.spaaseu.link/public-post"
headers={"Origin": "http://localhost:3000",
"Content-Type": "application/json"}
struct={"message": message}
resp=requests.post(url,
headers=headers,
data=json.dumps(struct))
print ("%s > %s" % (resp.status_code,
resp.text))
except RuntimeError as error:
print ("Error: %s" % str(error))
awscli
boto3
botocore
pyyaml
requests
#!/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": {
"AppApi": {
"Value": {
"Ref": "AppApi"
}
},
"AppIdentityPool": {
"Value": {
"Ref": "AppIdentityPool"
}
},
"AppStage": {
"Value": {
"Ref": "AppStage"
}
},
"AppUserPool": {
"Value": {
"Ref": "AppUserPool"
}
},
"AppUserPoolClient": {
"Value": {
"Ref": "AppUserPoolClient"
}
}
},
"Parameters": {
"AllowedOrigins": {
"Type": "String"
},
"CertificateArn": {
"Type": "String"
},
"DomainName": {
"Type": "String"
}
},
"Resources": {
"AppApi": {
"Properties": {
"CorsConfiguration": {
"AllowCredentials": true,
"AllowHeaders": [
"Content-Type",
"X-Amz-Date",
"Authorization",
"X-Api-Key"
],
"AllowMethods": [
"GET",
"POST"
],
"AllowOrigins": {
"Fn::Split": [
"|",
{
"Ref": "AllowedOrigins"
}
]
},
"ExposeHeaders": [
"X-Custom-Header"
],
"MaxAge": 3600
},
"Name": {
"Fn::Sub": "app-api-${AWS::StackName}"
},
"ProtocolType": "HTTP"
},
"Type": "AWS::ApiGatewayV2::Api"
},
"AppApiMapping": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"DomainName": {
"Ref": "DomainName"
},
"Stage": {
"Ref": "AppStage"
}
},
"Type": "AWS::ApiGatewayV2::ApiMapping"
},
"AppAuthorizer": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"AuthorizerType": "JWT",
"IdentitySource": [
"$request.header.Authorization"
],
"JwtConfiguration": {
"Audience": [
{
"Ref": "AppUserPoolClient"
}
],
"Issuer": {
"Fn::Sub": "https://cognito-idp.${AWS::Region}.amazonaws.com/${AppUserPool}"
}
},
"Name": {
"Fn::Sub": "app-authorizer-${AWS::StackName}"
}
},
"Type": "AWS::ApiGatewayV2::Authorizer"
},
"AppDomainName": {
"Properties": {
"DomainName": {
"Ref": "DomainName"
},
"DomainNameConfigurations": [
{
"CertificateArn": {
"Ref": "CertificateArn"
}
}
]
},
"Type": "AWS::ApiGatewayV2::DomainName"
},
"AppIdentityPool": {
"Properties": {
"AllowUnauthenticatedIdentities": true,
"CognitoIdentityProviders": [
{
"ClientId": {
"Ref": "AppUserPoolClient"
},
"ProviderName": {
"Fn::GetAtt": [
"AppUserPool",
"ProviderName"
]
}
}
]
},
"Type": "AWS::Cognito::IdentityPool"
},
"AppIdentityPoolAuthorizedPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"cognito-sync:*"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"cognito-identity:*"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"lambda:InvokeFunction"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-identity-pool-authorized-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppIdentityPoolAuthorizedRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppIdentityPoolAuthorizedRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRoleWithWebIdentity"
],
"Condition": {
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authorized"
},
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "AppIdentityPool"
}
}
},
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppIdentityPoolRoleAttachment": {
"Properties": {
"IdentityPoolId": {
"Ref": "AppIdentityPool"
},
"Roles": {
"authenticated": {
"Fn::GetAtt": [
"AppIdentityPoolAuthorizedRole",
"Arn"
]
},
"unauthenticated": {
"Fn::GetAtt": [
"AppIdentityPoolUnauthorizedRole",
"Arn"
]
}
}
},
"Type": "AWS::Cognito::IdentityPoolRoleAttachment"
},
"AppIdentityPoolUnauthorizedPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"cognito-sync:*"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-identity-pool-unauthorized-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppIdentityPoolUnauthorizedRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppIdentityPoolUnauthorizedRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRoleWithWebIdentity"
],
"Condition": {
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "unauthorized"
},
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "AppIdentityPool"
}
}
},
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppPrivateGetFunction": {
"Properties": {
"Code": {
"ZipFile": "Headers = {\n \"Content-Type\": \"text/plain\",\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,GET\"\n}\n\ndef handler(event, context = None, respheaders = Headers):\n try:\n if \"headers\" not in event:\n raise RuntimeError(\"request headers not found\")\n reqheaders = event[\"headers\"]\n if \"origin\" not in reqheaders:\n raise RuntimeError(\"origin not present in request headers\")\n respheaders[\"Access-Control-Allow-Origin\"] = reqheaders[\"origin\"]\n if \"queryStringParameters\" not in event:\n raise RuntimeError(\"querystring not found\")\n qs = event[\"queryStringParameters\"]\n if \"message\" not in qs:\n raise RuntimeError(\"message parameter not found\")\n message = qs[\"message\"]\n if message in [\"\", None]:\n raise RuntimeError(\"message parameter can't be blank\")\n respbody = f\"you sent '{message}' via GET\" \n return {\"statusCode\": 200,\n \"headers\": respheaders,\n \"body\": respbody}\n except RuntimeError as error:\n return {\"statusCode\": 400,\n \"headers\": respheaders,\n \"body\": str(error)}\n\nif __name__ == \"__main__\":\n for headers in [{},\n {\"origin\": \"http://localhost:3000\"}]:\n for event in [{},\n {\"queryStringParameters\": {\"message\": \"Hello World\"}},\n {\"queryStringParameters\": {\"message\": \"\"}},\n {\"queryStringParameters\": {\"messag\": \"Hello World\"}}]:\n event[\"headers\"] = headers # NB\n resp = handler(event)\n print (\"%s\\t%s\" % (resp[\"statusCode\"], resp[\"body\"]))\n\n"
},
"Handler": "index.handler",
"MemorySize": 512,
"Role": {
"Fn::GetAtt": [
"AppPrivateGetRole",
"Arn"
]
},
"Runtime": "python3.10",
"Timeout": 5
},
"Type": "AWS::Lambda::Function"
},
"AppPrivateGetIntegration": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"IntegrationMethod": "POST",
"IntegrationType": "AWS_PROXY",
"IntegrationUri": {
"Fn::Sub": [
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
{
"arn": {
"Fn::GetAtt": [
"AppPrivateGetFunction",
"Arn"
]
}
}
]
},
"PayloadFormatVersion": "2.0"
},
"Type": "AWS::ApiGatewayV2::Integration"
},
"AppPrivateGetPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "AppPrivateGetFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppApi}/${AppStage}/GET/private-get"
}
},
"Type": "AWS::Lambda::Permission"
},
"AppPrivateGetPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-private-get-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppPrivateGetRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppPrivateGetRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppPrivateGetRoute": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"AuthorizationType": "JWT",
"AuthorizerId": {
"Ref": "AppAuthorizer"
},
"RouteKey": "GET /private-get",
"Target": {
"Fn::Sub": "integrations/${AppPrivateGetIntegration}"
}
},
"Type": "AWS::ApiGatewayV2::Route"
},
"AppPrivatePostFunction": {
"Properties": {
"Code": {
"ZipFile": "import base64, json\n\nHeaders = {\n \"Content-Type\": \"text/plain\",\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}\n\ndef handler(event, context = None, respheaders = Headers):\n try:\n if \"headers\" not in event:\n raise RuntimeError(\"request headers not found\")\n reqheaders = event[\"headers\"]\n if \"origin\" not in reqheaders:\n raise RuntimeError(\"origin not present in request headers\")\n respheaders[\"Access-Control-Allow-Origin\"] = reqheaders[\"origin\"]\n if \"body\" not in event:\n raise RuntimeError(\"body not found\")\n rawbody = event[\"body\"]\n if (\"isBase64Encoded\" in event and\n event[\"isBase64Encoded\"]):\n rawbody = base64.b64decode(rawbody)\n try:\n reqbody = json.loads(rawbody)\n except:\n raise RuntimeError(\"couldn't parse JSON request body\")\n if not isinstance(reqbody, dict):\n raise RuntimeError(\"request body must be a dict\")\n if not \"message\" in reqbody:\n raise RuntimeError(\"message attribute not found\")\n message = reqbody[\"message\"]\n if message in [\"\", None]:\n raise RuntimeError(\"message attribute can't be blank\")\n respbody = f\"you sent '{message}' via POST\"\n return {\"statusCode\": 200,\n \"headers\": respheaders,\n \"body\": respbody}\n except RuntimeError as error:\n return {\"statusCode\": 400,\n \"headers\": respheaders,\n \"body\": str(error)}\n\nif __name__ == \"__main__\":\n for headers in [{},\n {\"origin\": \"http://localhost:3000\"}]:\n for event in [{},\n {\"body\": json.dumps([])},\n {\"body\": json.dumps({\"message\": \"Hello World\"})},\n {\"body\": base64.b64encode(json.dumps({\"message\": \"Hello World\"}).encode(\"utf-8\")),\n \"isBase64Encoded\": True},\n {\"body\": json.dumps({\"message\": \"\"})}, \n {\"body\": json.dumps({\"messag\": \"Hello World\"})}]:\n event[\"headers\"] = headers # NB\n resp = handler(event)\n print (\"%s\\t%s\" % (resp[\"statusCode\"], resp[\"body\"]))\n \n"
},
"Handler": "index.handler",
"MemorySize": 512,
"Role": {
"Fn::GetAtt": [
"AppPrivatePostRole",
"Arn"
]
},
"Runtime": "python3.10",
"Timeout": 5
},
"Type": "AWS::Lambda::Function"
},
"AppPrivatePostIntegration": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"IntegrationMethod": "POST",
"IntegrationType": "AWS_PROXY",
"IntegrationUri": {
"Fn::Sub": [
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
{
"arn": {
"Fn::GetAtt": [
"AppPrivatePostFunction",
"Arn"
]
}
}
]
},
"PayloadFormatVersion": "2.0"
},
"Type": "AWS::ApiGatewayV2::Integration"
},
"AppPrivatePostPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "AppPrivatePostFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppApi}/${AppStage}/POST/private-post"
}
},
"Type": "AWS::Lambda::Permission"
},
"AppPrivatePostPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-private-post-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppPrivatePostRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppPrivatePostRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppPrivatePostRoute": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"AuthorizationType": "JWT",
"AuthorizerId": {
"Ref": "AppAuthorizer"
},
"RouteKey": "POST /private-post",
"Target": {
"Fn::Sub": "integrations/${AppPrivatePostIntegration}"
}
},
"Type": "AWS::ApiGatewayV2::Route"
},
"AppPublicGetFunction": {
"Properties": {
"Code": {
"ZipFile": "Headers = {\n \"Content-Type\": \"text/plain\",\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,GET\"\n}\n\ndef handler(event, context = None, respheaders = Headers):\n try:\n if \"headers\" not in event:\n raise RuntimeError(\"request headers not found\")\n reqheaders = event[\"headers\"]\n if \"origin\" not in reqheaders:\n raise RuntimeError(\"origin not present in request headers\")\n respheaders[\"Access-Control-Allow-Origin\"] = reqheaders[\"origin\"]\n if \"queryStringParameters\" not in event:\n raise RuntimeError(\"querystring not found\")\n qs = event[\"queryStringParameters\"]\n if \"message\" not in qs:\n raise RuntimeError(\"message parameter not found\")\n message = qs[\"message\"]\n if message in [\"\", None]:\n raise RuntimeError(\"message parameter can't be blank\")\n respbody = f\"you sent '{message}' via GET\" \n return {\"statusCode\": 200,\n \"headers\": respheaders,\n \"body\": respbody}\n except RuntimeError as error:\n return {\"statusCode\": 400,\n \"headers\": respheaders,\n \"body\": str(error)}\n\nif __name__ == \"__main__\":\n for headers in [{},\n {\"origin\": \"http://localhost:3000\"}]:\n for event in [{},\n {\"queryStringParameters\": {\"message\": \"Hello World\"}},\n {\"queryStringParameters\": {\"message\": \"\"}},\n {\"queryStringParameters\": {\"messag\": \"Hello World\"}}]:\n event[\"headers\"] = headers # NB\n resp = handler(event)\n print (\"%s\\t%s\" % (resp[\"statusCode\"], resp[\"body\"]))\n\n"
},
"Handler": "index.handler",
"MemorySize": 512,
"Role": {
"Fn::GetAtt": [
"AppPublicGetRole",
"Arn"
]
},
"Runtime": "python3.10",
"Timeout": 5
},
"Type": "AWS::Lambda::Function"
},
"AppPublicGetIntegration": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"IntegrationMethod": "POST",
"IntegrationType": "AWS_PROXY",
"IntegrationUri": {
"Fn::Sub": [
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
{
"arn": {
"Fn::GetAtt": [
"AppPublicGetFunction",
"Arn"
]
}
}
]
},
"PayloadFormatVersion": "2.0"
},
"Type": "AWS::ApiGatewayV2::Integration"
},
"AppPublicGetPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "AppPublicGetFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppApi}/${AppStage}/GET/public-get"
}
},
"Type": "AWS::Lambda::Permission"
},
"AppPublicGetPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-public-get-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppPublicGetRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppPublicGetRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppPublicGetRoute": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"AuthorizationType": "NONE",
"RouteKey": "GET /public-get",
"Target": {
"Fn::Sub": "integrations/${AppPublicGetIntegration}"
}
},
"Type": "AWS::ApiGatewayV2::Route"
},
"AppPublicPostFunction": {
"Properties": {
"Code": {
"ZipFile": "import base64, json\n\nHeaders = {\n \"Content-Type\": \"text/plain\",\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}\n\ndef handler(event, context = None, respheaders = Headers):\n try:\n if \"headers\" not in event:\n raise RuntimeError(\"request headers not found\")\n reqheaders = event[\"headers\"]\n if \"origin\" not in reqheaders:\n raise RuntimeError(\"origin not present in request headers\")\n respheaders[\"Access-Control-Allow-Origin\"] = reqheaders[\"origin\"]\n if \"body\" not in event:\n raise RuntimeError(\"body not found\")\n rawbody = event[\"body\"]\n if (\"isBase64Encoded\" in event and\n event[\"isBase64Encoded\"]):\n rawbody = base64.b64decode(rawbody)\n try:\n reqbody = json.loads(rawbody)\n except:\n raise RuntimeError(\"couldn't parse JSON request body\")\n if not isinstance(reqbody, dict):\n raise RuntimeError(\"request body must be a dict\")\n if not \"message\" in reqbody:\n raise RuntimeError(\"message attribute not found\")\n message = reqbody[\"message\"]\n if message in [\"\", None]:\n raise RuntimeError(\"message attribute can't be blank\")\n respbody = f\"you sent '{message}' via POST\"\n return {\"statusCode\": 200,\n \"headers\": respheaders,\n \"body\": respbody}\n except RuntimeError as error:\n return {\"statusCode\": 400,\n \"headers\": respheaders,\n \"body\": str(error)}\n\nif __name__ == \"__main__\":\n for headers in [{},\n {\"origin\": \"http://localhost:3000\"}]:\n for event in [{},\n {\"body\": json.dumps([])},\n {\"body\": json.dumps({\"message\": \"Hello World\"})},\n {\"body\": base64.b64encode(json.dumps({\"message\": \"Hello World\"}).encode(\"utf-8\")),\n \"isBase64Encoded\": True},\n {\"body\": json.dumps({\"message\": \"\"})}, \n {\"body\": json.dumps({\"messag\": \"Hello World\"})}]:\n event[\"headers\"] = headers # NB\n resp = handler(event)\n print (\"%s\\t%s\" % (resp[\"statusCode\"], resp[\"body\"]))\n \n"
},
"Handler": "index.handler",
"MemorySize": 512,
"Role": {
"Fn::GetAtt": [
"AppPublicPostRole",
"Arn"
]
},
"Runtime": "python3.10",
"Timeout": 5
},
"Type": "AWS::Lambda::Function"
},
"AppPublicPostIntegration": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"IntegrationMethod": "POST",
"IntegrationType": "AWS_PROXY",
"IntegrationUri": {
"Fn::Sub": [
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
{
"arn": {
"Fn::GetAtt": [
"AppPublicPostFunction",
"Arn"
]
}
}
]
},
"PayloadFormatVersion": "2.0"
},
"Type": "AWS::ApiGatewayV2::Integration"
},
"AppPublicPostPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "AppPublicPostFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppApi}/${AppStage}/POST/public-post"
}
},
"Type": "AWS::Lambda::Permission"
},
"AppPublicPostPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-public-post-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppPublicPostRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppPublicPostRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppPublicPostRoute": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"AuthorizationType": "NONE",
"RouteKey": "POST /public-post",
"Target": {
"Fn::Sub": "integrations/${AppPublicPostIntegration}"
}
},
"Type": "AWS::ApiGatewayV2::Route"
},
"AppRecordSet": {
"Properties": {
"AliasTarget": {
"DNSName": {
"Fn::GetAtt": [
"AppDomainName",
"RegionalDomainName"
]
},
"EvaluateTargetHealth": false,
"HostedZoneId": {
"Fn::GetAtt": [
"AppDomainName",
"RegionalHostedZoneId"
]
}
},
"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"
},
"AppStage": {
"Properties": {
"ApiId": {
"Ref": "AppApi"
},
"AutoDeploy": true,
"StageName": "prod"
},
"Type": "AWS::ApiGatewayV2::Stage"
},
"AppUserPool": {
"Properties": {
"AutoVerifiedAttributes": [
"email"
],
"Policies": {
"PasswordPolicy": {
"MinimumLength": 8,
"RequireLowercase": true,
"RequireNumbers": true,
"RequireSymbols": true,
"RequireUppercase": true
}
},
"Schema": [
{
"AttributeDataType": "String",
"Mutable": true,
"Name": "email",
"Required": true,
"StringAttributeConstraints": {
"MinLength": "1"
}
}
],
"UsernameAttributes": [
"email"
]
},
"Type": "AWS::Cognito::UserPool"
},
"AppUserPoolClient": {
"Properties": {
"ExplicitAuthFlows": [
"ALLOW_USER_SRP_AUTH",
"ALLOW_ADMIN_USER_PASSWORD_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
],
"PreventUserExistenceErrors": "ENABLED",
"UserPoolId": {
"Ref": "AppUserPool"
}
},
"Type": "AWS::Cognito::UserPoolClient"
}
}
}

short

  • move setenv-priv stuff into setenv or app.props

pareto2

  • move simple email user pool into web_api demo
  • see if AltNamespaceMixin can be retired

medium

thoughts

done

[ERROR] JSONDecodeError: Expecting value: line 1 column 1 (char 0)Traceback (most recent call last):  File "/var/task/index.py", line 3, in handler    body=json.loads(event["body"])  File "/var/lang/lib/python3.10/json/__init__.py", line 346, in loads    return _default_decoder.decode(s)  File "/var/lang/lib/python3.10/json/decoder.py", line 337, in decode    obj, end = self.raw_decode(s, idx=_w(s, 0).end())  File "/var/lang/lib/python3.10/json/decoder.py", line 355, in raw_decode    raise JSONDecodeError("Expecting value", s, err.value) from None
  • private endpoints not working :(
  • test public endpoints
 Resource handler returned message: "Unexpected or malformed target in route null. Correct format should be integrations/<integration_id>. (Service: AmazonApiGatewayV2; Status Code: 400; Error Code: BadRequestException; Request ID: 9577f060-14f7-4511-a138-22629c5b3175; Proxy: null)" (RequestToken: 44bc3fe1-4bcb-bfd7-faf8-c8584b0aa327, HandlerErrorCode: GeneralServiceException)                                           |
  Resource handler returned message: "Invalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression specified: querystrings.message] (Service: AmazonApiGatewayV2; Status Code: 400; Error Code: BadRequestException; Request ID: 85caa3aa-f9ec-4019-92b7-e95a604413f9; Proxy: null)" (RequestToken: 173dc5c4-75f6-c96e-3318-450d44368b93, HandlerErrorCode: GeneralServiceException)   |
|  2024-03-27T11:54:08.817Z|  AppApiMapping                   |  AWS::ApiGatewayV2::ApiMapping |  CREATE_FAILED      |  Properties validation failed for resource AppApiMapping with message:
#: required key [Stage] not found  
|  2024-03-27T11:54:09.204Z|  AppRecordSet                    |  AWS::Route53::RecordSet       |  CREATE_FAILED      |  Requested attribute DistributionHostedZoneId does not exist in schema for AWS::ApiGatewayV2::DomainName   |
024-03-27T11:27:47.482Z|  AppDomainName       |  AWS::ApiGatewayV2::DomainName |  CREATE_FAILED        |  Resource handler returned message: "Invalid certificate ARN: arn:aws:acm:us-east-1:119552584133:certificate/77b7bf9e-38e9-4115-82bf-4403bbb30c02. Certificate must be in 'eu-west-1'. (Service: AmazonApiGatewayV2; Status Code: 400; Error Code: BadRequestException; Request ID: 66b08e2c-5128-4a8c-97dc-c79d7dbbce4e; Proxy: null)" (RequestToken: 78213f09-fb8e-1dcb-93ff-5a943c3cc418, HandlerErrorCode: GeneralServiceException)   |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment