In my post Creating an API with AWS: Part 4: Cognito Authentication, I added authentication to the API using AWS Cognito. In this post, I’ll add DELETE methods to each endpoint, so that we have full CRUD (Create, Read, Update, Delete) functionality. This means we’ll be able to create, get and delete links, tags and tag links using the API.
Prerequisites
Before you start, you’ll need the following things:
- An AWS account
- An Administrator IAM User with which to use the AWS CLI
- AWS CLI set up on your local machine
- The CloudFormation template from my previous post, Creating an API with AWS: Part 4: Cognito Authentication.
Note that AWS will charge you for creating and hosting this API if you don’t have Free Tier on your account.
Step 1: Add a GlobalSecondaryIndex to the tagLinkTable in the CloudFormation Template
Starting with the CloudFormation template from my blog post Creating an API with AWS: Part 4: Cognito Authentication, add a GlobalSecondaryIndexes section to the tagLinkTable resource:
GlobalSecondaryIndexes:
-
IndexName: "link_id_index"
KeySchema:
-
AttributeName: "link_id"
KeyType: "HASH"
-
AttributeName: "tag_id"
KeyType: "RANGE"
Projection:
ProjectionType: "KEYS_ONLY"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
This will enable us to query on the taglink table by link_id as well as tag_id, which will be needed when we come to look up which records to delete.
Step 2: Add the DELETE methods to the CloudFormation Template
Add DELETE methods for each endpoint, updating the DependsOn attributes for the resources that come after each DELETE method:
ProxyResourceLinkDELETE:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: DELETE
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceLinkPOST
- apiauth
ProxyResourceTagDELETE:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref tag
HttpMethod: DELETE
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagPOST
- apiauth
ProxyResourceTagLinkDELETE:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref taglink
HttpMethod: DELETE
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagLinkPOST
- apiauth
Step 3: Update the Lambda Function and update the CloudFormation Template
Update the lambda function with methods for handling DELETE requests for each endpoint, and for querying the database tables and performing the deletions. Update the CFT with the new code. The updated code should look like:
import json
import boto3
import uuid
from botocore.exceptions import ClientError
# Establish credentials
session_var = boto3.session.Session()
credentials = session_var.get_credentials()
""" --- Parameters --- """
# DynamoDB tables, read lambda function environment variables or initialize with defaults if they not exist.
DYNAMODB_LINK_TABLE = 'linkTable'
DYNAMODB_TAG_TABLE = 'tagTable'
DYNAMODB_TAG_LINK_TABLE = 'tagLinkTable'
# Initialize DynamoDB Client
dynamodb = boto3.client('dynamodb')
def store_link(link):
link_id = str(uuid.uuid4())
dynamodb.put_item(
TableName=DYNAMODB_LINK_TABLE,
Item={
'link_id': {
'S': str(link_id)
},
'link': {
'S': str(link)
}
}
)
return link_id
def store_tag(tag):
tag_id = str(uuid.uuid4())
dynamodb.put_item(
TableName=DYNAMODB_TAG_TABLE,
Item={
'tag_id': {
'S': str(tag_id)
},
'tag': {
'S': str(tag)
}
}
)
return tag_id
def store_taglink(tag_id, link_id):
dynamodb.put_item(
TableName=DYNAMODB_TAG_LINK_TABLE,
Item={
'tag_id': {
'S': str(tag_id)
},
'link_id': {
'S': str(link_id)
}
}
)
def get_link_id(link):
result = dynamodb.query(
TableName=DYNAMODB_LINK_TABLE,
IndexName='link_index',
KeyConditionExpression='link = :link',
ExpressionAttributeValues={
':link': {
"S": link
}
}
)
if result['Count'] > 0:
link_id = result['Items'][0]['link_id']['S']
else:
link_id = store_link(link)
return link_id
def get_tag_id(tag):
result = dynamodb.query(
TableName=DYNAMODB_TAG_TABLE,
IndexName='tag_index',
KeyConditionExpression='tag = :tag',
ExpressionAttributeValues={
':tag': {
"S": tag
}
}
)
if result['Count'] > 0:
tag_id = result['Items'][0]['tag_id']['S']
else:
tag_id = store_tag(tag)
return tag_id
def get_tags(tag_id=None):
tags = []
if tag_id is None:
result = dynamodb.scan(
TableName=DYNAMODB_TAG_TABLE,
Select="SPECIFIC_ATTRIBUTES",
ProjectionExpression="tag_id,tag"
)
else:
# Run a query with a filter on tag_id on the tagTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_TABLE,
KeyConditionExpression='tag_id = :tag_id',
ExpressionAttributeValues={
':tag_id': {
"S": tag_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
tag = {}
tag['tag_id'] = result['Items'][x]['tag_id']['S']
tag['tag'] = result['Items'][x]['tag']['S']
tags.append(tag)
return tags
def extract_link_result(result):
links = []
if result['Count'] > 0:
for x in range(result['Count']):
link = {}
link['link_id'] = result['Items'][x]['link_id']['S']
link['link'] = result['Items'][x]['link']['S']
links.append(link)
return links
def get_links(tag_id=None, link_id=None):
links = []
link_id_list = []
if tag_id is None and link_id is None:
result = dynamodb.scan(
TableName=DYNAMODB_LINK_TABLE,
Select="SPECIFIC_ATTRIBUTES",
ProjectionExpression="link_id,link"
)
links = extract_link_result(result)
elif link_id is not None:
# Run a query with a filter on link_id on the linkTable
result = dynamodb.query(
TableName=DYNAMODB_LINK_TABLE,
KeyConditionExpression='link_id = :link_id',
ExpressionAttributeValues={
':link_id': {
"S": link_id
}
}
)
links = extract_link_result(result)
else:
# Run a query with a filter on tag_id on the tagLinkTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_LINK_TABLE,
KeyConditionExpression='tag_id = :tag_id',
ExpressionAttributeValues={
':tag_id': {
"S": tag_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
link_id_list.append(result['Items'][x]['link_id']['S'])
# From the retrieved link_ids, retrieve the links from the link table
if len(link_id_list) > 0:
for link_id in link_id_list:
result = dynamodb.query(
TableName=DYNAMODB_LINK_TABLE,
KeyConditionExpression='link_id = :link_id',
ExpressionAttributeValues={
':link_id': {
"S": link_id
}
}
)
if result['Count'] > 0:
link = {}
link['link_id'] = link_id
# There should only be one result per link_id
link['link'] = result['Items'][0]['link']['S']
links.append(link)
return links
def get_taglinks(tag_id=None, link_id=None):
taglinks = []
if tag_id is not None and link_id is None:
# Run a query with a filter on tag_id on the tagLinkTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_LINK_TABLE,
KeyConditionExpression='tag_id = :tag_id',
ExpressionAttributeValues={
':tag_id': {
"S": tag_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
taglink = {}
taglink['tag_id'] = tag_id
taglink['link_id'] = result['Items'][x]['link_id']['S']
taglinks.append(taglink)
elif link_id is not None and tag_id is None:
# Run a query with a filter on link_id on the tagLinkTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_LINK_TABLE,
IndexName='link_id_index',
KeyConditionExpression='link_id = :link_id',
ExpressionAttributeValues={
':link_id': {
"S": link_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
taglink = {}
taglink['link_id'] = link_id
taglink['tag_id'] = result['Items'][x]['tag_id']['S']
taglinks.append(taglink)
elif link_id is not None and tag_id is not None:
result = dynamodb.get_item(
TableName=DYNAMODB_TAG_LINK_TABLE,
Key={
'link_id': {
"S": link_id
},
'tag_id': {
"S": tag_id
}
}
)
if result:
taglink = {}
taglink['link_id'] = result['Item']['link_id']['S']
taglink['tag_id'] = result['Item']['tag_id']['S']
taglinks.append(taglink)
return taglinks
def delete_link(link_id):
links = get_links(tag_id=None, link_id=link_id)
for link in links:
dynamodb.delete_item(
TableName=DYNAMODB_LINK_TABLE,
Key={
'link_id': {
"S": link['link_id']
},
'link': {
"S": link['link']
}
}
)
return
def delete_tag(tag_id):
tags = get_tags(tag_id=tag_id)
for tag in tags:
dynamodb.delete_item(
TableName=DYNAMODB_TAG_TABLE,
Key={
'tag_id': {
"S": tag['tag_id'],
},
'tag': {
"S": tag['tag']
}
}
)
return
def delete_taglinks(tag_id=None, link_id=None):
taglinks = get_taglinks(tag_id=tag_id, link_id=link_id)
for taglink in taglinks:
dynamodb.delete_item(
TableName=DYNAMODB_TAG_LINK_TABLE,
Key={
'tag_id': {
"S": taglink['tag_id'],
},
'link_id': {
"S": taglink['link_id']
},
}
)
return
def error_response(error_message, aws_request_id):
return {
"statusCode": 500,
"isBase64Encoded": False,
"body": json.dumps({
"Error": error_message,
"Reference": aws_request_id,
}),
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
def bad_request_response(error_message, aws_request_id):
return {
"statusCode": 400,
"isBase64Encoded": False,
"body": json.dumps({
"Error": error_message,
"Reference": aws_request_id,
}),
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
def lambda_handler(event, context):
try:
print(event)
print(context)
return_body = ""
if event['resource'] == '/link':
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
link = post_body['link']
tag = post_body.get('tag')
tag_id = post_body.get('tag_id')
if tag_id is None:
tag_id = get_tag_id(tag)
else:
tags = get_tags(tag_id)
if not tags:
return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
link_id = get_link_id(link)
store_taglink(tag_id, link_id)
return_body = json.dumps({"link_id": link_id})
elif event['httpMethod'] == "GET":
tag_id = None
link_id = None
query_params = event['queryStringParameters']
if query_params is not None:
tag_id = query_params.get('tag_id', None)
tag = query_params.get('tag', None)
link_id = query_params.get('link_id', None)
if tag_id is None and tag is not None:
tag_id = get_tag_id(tag)
links = get_links(tag_id, link_id)
return_body = json.dumps(links)
elif event['httpMethod'] == "DELETE":
query_params = event['queryStringParameters']
link_id = query_params.get('link_id', None)
if not link_id:
return bad_request_response('link_id is required', context.aws_request_id)
delete_link(link_id)
delete_taglinks(tag_id=None, link_id=link_id)
return_body = json.dumps({"link_id": link_id})
elif event['resource'] == '/tag':
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
tag = post_body.get('tag')
tag_id = get_tag_id(tag)
return_body = json.dumps({"tag_id": tag_id})
elif event['httpMethod'] == "GET":
tag_id = None
query_params = event['queryStringParameters']
if query_params is not None:
tag_id = query_params.get('tag_id', None)
tag = query_params.get('tag', None)
if tag_id is None and tag is not None:
tag_id = get_tag_id(tag)
tags = get_tags(tag_id)
return_body = json.dumps(tags)
elif event['httpMethod'] == "DELETE":
query_params = event['queryStringParameters']
tag_id = query_params.get('tag_id', None)
if not tag_id:
return bad_request_response('tag_id is required', context.aws_request_id)
delete_tag(tag_id)
delete_taglinks(tag_id=tag_id, link_id=None)
return_body = json.dumps({"tag_id": tag_id})
elif event['resource'] == '/taglink':
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
tag_id = post_body.get('tag_id')
link_id = post_body.get('link_id')
tags = get_tags(tag_id)
links = get_links(tag_id=None, link_id=link_id)
if not tags:
return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
if not links:
return bad_request_response('Unknown link_id ' + link_id, context.aws_request_id)
store_taglink(tag_id, link_id)
return_body = json.dumps({"tag_id": tag_id, "link_id": link_id})
elif event['httpMethod'] == "DELETE":
query_params = event['queryStringParameters']
tag_id = query_params.get('tag_id', None)
link_id = query_params.get('link_id', None)
if not tag_id:
return bad_request_response('tag_id is required', context.aws_request_id)
if not link_id:
return bad_request_response('link_id is required', context.aws_request_id)
delete_taglinks(tag_id, link_id)
return_body = json.dumps({"tag_id": tag_id, "link_id": link_id})
# Because this Lambda function is called by an API Gateway proxy integration
# the result object must use the following structure.
return {
"statusCode": 200,
"isBase64Encoded": False,
"body": return_body,
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
except Exception as err:
print(str(err))
# If there is an error during processing, catch it and return
# from the Lambda function successfully. Specify a 500 HTTP status
# code and provide an error message in the body. This will provide a
# more meaningful error response to the end client.
return error_response(str(err), context.aws_request_id)
Step 4: Update the test.sh script
Add tests for the new endpoint methods and auth token authentication to the test.sh script. The new script should look like:
BASEURL=$1
echo "=========================="
echo "Test POST /link link=https://www.allthecoding.com, tag=blog"
echo $HEADERS
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link\": \"https://www.allthecoding.com\", \"tag\": \"blog\"}" $BASEURL/link)
echo $resp
link_id1=$(echo $resp | jq -r '.link_id')
echo $link_id1
echo "=========================="
echo "Test GET /tag tag=blog"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/tag?tag=blog)
echo $resp
tag_id1=$(echo $resp | jq -r '.[0].tag_id')
echo $tag_id1
echo "=========================="
echo "Test POST /tag tag=stuff"
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"tag\": \"stuff\"}" $BASEURL/tag)
echo $resp
tag_id2=$(echo $resp | jq -r '.tag_id')
echo $tag_id2
echo "=========================="
echo "Test POST /link link=https://www.google.com, tag=stuff"
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link\": \"https://www.google.com\", \"tag\": \"stuff\"}" $BASEURL/link)
echo $resp
link_id2=$(echo $resp | jq -r '.link_id')
echo $link_id2
echo "=========================="
echo "Test POST /link link=https://www.google.com, tag_id=$tag_id1"
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link\": \"https://www.google.com\", \"tag_id\": \"$tag_id1\"}" $BASEURL/link)
echo $resp
link_id2=$(echo $resp | jq -r '.link_id')
echo $link_id2
echo "=========================="
echo "Test POST /link link=https://www.google.com, tag_id=invalid". Expect 400 response: Unknown tag_id invalid
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link\": \"https://www.google.com\", \"tag_id\": \"invalid\"}" $BASEURL/link)
echo $resp
echo "=========================="
echo "Test GET /link"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/link)
echo $resp
echo "=========================="
echo "Test GET /link tag_id=$tag_id1"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/link?tag_id=$tag_id1)
echo $resp
echo "=========================="
echo "Test GET /link tag=blog"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/link?tag=blog)
echo $resp
echo "=========================="
echo "Test GET /link link_id=$link_id1"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/link?link_id=$link_id1)
echo $resp
echo "=========================="
echo "Test GET /tag tag_id=$tag_id1"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/tag?tag_id=$tag_id1)
echo $resp
echo "=========================="
echo "Test GET /tag"
resp=$(curl -X "GET" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/tag)
echo $resp
echo "=========================="
echo "Test POST /taglink tag_id=$tag_id1, link_id=$link_id1"
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"$tag_id1\"}" $BASEURL/taglink)
echo $resp
echo "=========================="
echo "Test POST /taglink tag_id=invalid, link_id=$link_id1. Expect 400 response: Unknown tag_id invalid"
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"invalid\"}" $BASEURL/taglink)
echo $resp
echo "=========================="
echo "Test POST /taglink tag_id=$tag_id1, link_id=invalid. Expect 400 response: Unknown link_id invalid"
resp=$(curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link_id\": \"invalid\", \"tag_id\": \"$tag_id1\"}" $BASEURL/taglink)
echo $resp
echo "=========================="
echo "Test DELETE /link link_id=$link_id1"
resp=$(curl -X "DELETE" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/link?link_id=$link_id1)
echo $resp
echo "=========================="
echo "Test DELETE /tag tag_id=$tag_id1"
resp=$(curl -X "DELETE" -H "Content-Type: application/json" -H "Authorization: $TOKEN" $BASEURL/tag?tag_id=$tag_id1)
echo $resp
echo "=========================="
echo "Test DELETE /taglink tag_id=$tag_id2, link_id=$link_id2"
resp=$(curl -X "DELETE" -H "Content-Type: application/json" -H "Authorization: $TOKEN" "$BASEURL/taglink?tag_id=$tag_id2&link_id=$link_id2")
echo $resp
Step 5: Create an authentication.sh script
Create a new script called authentication.sh. This script will run the authentication steps from Creating an API with AWS: Part 4: Cognito Authentication and generate an authentication token:
EMAIL=$1
PASSWORD=$2
aws cognito-idp admin-create-user --user-pool-id $POOL_ID --username $EMAIL
aws cognito-idp admin-set-user-password --user-pool-id $POOL_ID --username $EMAIL --password $PASSWORD --permanent
resp=$(aws cognito-idp describe-user-pool-client --client-id $CLIENT_ID --user-pool-id $POOL_ID)
CLIENT_SECRET=$(echo $resp | jq -r '.UserPoolClient.ClientSecret')
SECRET_HASH=$(python get_client_secret.py $EMAIL $CLIENT_ID $CLIENT_SECRET | awk -F: '{print $2}' | xargs)
resp=$(aws cognito-idp initiate-auth --region us-east-1 --auth-flow USER_PASSWORD_AUTH --client-id $CLIENT_ID --auth-parameters USERNAME=$EMAIL,PASSWORD=$PASSWORD,SECRET_HASH=$SECRET_HASH)
TOKEN=$(echo $resp | jq -r '.AuthenticationResult.IdToken')
echo $TOKEN
export TOKEN=$TOKEN
Create a python script called get_client_secret.py (if you haven’t already) and add the following code:
import sys
import hmac, hashlib, base64
username = sys.argv[1]
app_client_id = sys.argv[2]
key = sys.argv[3]
message = bytes(sys.argv[1]+sys.argv[2], 'utf-8')
key = bytes(sys.argv[3], 'utf-8')
secret_hash = base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()
print("SECRET HASH:", secret_hash)
Step 6: Launch the CloudFormation Template
Save the CloudFormation template to a file called setupapidbauthdel.yml. With the description updated, the whole file should look like the below:
AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a simple API with three endpoints that can be called to POST/GET/DELETE links and tags and tag links, with a DynamoDB database
Resources:
linkTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
-
AttributeName: "link_id"
AttributeType: "S"
-
AttributeName: "link"
AttributeType: "S"
KeySchema:
-
AttributeName: "link_id"
KeyType: "HASH"
-
AttributeName: "link"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
TableName: "linkTable"
GlobalSecondaryIndexes:
-
IndexName: "link_index"
KeySchema:
-
AttributeName: "link"
KeyType: "HASH"
-
AttributeName: "link_id"
KeyType: "RANGE"
Projection:
ProjectionType: "KEYS_ONLY"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
tagTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
-
AttributeName: "tag_id"
AttributeType: "S"
-
AttributeName: "tag"
AttributeType: "S"
KeySchema:
-
AttributeName: "tag_id"
KeyType: "HASH"
-
AttributeName: "tag"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
TableName: "tagTable"
GlobalSecondaryIndexes:
-
IndexName: "tag_index"
KeySchema:
-
AttributeName: "tag"
KeyType: "HASH"
-
AttributeName: "tag_id"
KeyType: "RANGE"
Projection:
ProjectionType: "KEYS_ONLY"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
tagLinkTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
-
AttributeName: "tag_id"
AttributeType: "S"
-
AttributeName: "link_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "tag_id"
KeyType: "HASH"
-
AttributeName: "link_id"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
TableName: "tagLinkTable"
GlobalSecondaryIndexes:
-
IndexName: "link_id_index"
KeySchema:
-
AttributeName: "link_id"
KeyType: "HASH"
-
AttributeName: "tag_id"
KeyType: "RANGE"
Projection:
ProjectionType: "KEYS_ONLY"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
apiuserpool:
Type: AWS::Cognito::UserPool
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
UsernameConfiguration:
CaseSensitive: false
UserPoolName: apiuserpool
applicationAPIUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
AccessTokenValidity: 1
ClientName: applicationapiclient
EnableTokenRevocation: true
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
- ALLOW_ADMIN_USER_PASSWORD_AUTH
GenerateSecret: true
IdTokenValidity: 1
PreventUserExistenceErrors: ENABLED
RefreshTokenValidity: 1
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref apiuserpool
DependsOn:
- apiuserpool
requestHandlerLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: Allow
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
Policies:
-
PolicyName: "root"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- logs:CreateLogGroup
- logs:PutLogEvents
- logs:CreateLogStream
Resource: "*"
-
PolicyName: "db_access"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- dynamodb:DeleteItem
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Scan
- dynamodb:UpdateItem
- dynamodb:Query
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*"
RoleName: requestHandlerLambdaExecutionRole
requestHandler:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: !Sub |
import json
import boto3
import uuid
from botocore.exceptions import ClientError
# Establish credentials
session_var = boto3.session.Session()
credentials = session_var.get_credentials()
""" --- Parameters --- """
# DynamoDB tables, read lambda function environment variables or initialize with defaults if they not exist.
DYNAMODB_LINK_TABLE = 'linkTable'
DYNAMODB_TAG_TABLE = 'tagTable'
DYNAMODB_TAG_LINK_TABLE = 'tagLinkTable'
# Initialize DynamoDB Client
dynamodb = boto3.client('dynamodb')
def store_link(link):
link_id = str(uuid.uuid4())
dynamodb.put_item(
TableName=DYNAMODB_LINK_TABLE,
Item={
'link_id': {
'S': str(link_id)
},
'link': {
'S': str(link)
}
}
)
return link_id
def store_tag(tag):
tag_id = str(uuid.uuid4())
dynamodb.put_item(
TableName=DYNAMODB_TAG_TABLE,
Item={
'tag_id': {
'S': str(tag_id)
},
'tag': {
'S': str(tag)
}
}
)
return tag_id
def store_taglink(tag_id, link_id):
dynamodb.put_item(
TableName=DYNAMODB_TAG_LINK_TABLE,
Item={
'tag_id': {
'S': str(tag_id)
},
'link_id': {
'S': str(link_id)
}
}
)
def get_link_id(link):
result = dynamodb.query(
TableName=DYNAMODB_LINK_TABLE,
IndexName='link_index',
KeyConditionExpression='link = :link',
ExpressionAttributeValues={
':link': {
"S": link
}
}
)
if result['Count'] > 0:
link_id = result['Items'][0]['link_id']['S']
else:
link_id = store_link(link)
return link_id
def get_tag_id(tag):
result = dynamodb.query(
TableName=DYNAMODB_TAG_TABLE,
IndexName='tag_index',
KeyConditionExpression='tag = :tag',
ExpressionAttributeValues={
':tag': {
"S": tag
}
}
)
if result['Count'] > 0:
tag_id = result['Items'][0]['tag_id']['S']
else:
tag_id = store_tag(tag)
return tag_id
def get_tags(tag_id=None):
tags = []
if tag_id is None:
result = dynamodb.scan(
TableName=DYNAMODB_TAG_TABLE,
Select="SPECIFIC_ATTRIBUTES",
ProjectionExpression="tag_id,tag"
)
else:
# Run a query with a filter on tag_id on the tagTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_TABLE,
KeyConditionExpression='tag_id = :tag_id',
ExpressionAttributeValues={
':tag_id': {
"S": tag_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
tag = {}
tag['tag_id'] = result['Items'][x]['tag_id']['S']
tag['tag'] = result['Items'][x]['tag']['S']
tags.append(tag)
return tags
def extract_link_result(result):
links = []
if result['Count'] > 0:
for x in range(result['Count']):
link = {}
link['link_id'] = result['Items'][x]['link_id']['S']
link['link'] = result['Items'][x]['link']['S']
links.append(link)
return links
def get_links(tag_id=None, link_id=None):
links = []
link_id_list = []
if tag_id is None and link_id is None:
result = dynamodb.scan(
TableName=DYNAMODB_LINK_TABLE,
Select="SPECIFIC_ATTRIBUTES",
ProjectionExpression="link_id,link"
)
links = extract_link_result(result)
elif link_id is not None:
# Run a query with a filter on link_id on the linkTable
result = dynamodb.query(
TableName=DYNAMODB_LINK_TABLE,
KeyConditionExpression='link_id = :link_id',
ExpressionAttributeValues={
':link_id': {
"S": link_id
}
}
)
links = extract_link_result(result)
else:
# Run a query with a filter on tag_id on the tagLinkTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_LINK_TABLE,
KeyConditionExpression='tag_id = :tag_id',
ExpressionAttributeValues={
':tag_id': {
"S": tag_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
link_id_list.append(result['Items'][x]['link_id']['S'])
# From the retrieved link_ids, retrieve the links from the link table
if len(link_id_list) > 0:
for link_id in link_id_list:
result = dynamodb.query(
TableName=DYNAMODB_LINK_TABLE,
KeyConditionExpression='link_id = :link_id',
ExpressionAttributeValues={
':link_id': {
"S": link_id
}
}
)
if result['Count'] > 0:
link = {}
link['link_id'] = link_id
# There should only be one result per link_id
link['link'] = result['Items'][0]['link']['S']
links.append(link)
return links
def get_taglinks(tag_id=None, link_id=None):
taglinks = []
if tag_id is not None and link_id is None:
# Run a query with a filter on tag_id on the tagLinkTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_LINK_TABLE,
KeyConditionExpression='tag_id = :tag_id',
ExpressionAttributeValues={
':tag_id': {
"S": tag_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
taglink = {}
taglink['tag_id'] = tag_id
taglink['link_id'] = result['Items'][x]['link_id']['S']
taglinks.append(taglink)
elif link_id is not None and tag_id is None:
# Run a query with a filter on link_id on the tagLinkTable
result = dynamodb.query(
TableName=DYNAMODB_TAG_LINK_TABLE,
IndexName='link_id_index',
KeyConditionExpression='link_id = :link_id',
ExpressionAttributeValues={
':link_id': {
"S": link_id
}
}
)
if result['Count'] > 0:
for x in range(result['Count']):
taglink = {}
taglink['link_id'] = link_id
taglink['tag_id'] = result['Items'][x]['tag_id']['S']
taglinks.append(taglink)
elif link_id is not None and tag_id is not None:
result = dynamodb.get_item(
TableName=DYNAMODB_TAG_LINK_TABLE,
Key={
'link_id': {
"S": link_id
},
'tag_id': {
"S": tag_id
}
}
)
if result:
taglink = {}
taglink['link_id'] = result['Item']['link_id']['S']
taglink['tag_id'] = result['Item']['tag_id']['S']
taglinks.append(taglink)
return taglinks
def delete_link(link_id):
links = get_links(tag_id=None, link_id=link_id)
for link in links:
dynamodb.delete_item(
TableName=DYNAMODB_LINK_TABLE,
Key={
'link_id': {
"S": link['link_id']
},
'link': {
"S": link['link']
}
}
)
return
def delete_tag(tag_id):
tags = get_tags(tag_id=tag_id)
for tag in tags:
dynamodb.delete_item(
TableName=DYNAMODB_TAG_TABLE,
Key={
'tag_id': {
"S": tag['tag_id'],
},
'tag': {
"S": tag['tag']
}
}
)
return
def delete_taglinks(tag_id=None, link_id=None):
taglinks = get_taglinks(tag_id=tag_id, link_id=link_id)
for taglink in taglinks:
dynamodb.delete_item(
TableName=DYNAMODB_TAG_LINK_TABLE,
Key={
'tag_id': {
"S": taglink['tag_id'],
},
'link_id': {
"S": taglink['link_id']
},
}
)
return
def error_response(error_message, aws_request_id):
return {
"statusCode": 500,
"isBase64Encoded": False,
"body": json.dumps({
"Error": error_message,
"Reference": aws_request_id,
}),
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
def bad_request_response(error_message, aws_request_id):
return {
"statusCode": 400,
"isBase64Encoded": False,
"body": json.dumps({
"Error": error_message,
"Reference": aws_request_id,
}),
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
def lambda_handler(event, context):
try:
print(event)
print(context)
return_body = ""
if event['resource'] == '/link':
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
link = post_body['link']
tag = post_body.get('tag')
tag_id = post_body.get('tag_id')
if tag_id is None:
tag_id = get_tag_id(tag)
else:
tags = get_tags(tag_id)
if not tags:
return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
link_id = get_link_id(link)
store_taglink(tag_id, link_id)
return_body = json.dumps({"link_id": link_id})
elif event['httpMethod'] == "GET":
tag_id = None
link_id = None
query_params = event['queryStringParameters']
if query_params is not None:
tag_id = query_params.get('tag_id', None)
tag = query_params.get('tag', None)
link_id = query_params.get('link_id', None)
if tag_id is None and tag is not None:
tag_id = get_tag_id(tag)
links = get_links(tag_id, link_id)
return_body = json.dumps(links)
elif event['httpMethod'] == "DELETE":
query_params = event['queryStringParameters']
link_id = query_params.get('link_id', None)
if not link_id:
return bad_request_response('link_id is required', context.aws_request_id)
delete_link(link_id)
delete_taglinks(tag_id=None, link_id=link_id)
return_body = json.dumps({"link_id": link_id})
elif event['resource'] == '/tag':
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
tag = post_body.get('tag')
tag_id = get_tag_id(tag)
return_body = json.dumps({"tag_id": tag_id})
elif event['httpMethod'] == "GET":
tag_id = None
query_params = event['queryStringParameters']
if query_params is not None:
tag_id = query_params.get('tag_id', None)
tag = query_params.get('tag', None)
if tag_id is None and tag is not None:
tag_id = get_tag_id(tag)
tags = get_tags(tag_id)
return_body = json.dumps(tags)
elif event['httpMethod'] == "DELETE":
query_params = event['queryStringParameters']
tag_id = query_params.get('tag_id', None)
if not tag_id:
return bad_request_response('tag_id is required', context.aws_request_id)
delete_tag(tag_id)
delete_taglinks(tag_id=tag_id, link_id=None)
return_body = json.dumps({"tag_id": tag_id})
elif event['resource'] == '/taglink':
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
tag_id = post_body.get('tag_id')
link_id = post_body.get('link_id')
tags = get_tags(tag_id)
links = get_links(tag_id=None, link_id=link_id)
if not tags:
return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
if not links:
return bad_request_response('Unknown link_id ' + link_id, context.aws_request_id)
store_taglink(tag_id, link_id)
return_body = json.dumps({"tag_id": tag_id, "link_id": link_id})
elif event['httpMethod'] == "DELETE":
query_params = event['queryStringParameters']
tag_id = query_params.get('tag_id', None)
link_id = query_params.get('link_id', None)
if not tag_id:
return bad_request_response('tag_id is required', context.aws_request_id)
if not link_id:
return bad_request_response('link_id is required', context.aws_request_id)
delete_taglinks(tag_id, link_id)
return_body = json.dumps({"tag_id": tag_id, "link_id": link_id})
# Because this Lambda function is called by an API Gateway proxy integration
# the result object must use the following structure.
return {
"statusCode": 200,
"isBase64Encoded": False,
"body": return_body,
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
except Exception as err:
print(str(err))
# If there is an error during processing, catch it and return
# from the Lambda function successfully. Specify a 500 HTTP status
# code and provide an error message in the body. This will provide a
# more meaningful error response to the end client.
return error_response(str(err), context.aws_request_id)
Description: Lambda function for processing api endpoint requests
FunctionName: requestHandler
Handler: index.lambda_handler
Role:
Fn::GetAtt:
- "requestHandlerLambdaExecutionRole"
- "Arn"
Runtime: python3.8
DependsOn:
- requestHandlerLambdaExecutionRole
ApplicationAPI:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ApplicationAPI
Description: Simple API for an application
DependsOn:
- requestHandler
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt
- requestHandler
- Arn
Action: 'lambda:InvokeFunction'
Principal: apigateway.amazonaws.com
SourceArn: !Join ["", [!Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:", !Ref ApplicationAPI, "/*"]]
DependsOn:
- ApplicationAPI
link:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApplicationAPI
ParentId: !GetAtt [ApplicationAPI, RootResourceId]
PathPart: 'link'
DependsOn:
- ApplicationAPI
tag:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApplicationAPI
ParentId: !GetAtt [ApplicationAPI, RootResourceId]
PathPart: 'tag'
DependsOn:
- ApplicationAPI
taglink:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApplicationAPI
ParentId: !GetAtt [ApplicationAPI, RootResourceId]
PathPart: 'taglink'
DependsOn:
- ApplicationAPI
apiauth:
Type: AWS::ApiGateway::Authorizer
Properties:
IdentitySource: method.request.header.Authorization
Name: apiauth
ProviderARNs:
- !GetAtt
- apiuserpool
- Arn
RestApiId: !Ref ApplicationAPI
Type: COGNITO_USER_POOLS
DependsOn:
- apiuserpool
ProxyResourceLinkGET:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: GET
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ApplicationAPI
- apiauth
- link
ProxyResourceLinkPOST:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: POST
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceLinkGET
- apiauth
ProxyResourceLinkDELETE:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: DELETE
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceLinkPOST
- apiauth
OptionsMethodLink:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: OPTIONS
Integration:
IntegrationResponses:
-
StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: NEVER
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Origin: true
DependsOn:
- ProxyResourceLinkDELETE
- apiauth
ProxyResourceTagGET:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref tag
HttpMethod: GET
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceLinkPOST
- apiauth
ProxyResourceTagPOST:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref tag
HttpMethod: POST
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagGET
- apiauth
ProxyResourceTagDELETE:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref tag
HttpMethod: DELETE
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagPOST
- apiauth
OptionsMethodTag:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref tag
HttpMethod: OPTIONS
Integration:
IntegrationResponses:
-
StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: NEVER
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Origin: true
DependsOn:
- ProxyResourceTagDELETE
- apiauth
ProxyResourceTagLinkGET:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref taglink
HttpMethod: GET
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagPOST
- apiauth
ProxyResourceTagLinkPOST:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref taglink
HttpMethod: POST
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagLinkGET
- apiauth
ProxyResourceTagLinkDELETE:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref taglink
HttpMethod: DELETE
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ProxyResourceTagLinkPOST
- apiauth
OptionsMethodTagLink:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref apiauth
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref taglink
HttpMethod: OPTIONS
Integration:
IntegrationResponses:
-
StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: NEVER
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Origin: true
DependsOn:
- ProxyResourceTagLinkDELETE
- apiauth
Deployment:
DependsOn: "OptionsMethodTagLink"
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: "ApplicationAPI"
Description: "Application API deployment"
StageName: "prod"
Outputs:
APIBaseUrl:
Description: Application API Gateway base URL
Value: !Join
- ''
- - 'https://'
- !Ref ApplicationAPI
- '.execute-api.'
- !Ref AWS::Region
- '.amazonaws.com/prod'
APILinkUrl:
Description: Application API Gateway link endpoint URL
Value: !Join
- ''
- - 'https://'
- !Ref ApplicationAPI
- '.execute-api.'
- !Ref AWS::Region
- '.amazonaws.com/prod/link'
APITagUrl:
Description: Application API Gateway tag endpoint URL
Value: !Join
- ''
- - 'https://'
- !Ref ApplicationAPI
- '.execute-api.'
- !Ref AWS::Region
- '.amazonaws.com/prod/tag'
APITagLinkUrl:
Description: Application API Gateway taglink endpoint URL
Value: !Join
- ''
- - 'https://'
- !Ref ApplicationAPI
- '.execute-api.'
- !Ref AWS::Region
- '.amazonaws.com/prod/taglink'
UserPoolID:
Description: The Cognito user pool ID
Value: !Ref apiuserpool
APIClientID:
Description: The API user pool client ID
Value: !Ref applicationAPIUserPoolClient
Launch the CFT with this command:
$ aws cloudformation create-stack --stack-name apidbauthdel --template-body file://setupapidbauthdel.yml --capabilities CAPABILITY_NAMED_IAM
Poll for its progress with:
$ aws cloudformation describe-stacks --stack-name apidbauthdel
When complete, make a note of the APIBaseUrl, UserPoolID and APIClientID values. Export them to environment variables with these commands. Be sure the replace the values in angle brackets with the real values:
$ export BASEURL=<APIBaseUrl> $ export POOL_ID=<UserPoolID> $ export CLIENT_ID=<APIClientID>
Step 7: Run the authentication.sh script
Run the authentication.sh script (created in Step 5) to generate an auth token and export it to the environment. Replace ‘EMAIL’ and ‘PASSWORD’ with an email address for the user and and a secure password:
$ source ./authentication.sh EMAIL PASSWORD
Step 8: Run the test.sh script
Run the test.sh script (updated in Step 4) to check that the API is working correctly:
$ ./test.sh $BASEURL
If all goes well, the script should run with no errors other than those expected (as detailed in the output).
Step 9: Clean Up
If you don’t want to keep your API (note that AWS will charge you for it if you don’t have Free Tier on your account), delete the CFT stack (all resources created) by running:
$ aws cloudformation delete-stack --stack-name apidbauthdel
You will then need to remove any logs that were generated by the Lambda function. To do this:
- Log into the AWS Console (your AWS account) using your Administrator IAM user, and navigate to CloudWatch.
- On the left-hand side, select Logs -> Log Groups.
- Select ‘aws/lambda/requestHandler’.
- Under Actions, select ‘Delete log groups’ and confirm.
Conclusion
You should now be able to create a simple API on AWS with a CloudFormation template, with three authenticated endpoints and three DynamoDB tables. In this post, you learned how to add DELETE methods to each endpoint, giving your API full CRUD (Create, Read, Update, Delete) functionality.
If you want to find out how to break up the (rather cumbersome now) CloudFormation Template into smaller nested templates, see my follow-on post, Creating an API with AWS: Part 6: Nested CloudFormation Templates.
Thanks for reading!

2 Responses
[…] Creating an API with AWS: Part 5: DELETE methods January 13, 2022 […]
[…] Creating an API with AWS: Part 5: DELETE methods January 13, 2022 […]