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 […]