Skip to content

Commit b7c6986

Browse files
chrisoverzerokeetonian
authored andcommitted
feat(gatewayresponses): add support for API Gateway Responses (#841)
1 parent a51ba12 commit b7c6986

File tree

37 files changed

+2861
-11
lines changed

37 files changed

+2861
-11
lines changed

docs/globals.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Currently, the following resources and properties are being supported:
8282
BinaryMediaTypes:
8383
MinimumCompressionSize:
8484
Cors:
85+
GatewayResponses:
8586
AccessLogSetting:
8687
CanarySetting:
8788
TracingEnabled:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
const createResponse = (statusCode, body) => ({ statusCode, body });
3+
4+
exports.get = (event, context, callback) => {
5+
callback(null, createResponse(200, 'You will never see this.'));
6+
};
7+
8+
exports.auth = (event, context, callback) => {
9+
return callback('Unauthorized', null)
10+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
AWSTemplateFormatVersion: "2010-09-09"
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: Simple webservice deomnstrating gateway responses.
4+
5+
Resources:
6+
ExplicitApi:
7+
Type: AWS::Serverless::Api
8+
Properties:
9+
Auth:
10+
Authorizers:
11+
Authorizer:
12+
FunctionArn: !GetAtt AuthorizerFunction.Arn
13+
Identity:
14+
ValidationExpression: "^Bearer +[-0-9a-zA-Z\\._]*$"
15+
ReauthorizeEvery: 300
16+
GatewayResponses:
17+
UNAUTHORIZED:
18+
ResponseParameters:
19+
Headers:
20+
Access-Control-Expose-Headers: "'WWW-Authenticate'"
21+
Access-Control-Allow-Origin: "'*'"
22+
WWW-Authenticate: >-
23+
'Bearer realm="admin"'
24+
GetFunction:
25+
Type: AWS::Serverless::Function
26+
Properties:
27+
Handler: index.get
28+
Runtime: nodejs6.10
29+
CodeUri: src/
30+
Events:
31+
GetResource:
32+
Type: Api
33+
Properties:
34+
Path: /resource/{resourceId}
35+
Method: get
36+
Auth:
37+
Authorizer: Authorizer
38+
RestApiId: !Ref ExplicitApi
39+
AuthorizerFunction:
40+
Type: AWS::Serverless::Function
41+
Properties:
42+
Handler: index.auth
43+
Runtime: nodejs6.10
44+
CodeUri: src/
45+
Outputs:
46+
ApiURL:
47+
Description: "API endpoint URL for Prod environment"
48+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/resource/"

samtranslator/model/api/api_generator.py

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

44
from samtranslator.model.intrinsics import ref
55
from samtranslator.model.apigateway import (ApiGatewayDeployment, ApiGatewayRestApi,
6-
ApiGatewayStage, ApiGatewayAuthorizer)
6+
ApiGatewayStage, ApiGatewayAuthorizer,
7+
ApiGatewayResponse)
78
from samtranslator.model.exceptions import InvalidResourceException
89
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
910
from samtranslator.region_configuration import RegionConfiguration
@@ -21,14 +22,16 @@
2122
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer", "InvokeRole"])
2223
AuthProperties.__new__.__defaults__ = (None, None, None)
2324

25+
GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"]
26+
2427

2528
class ApiGenerator(object):
2629

2730
def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variables, depends_on,
2831
definition_body, definition_uri, name, stage_name, endpoint_configuration=None,
2932
method_settings=None, binary_media=None, minimum_compression_size=None, cors=None,
30-
auth=None, access_log_setting=None, canary_setting=None, tracing_enabled=None,
31-
resource_attributes=None, passthrough_resource_attributes=None):
33+
auth=None, gateway_responses=None, access_log_setting=None, canary_setting=None,
34+
tracing_enabled=None, resource_attributes=None, passthrough_resource_attributes=None):
3235
"""Constructs an API Generator class that generates API Gateway resources
3336
3437
:param logical_id: Logical id of the SAM API Resource
@@ -61,6 +64,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
6164
self.minimum_compression_size = minimum_compression_size
6265
self.cors = cors
6366
self.auth = auth
67+
self.gateway_responses = gateway_responses
6468
self.access_log_setting = access_log_setting
6569
self.canary_setting = canary_setting
6670
self.tracing_enabled = tracing_enabled
@@ -91,6 +95,7 @@ def _construct_rest_api(self):
9195

9296
self._add_cors()
9397
self._add_auth()
98+
self._add_gateway_responses()
9499

95100
if self.definition_uri:
96101
rest_api.BodyS3Location = self._construct_body_s3_dict()
@@ -275,6 +280,49 @@ def _add_auth(self):
275280
# Assign the Swagger back to template
276281
self.definition_body = swagger_editor.swagger
277282

283+
def _add_gateway_responses(self):
284+
"""
285+
Add Gateway Response configuration to the Swagger file, if necessary
286+
"""
287+
288+
if not self.gateway_responses:
289+
return
290+
291+
if self.gateway_responses and not self.definition_body:
292+
raise InvalidResourceException(
293+
self.logical_id, "GatewayResponses works only with inline Swagger specified in "
294+
"'DefinitionBody' property")
295+
296+
# Make sure keys in the dict are recognized
297+
for responses_key, responses_value in self.gateway_responses.items():
298+
for response_key in responses_value.keys():
299+
if response_key not in GatewayResponseProperties:
300+
raise InvalidResourceException(
301+
self.logical_id,
302+
"Invalid property '{}' in 'GatewayResponses' property '{}'".format(response_key, responses_key))
303+
304+
if not SwaggerEditor.is_valid(self.definition_body):
305+
raise InvalidResourceException(
306+
self.logical_id, "Unable to add Auth configuration because "
307+
"'DefinitionBody' does not contain a valid Swagger")
308+
309+
swagger_editor = SwaggerEditor(self.definition_body)
310+
311+
gateway_responses = {}
312+
for response_type, response in self.gateway_responses.items():
313+
gateway_responses[response_type] = ApiGatewayResponse(
314+
api_logical_id=self.logical_id,
315+
response_parameters=response.get('ResponseParameters', {}),
316+
response_templates=response.get('ResponseTemplates', {}),
317+
status_code=response.get('StatusCode', None)
318+
)
319+
320+
if gateway_responses:
321+
swagger_editor.add_gateway_responses(gateway_responses)
322+
323+
# Assign the Swagger back to template
324+
self.definition_body = swagger_editor.swagger
325+
278326
def _get_authorizers(self, authorizers_config, default_authorizer=None):
279327
authorizers = {}
280328
if default_authorizer == 'AWS_IAM':

samtranslator/model/apigateway.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from re import match
2+
13
from samtranslator.model import PropertyType, Resource
24
from samtranslator.model.exceptions import InvalidResourceException
35
from samtranslator.model.types import is_type, one_of, is_str
@@ -93,6 +95,55 @@ def make_auto_deployable(self, stage, swagger=None):
9395
stage.update_deployment_ref(self.logical_id)
9496

9597

98+
class ApiGatewayResponse(object):
99+
ResponseParameterProperties = ["Headers", "Paths", "QueryStrings"]
100+
101+
def __init__(self, api_logical_id=None, response_parameters=None, response_templates=None, status_code=None):
102+
if response_parameters:
103+
for response_parameter_key in response_parameters.keys():
104+
if response_parameter_key not in ApiGatewayResponse.ResponseParameterProperties:
105+
raise InvalidResourceException(
106+
api_logical_id,
107+
"Invalid gateway response parameter '{}'".format(response_parameter_key))
108+
109+
status_code_str = self._status_code_string(status_code)
110+
# status_code must look like a status code, if present. Let's not be judgmental; just check 0-999.
111+
if status_code and not match(r'^[0-9]{1,3}$', status_code_str):
112+
raise InvalidResourceException(api_logical_id, "Property 'StatusCode' must be numeric")
113+
114+
self.api_logical_id = api_logical_id
115+
self.response_parameters = response_parameters or {}
116+
self.response_templates = response_templates or {}
117+
self.status_code = status_code_str
118+
119+
def generate_swagger(self):
120+
swagger = {
121+
"responseParameters": self._add_prefixes(self.response_parameters),
122+
"responseTemplates": self.response_templates
123+
}
124+
125+
# Prevent "null" being written.
126+
if self.status_code:
127+
swagger["statusCode"] = self.status_code
128+
129+
return swagger
130+
131+
def _add_prefixes(self, response_parameters):
132+
GATEWAY_RESPONSE_PREFIX = 'gatewayresponse.'
133+
prefixed_parameters = {}
134+
for key, value in response_parameters.get('Headers', {}).items():
135+
prefixed_parameters[GATEWAY_RESPONSE_PREFIX + 'header.' + key] = value
136+
for key, value in response_parameters.get('Paths', {}).items():
137+
prefixed_parameters[GATEWAY_RESPONSE_PREFIX + 'path.' + key] = value
138+
for key, value in response_parameters.get('QueryStrings', {}).items():
139+
prefixed_parameters[GATEWAY_RESPONSE_PREFIX + 'querystring.' + key] = value
140+
141+
return prefixed_parameters
142+
143+
def _status_code_string(self, status_code):
144+
return None if status_code is None else str(status_code)
145+
146+
96147
class ApiGatewayAuthorizer(object):
97148
_VALID_FUNCTION_PAYLOAD_TYPES = [None, 'TOKEN', 'REQUEST']
98149

samtranslator/model/sam_resources.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" SAM macro definitions """
1+
""" SAM macro definitions """
22
from six import string_types
33

44
import samtranslator.model.eventsources
@@ -442,6 +442,7 @@ class SamApi(SamResourceMacro):
442442
'MinimumCompressionSize': PropertyType(False, is_type(int)),
443443
'Cors': PropertyType(False, one_of(is_str(), is_type(dict))),
444444
'Auth': PropertyType(False, is_type(dict)),
445+
'GatewayResponses': PropertyType(False, is_type(dict)),
445446
'AccessLogSetting': PropertyType(False, is_type(dict)),
446447
'CanarySetting': PropertyType(False, is_type(dict)),
447448
'TracingEnabled': PropertyType(False, is_type(bool))
@@ -477,6 +478,7 @@ def to_cloudformation(self, **kwargs):
477478
minimum_compression_size=self.MinimumCompressionSize,
478479
cors=self.Cors,
479480
auth=self.Auth,
481+
gateway_responses=self.GatewayResponses,
480482
access_log_setting=self.AccessLogSetting,
481483
canary_setting=self.CanarySetting,
482484
tracing_enabled=self.TracingEnabled,

samtranslator/plugins/globals/globals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from samtranslator.public.sdk.resource import SamResourceType
1+
from samtranslator.public.sdk.resource import SamResourceType
22
from samtranslator.public.intrinsics import is_intrinsics
33

44

@@ -49,6 +49,7 @@ class Globals(object):
4949
"BinaryMediaTypes",
5050
"MinimumCompressionSize",
5151
"Cors",
52+
"GatewayResponses",
5253
"AccessLogSetting",
5354
"CanarySetting",
5455
"TracingEnabled"

samtranslator/swagger/swagger.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import copy
1+
import copy
22
from six import string_types
33

44
from samtranslator.model.intrinsics import ref
@@ -16,6 +16,7 @@ class SwaggerEditor(object):
1616
_OPTIONS_METHOD = "options"
1717
_X_APIGW_INTEGRATION = 'x-amazon-apigateway-integration'
1818
_CONDITIONAL_IF = "Fn::If"
19+
_X_APIGW_GATEWAY_RESPONSES = 'x-amazon-apigateway-gateway-responses'
1920
_X_ANY_METHOD = 'x-amazon-apigateway-any-method'
2021

2122
def __init__(self, doc):
@@ -33,6 +34,7 @@ def __init__(self, doc):
3334
self._doc = copy.deepcopy(doc)
3435
self.paths = self._doc["paths"]
3536
self.security_definitions = self._doc.get("securityDefinitions", {})
37+
self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, {})
3638

3739
def get_path(self, path):
3840
path_dict = self.paths.get(path)
@@ -386,8 +388,8 @@ def add_authorizers(self, authorizers):
386388
"""
387389
self.security_definitions = self.security_definitions or {}
388390

389-
for authorizerName, authorizer in authorizers.items():
390-
self.security_definitions[authorizerName] = authorizer.generate_swagger()
391+
for authorizer_name, authorizer in authorizers.items():
392+
self.security_definitions[authorizer_name] = authorizer.generate_swagger()
391393

392394
def set_path_default_authorizer(self, path, default_authorizer, authorizers):
393395
"""
@@ -508,6 +510,17 @@ def set_method_authorizer(self, path, method_name, authorizer_name, authorizers,
508510
elif 'AWS_IAM' not in self.security_definitions:
509511
self.security_definitions.update(aws_iam_security_definition)
510512

513+
def add_gateway_responses(self, gateway_responses):
514+
"""
515+
Add Gateway Response definitions to Swagger.
516+
517+
:param dict gateway_responses: Dictionary of GatewayResponse configuration which gets translated.
518+
"""
519+
self.gateway_responses = self.gateway_responses or {}
520+
521+
for response_type, response in gateway_responses.items():
522+
self.gateway_responses[response_type] = response.generate_swagger()
523+
511524
@property
512525
def swagger(self):
513526
"""
@@ -521,6 +534,8 @@ def swagger(self):
521534

522535
if self.security_definitions:
523536
self._doc["securityDefinitions"] = self.security_definitions
537+
if self.gateway_responses:
538+
self._doc[self._X_APIGW_GATEWAY_RESPONSES] = self.gateway_responses
524539

525540
return copy.deepcopy(self._doc)
526541

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Resources:
2+
Function:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
CodeUri: s3://sam-demo-bucket/member_portal.zip
6+
Handler: index.gethtml
7+
Runtime: nodejs4.3
8+
Events:
9+
GetHtml:
10+
Type: Api
11+
Properties:
12+
Path: /
13+
Method: get
14+
RestApiId: !Ref ExplicitApi
15+
16+
ExplicitApi:
17+
Type: AWS::Serverless::Api
18+
Properties:
19+
StageName: Prod
20+
GatewayResponses:
21+
UNAUTHORIZED:
22+
StatusCode: 401
23+
ResponseParameters:
24+
Headers:
25+
Access-Control-Expose-Headers: "'WWW-Authenticate'"
26+
Access-Control-Allow-Origin: "'*'"
27+
WWW-Authenticate: >-
28+
'Bearer realm="admin"'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Resources:
2+
Function:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
CodeUri: s3://sam-demo-bucket/member_portal.zip
6+
Handler: index.gethtml
7+
Runtime: nodejs4.3
8+
Events:
9+
GetHtml:
10+
Type: Api
11+
Properties:
12+
Path: /
13+
Method: get
14+
RestApiId: !Ref ExplicitApi
15+
16+
ExplicitApi:
17+
Type: AWS::Serverless::Api
18+
Properties:
19+
StageName: Prod
20+
GatewayResponses:
21+
UNAUTHORIZED:
22+
StatusCode: 401
23+
ResponseParameters:
24+
Headers:
25+
Access-Control-Expose-Headers: "'WWW-Authenticate'"
26+
Access-Control-Allow-Origin: "'*'"
27+
WWW-Authenticate: >-
28+
'Bearer realm="admin"'
29+
Paths:
30+
PathKey: "'path-value'"
31+
QueryStrings:
32+
QueryStringKey: "'query-string-value'"
33+
QUOTA_EXCEEDED:
34+
StatusCode: 429
35+
ResponseParameters:
36+
Headers:
37+
Retry-After: "'31536000'"

0 commit comments

Comments
 (0)