Skip to content

Commit c5221fc

Browse files
author
Shreya
authored
feat: cors httpapi (#1381)
1 parent 99f5ab7 commit c5221fc

32 files changed

+1180
-32
lines changed

docs/globals.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Currently, the following resources and properties are being supported:
9696
# Properties of AWS::Serverless::HttpApi
9797
# Also works with Implicit APIs
9898
Auth:
99+
CorsConfiguration:
99100
AccessLogSettings:
100101
Tags:
101102
DefaultRouteSettings:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# HttpApi with CORS Example
2+
3+
Example SAM template to configure CORS for HttpApi
4+
5+
## Running the example
6+
7+
```bash
8+
$ sam deploy \
9+
--template-file template.yaml \
10+
--stack-name my-stack-name \
11+
--capabilities CAPABILITY_IAM
12+
```
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
AWSTemplateFormatVersion : '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: Http Api with Cors.
4+
5+
Resources:
6+
HttpApiFunction:
7+
Type: AWS::Serverless::Function
8+
Properties:
9+
InlineCode: |
10+
exports.handler = async (event) => {
11+
console.log("Hello from MyAuthFunction")
12+
return {
13+
statusCode: 200,
14+
body: JSON.stringify(event),
15+
headers: {}
16+
}
17+
}
18+
Handler: index.handler
19+
Runtime: nodejs12.x
20+
Events:
21+
SimpleCase:
22+
Type: HttpApi
23+
Properties:
24+
ApiId: !Ref MyApi
25+
26+
MyApi:
27+
Type: AWS::Serverless::HttpApi
28+
Properties:
29+
CorsConfiguration:
30+
AllowHeaders:
31+
- x-apigateway-header
32+
AllowMethods:
33+
- GET
34+
AllowOrigins:
35+
- https://foo.com
36+
ExposeHeaders:
37+
- x-amzn-header
38+
DefinitionBody:
39+
info:
40+
version: '1.0'
41+
title:
42+
Ref: AWS::StackName
43+
paths:
44+
"$default":
45+
x-amazon-apigateway-any-method:
46+
isDefaultRoute: true
47+
openapi: 3.0.1

samtranslator/model/api/api_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
1919
from samtranslator.region_configuration import RegionConfiguration
2020
from samtranslator.swagger.swagger import SwaggerEditor
21-
from samtranslator.model.intrinsics import is_instrinsic, fnSub
21+
from samtranslator.model.intrinsics import is_intrinsic, fnSub
2222
from samtranslator.model.lambda_ import LambdaPermission
2323
from samtranslator.translator import logical_id_generator
2424
from samtranslator.translator.arn_generator import ArnGenerator
@@ -430,7 +430,7 @@ def _add_cors(self):
430430
self.logical_id, "Cors works only with inline Swagger specified in 'DefinitionBody' property."
431431
)
432432

433-
if isinstance(self.cors, string_types) or is_instrinsic(self.cors):
433+
if isinstance(self.cors, string_types) or is_intrinsic(self.cors):
434434
# Just set Origin property. Others will be defaults
435435
properties = CorsProperties(AllowOrigin=self.cors)
436436
elif isinstance(self.cors, dict):

samtranslator/model/api/http_api_generator.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
from samtranslator.open_api.open_api import OpenApiEditor
88
from samtranslator.translator import logical_id_generator
99
from samtranslator.model.tags.resource_tagging import get_tag_list
10+
from samtranslator.model.intrinsics import is_intrinsic
11+
12+
_CORS_WILDCARD = "*"
13+
CorsProperties = namedtuple(
14+
"_CorsProperties", ["AllowMethods", "AllowHeaders", "AllowOrigins", "MaxAge", "ExposeHeaders", "AllowCredentials"]
15+
)
16+
CorsProperties.__new__.__defaults__ = (None, None, None, None, None, False)
1017

1118
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
1219
AuthProperties.__new__.__defaults__ = (None, None)
@@ -25,6 +32,7 @@ def __init__(
2532
stage_name,
2633
tags=None,
2734
auth=None,
35+
cors_configuration=None,
2836
access_log_settings=None,
2937
default_route_settings=None,
3038
resource_attributes=None,
@@ -53,6 +61,7 @@ def __init__(
5361
if not self.stage_name:
5462
self.stage_name = DefaultStageName
5563
self.auth = auth
64+
self.cors_configuration = cors_configuration
5665
self.tags = tags
5766
self.access_log_settings = access_log_settings
5867
self.default_route_settings = default_route_settings
@@ -69,8 +78,11 @@ def _construct_http_api(self):
6978

7079
if self.definition_uri and self.definition_body:
7180
raise InvalidResourceException(
72-
self.logical_id, "Specify either 'DefinitionUri' or 'DefinitionBody' property and not both"
81+
self.logical_id, "Specify either 'DefinitionUri' or 'DefinitionBody' property and not both."
7382
)
83+
if self.cors_configuration:
84+
# call this method to add cors in open api
85+
self._add_cors()
7486

7587
self._add_auth()
7688
self._add_tags()
@@ -84,11 +96,74 @@ def _construct_http_api(self):
8496
self.logical_id,
8597
"'DefinitionUri' or 'DefinitionBody' are required properties of an "
8698
"'AWS::Serverless::HttpApi'. Add a value for one of these properties or "
87-
"add a 'HttpApi' event to an 'AWS::Serverless::Function'",
99+
"add a 'HttpApi' event to an 'AWS::Serverless::Function'.",
88100
)
89101

90102
return http_api
91103

104+
def _add_cors(self):
105+
"""
106+
Add CORS configuration if CORSConfiguration property is set in SAM.
107+
Adds CORS configuration only if DefinitionBody is present and
108+
APIGW extension for CORS is not present in the DefinitionBody
109+
"""
110+
111+
if self.cors_configuration and not self.definition_body:
112+
raise InvalidResourceException(
113+
self.logical_id, "Cors works only with inline OpenApi specified in 'DefinitionBody' property."
114+
)
115+
116+
# If cors configuration is set to true add * to the allow origins.
117+
# This also support referencing the value as a parameter
118+
if isinstance(self.cors_configuration, bool):
119+
# if cors config is true add Origins as "'*'"
120+
properties = CorsProperties(AllowOrigins=[_CORS_WILDCARD])
121+
122+
elif is_intrinsic(self.cors_configuration):
123+
# Just set Origin property. Intrinsics will be handledOthers will be defaults
124+
properties = CorsProperties(AllowOrigins=self.cors_configuration)
125+
126+
elif isinstance(self.cors_configuration, dict):
127+
# Make sure keys in the dict are recognized
128+
if not all(key in CorsProperties._fields for key in self.cors_configuration.keys()):
129+
raise InvalidResourceException(self.logical_id, "Invalid value for 'Cors' property.")
130+
131+
properties = CorsProperties(**self.cors_configuration)
132+
133+
else:
134+
raise InvalidResourceException(self.logical_id, "Invalid value for 'Cors' property.")
135+
136+
if not OpenApiEditor.is_valid(self.definition_body):
137+
raise InvalidResourceException(
138+
self.logical_id,
139+
"Unable to add Cors configuration because "
140+
"'DefinitionBody' does not contain a valid "
141+
"OpenApi definition.",
142+
)
143+
144+
if properties.AllowCredentials is True and properties.AllowOrigins == [_CORS_WILDCARD]:
145+
raise InvalidResourceException(
146+
self.logical_id,
147+
"Unable to add Cors configuration because "
148+
"'AllowCredentials' can not be true when "
149+
"'AllowOrigin' is \"'*'\" or not set.",
150+
)
151+
152+
editor = OpenApiEditor(self.definition_body)
153+
# if CORS is set in both definition_body and as a CorsConfiguration property,
154+
# SAM merges and overrides the cors headers in definition_body with headers of CorsConfiguration
155+
editor.add_cors(
156+
properties.AllowOrigins,
157+
properties.AllowHeaders,
158+
properties.AllowMethods,
159+
properties.ExposeHeaders,
160+
properties.MaxAge,
161+
properties.AllowCredentials,
162+
)
163+
164+
# Assign the OpenApi back to template
165+
self.definition_body = editor.openapi
166+
92167
def _add_auth(self):
93168
"""
94169
Add Auth configuration to the OAS file, if necessary

samtranslator/model/function_policies.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from six import string_types
55

6-
from samtranslator.model.intrinsics import is_instrinsic, is_intrinsic_if, is_intrinsic_no_value
6+
from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_if, is_intrinsic_no_value
77
from samtranslator.model.exceptions import InvalidTemplateException
88

99
PolicyEntry = namedtuple("PolicyEntry", "data type")
@@ -126,7 +126,7 @@ def _get_type(self, policy):
126126
return self._get_type_from_intrinsic_if(policy)
127127

128128
# Intrinsic functions are treated as managed policies by default
129-
if is_instrinsic(policy):
129+
if is_intrinsic(policy):
130130
return PolicyTypes.MANAGED_POLICY
131131

132132
# Policy statement is a dictionary with the key "Statement" in it

samtranslator/model/intrinsics.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def make_shorthand(intrinsic_dict):
129129
raise NotImplementedError("Shorthanding is only supported for Ref and Fn::GetAtt")
130130

131131

132-
def is_instrinsic(input):
132+
def is_intrinsic(input):
133133
"""
134134
Checks if the given input is an intrinsic function dictionary. Intrinsic function is a dictionary with single
135135
key that is the name of the intrinsics.
@@ -155,7 +155,7 @@ def is_intrinsic_if(input):
155155
:return: True, if yes
156156
"""
157157

158-
if not is_instrinsic(input):
158+
if not is_intrinsic(input):
159159
return False
160160

161161
key = list(input.keys())[0]
@@ -171,7 +171,7 @@ def is_intrinsic_no_value(input):
171171
:return: True, if yes
172172
"""
173173

174-
if not is_instrinsic(input):
174+
if not is_intrinsic(input):
175175
return False
176176

177177
key = list(input.keys())[0]

samtranslator/model/preferences/deployment_preference_collection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from samtranslator.model.codedeploy import CodeDeployApplication
33
from samtranslator.model.codedeploy import CodeDeployDeploymentGroup
44
from samtranslator.model.iam import IAMRole
5-
from samtranslator.model.intrinsics import fnSub, is_instrinsic
5+
from samtranslator.model.intrinsics import fnSub, is_intrinsic
66
from samtranslator.model.update_policy import UpdatePolicy
77
from samtranslator.translator.arn_generator import ArnGenerator
88
import copy
@@ -149,7 +149,7 @@ def _replace_deployment_types(self, value, key=None):
149149
for i in range(len(value)):
150150
value[i] = self._replace_deployment_types(value[i])
151151
return value
152-
elif is_instrinsic(value):
152+
elif is_intrinsic(value):
153153
for (k, v) in value.items():
154154
value[k] = self._replace_deployment_types(v, k)
155155
return value

samtranslator/model/sam_resources.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,7 @@ class SamHttpApi(SamResourceMacro):
872872
"DefinitionBody": PropertyType(False, is_type(dict)),
873873
"DefinitionUri": PropertyType(False, one_of(is_str(), is_type(dict))),
874874
"StageVariables": PropertyType(False, is_type(dict)),
875-
"Cors": PropertyType(False, one_of(is_str(), is_type(dict))),
875+
"CorsConfiguration": PropertyType(False, one_of(is_type(bool), is_type(dict))),
876876
"AccessLogSettings": PropertyType(False, is_type(dict)),
877877
"DefaultRouteSettings": PropertyType(False, is_type(dict)),
878878
"Auth": PropertyType(False, is_type(dict)),
@@ -881,14 +881,16 @@ class SamHttpApi(SamResourceMacro):
881881
referable_properties = {"Stage": ApiGatewayV2Stage.resource_type}
882882

883883
def to_cloudformation(self, **kwargs):
884-
"""Returns the API Gateway RestApi, Deployment, and Stage to which this SAM Api corresponds.
884+
"""Returns the API GatewayV2 Api, Deployment, and Stage to which this SAM Api corresponds.
885885
886886
:param dict kwargs: already-converted resources that may need to be modified when converting this \
887887
macro to pure CloudFormation
888888
:returns: a list of vanilla CloudFormation Resources, to which this Function expands
889889
:rtype: list
890890
"""
891891
resources = []
892+
intrinsics_resolver = kwargs["intrinsics_resolver"]
893+
self.CorsConfiguration = intrinsics_resolver.resolve_parameter_refs(self.CorsConfiguration)
892894

893895
api_generator = HttpApiGenerator(
894896
self.logical_id,
@@ -899,6 +901,7 @@ def to_cloudformation(self, **kwargs):
899901
self.StageName,
900902
tags=self.Tags,
901903
auth=self.Auth,
904+
cors_configuration=self.CorsConfiguration,
902905
access_log_settings=self.AccessLogSettings,
903906
default_route_settings=self.DefaultRouteSettings,
904907
resource_attributes=self.resource_attributes,

samtranslator/open_api/open_api.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
from samtranslator.model.intrinsics import ref
66
from samtranslator.model.intrinsics import make_conditional
7+
from samtranslator.model.intrinsics import is_intrinsic
78
from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException
9+
import json
810

911

1012
class OpenApiEditor(object):
@@ -17,6 +19,7 @@ class OpenApiEditor(object):
1719

1820
_X_APIGW_INTEGRATION = "x-amazon-apigateway-integration"
1921
_X_APIGW_TAG_VALUE = "x-amazon-apigateway-tag-value"
22+
_X_APIGW_CORS = "x-amazon-apigateway-cors"
2023
_CONDITIONAL_IF = "Fn::If"
2124
_X_ANY_METHOD = "x-amazon-apigateway-any-method"
2225
_ALL_HTTP_METHODS = ["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"]
@@ -376,6 +379,74 @@ def add_tags(self, tags):
376379
tag = {"name": name, self._X_APIGW_TAG_VALUE: value}
377380
self.tags.append(tag)
378381

382+
def add_cors(
383+
self,
384+
allow_origins,
385+
allow_headers=None,
386+
allow_methods=None,
387+
expose_headers=None,
388+
max_age=None,
389+
allow_credentials=None,
390+
):
391+
"""
392+
Add CORS configuration to this Api to _X_APIGW_CORS header in open api definition
393+
394+
Following this guide:
395+
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html
396+
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-api-cors.html
397+
398+
:param list/dict allowed_origins: Comma separate list of allowed origins.
399+
Value can also be an intrinsic function dict.
400+
:param list/dict allowed_headers: Comma separated list of allowed headers.
401+
Value can also be an intrinsic function dict.
402+
:param list/dict allowed_methods: Comma separated list of allowed methods.
403+
Value can also be an intrinsic function dict.
404+
:param list/dict expose_headers: Comma separated list of allowed methods.
405+
Value can also be an intrinsic function dict.
406+
:param integer/dict max_age: Maximum duration to cache the CORS Preflight request. Value is set on
407+
Access-Control-Max-Age header. Value can also be an intrinsic function dict.
408+
:param bool/None allowed_credentials: Flags whether request is allowed to contain credentials.
409+
"""
410+
ALLOW_ORIGINS = "allowOrigins"
411+
ALLOW_HEADERS = "allowHeaders"
412+
ALLOW_METHODS = "allowMethods"
413+
EXPOSE_HEADERS = "exposeHeaders"
414+
MAX_AGE = "maxAge"
415+
ALLOW_CREDENTIALS = "allowCredentials"
416+
cors_headers = [ALLOW_ORIGINS, ALLOW_HEADERS, ALLOW_METHODS, EXPOSE_HEADERS, MAX_AGE, ALLOW_CREDENTIALS]
417+
cors_configuration = self._doc.get(self._X_APIGW_CORS, dict())
418+
419+
# intrinsics will not work if cors configuration is defined in open api and as a property to the HttpApi
420+
if allow_origins and is_intrinsic(allow_origins):
421+
cors_configuration_string = json.dumps(allow_origins)
422+
for header in cors_headers:
423+
# example: allowOrigins to AllowOrigins
424+
keyword = header[0].upper() + header[1:]
425+
cors_configuration_string = cors_configuration_string.replace(keyword, header)
426+
cors_configuration_dict = json.loads(cors_configuration_string)
427+
cors_configuration.update(cors_configuration_dict)
428+
429+
else:
430+
if allow_origins:
431+
cors_configuration[ALLOW_ORIGINS] = allow_origins
432+
if allow_headers:
433+
cors_configuration[ALLOW_HEADERS] = allow_headers
434+
if allow_methods:
435+
cors_configuration[ALLOW_METHODS] = allow_methods
436+
if expose_headers:
437+
cors_configuration[EXPOSE_HEADERS] = expose_headers
438+
if max_age is not None:
439+
cors_configuration[MAX_AGE] = max_age
440+
if allow_credentials is True:
441+
cors_configuration[ALLOW_CREDENTIALS] = allow_credentials
442+
443+
self._doc[self._X_APIGW_CORS] = cors_configuration
444+
445+
def has_api_gateway_cors(self):
446+
if self._doc.get(self._X_APIGW_CORS):
447+
return True
448+
return False
449+
379450
@property
380451
def openapi(self):
381452
"""

0 commit comments

Comments
 (0)