Skip to content

Commit 503737a

Browse files
RondineliRondineli Gomes de AraujoRondineliRondineli
authored
feat: Add ValidateBody/ValidateParameters attribute to API models (#2026)
* Added validators to the swagger definitions when a model is required * Remove approach of trying an extra field for validators * WIP - tests deps not working * Finished and fixed all tests to make pr command was complaining. Fixed case where a validator could have a / only and changed it to translate as 'root' * py2.7 complaint * Fix py2.7 order of validators on output folder * WIP - adding validator fields * Set another strategy to set the validators without affect existent clients with model required * Set another strategy to set the validators without affect existent clients with model required * Remove mistaken file pushed * Remove mistaken file pushed * Partial coments fixes * Implemented case where we can set a only limited combinations on the specs * Fix case test when multiple path are using same validator * Added openapi3 specific tests * Added openapi3 specific tests * Fix requested changes from review: Renamed from ValidateRequest -> ValidateParameters also renamed everything from validator_request. Moved validators names according with the comum use. * new approach to not cause KeyErrors * Setting validator to the only given method not all methods on the path * removed extra space on comment - lint * Normalized method name path that comes from the template Co-authored-by: Rondineli Gomes de Araujo <[email protected]> Co-authored-by: Rondineli <[email protected]> Co-authored-by: Rondineli <[email protected]>
1 parent 09677fc commit 503737a

16 files changed

+3509
-0
lines changed

samtranslator/model/eventsources/push.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,35 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver):
788788
path=self.Path, method_name=self.Method, request_model=self.RequestModel
789789
)
790790

791+
validate_body = self.RequestModel.get("ValidateBody")
792+
validate_parameters = self.RequestModel.get("ValidateParameters")
793+
794+
# Checking if any of the fields are defined as it can be false we are checking if the field are not None
795+
if validate_body is not None or validate_parameters is not None:
796+
797+
# as we are setting two different fields we are here setting as default False
798+
# In case one of them are not defined
799+
validate_body = False if validate_body is None else validate_body
800+
validate_parameters = False if validate_parameters is None else validate_parameters
801+
802+
# If not type None but any other type it should explicitly invalidate the Spec
803+
# Those fields should be only a boolean
804+
if not isinstance(validate_body, bool) or not isinstance(validate_parameters, bool):
805+
raise InvalidEventException(
806+
self.relative_id,
807+
"Unable to set Validator to RequestModel [{model}] on API method [{method}] for path [{path}] "
808+
"ValidateBody and ValidateParameters must be a boolean type, strings or intrinsics are not supported.".format(
809+
model=method_model, method=self.Method, path=self.Path
810+
),
811+
)
812+
813+
editor.add_request_validator_to_method(
814+
path=self.Path,
815+
method_name=self.Method,
816+
validate_body=validate_body,
817+
validate_parameters=validate_parameters,
818+
)
819+
791820
if self.RequestParameters:
792821

793822
default_value = {"Required": False, "Caching": False}

samtranslator/swagger/swagger.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class SwaggerEditor(object):
2323
_X_APIGW_GATEWAY_RESPONSES = "x-amazon-apigateway-gateway-responses"
2424
_X_APIGW_POLICY = "x-amazon-apigateway-policy"
2525
_X_ANY_METHOD = "x-amazon-apigateway-any-method"
26+
_X_APIGW_REQUEST_VALIDATORS = "x-amazon-apigateway-request-validators"
27+
_X_APIGW_REQUEST_VALIDATOR = "x-amazon-apigateway-request-validator"
2628
_CACHE_KEY_PARAMETERS = "cacheKeyParameters"
2729
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
2830
_ALL_HTTP_METHODS = ["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"]
@@ -781,6 +783,46 @@ def _set_method_apikey_handling(self, path, method_name, apikey_required):
781783
if security != existing_security:
782784
method_definition["security"] = security
783785

786+
def add_request_validator_to_method(self, path, method_name, validate_body=False, validate_parameters=False):
787+
"""
788+
Adds request model body parameter for this path/method.
789+
790+
:param string path: Path name
791+
:param string method_name: Method name
792+
:param bool validate_body: Add validator parameter on the body
793+
:param bool validate_parameters: Validate request
794+
"""
795+
796+
normalized_method_name = self._normalize_method_name(method_name)
797+
validator_name = SwaggerEditor.get_validator_name(validate_body, validate_parameters)
798+
799+
# Creating validator
800+
request_validator_definition = {
801+
validator_name: {"validateRequestBody": validate_body, "validateRequestParameters": validate_parameters}
802+
}
803+
if not self._doc.get(self._X_APIGW_REQUEST_VALIDATORS):
804+
self._doc[self._X_APIGW_REQUEST_VALIDATORS] = {}
805+
806+
if not self._doc[self._X_APIGW_REQUEST_VALIDATORS].get(validator_name):
807+
# Adding only if the validator hasn't been defined already
808+
self._doc[self._X_APIGW_REQUEST_VALIDATORS].update(request_validator_definition)
809+
810+
# It is possible that the method could have two definitions in a Fn::If block.
811+
for path_method_name, method in self.get_path(path).items():
812+
normalized_path_method_name = self._normalize_method_name(path_method_name)
813+
814+
# Adding it to only given method to the path
815+
if normalized_path_method_name == normalized_method_name:
816+
for method_definition in self.get_method_contents(method):
817+
818+
# If no integration given, then we don't need to process this definition (could be AWS::NoValue)
819+
if not self.method_definition_has_integration(method_definition):
820+
continue
821+
822+
set_validator_to_method = {self._X_APIGW_REQUEST_VALIDATOR: validator_name}
823+
# Setting validator to the given method
824+
method_definition.update(set_validator_to_method)
825+
784826
def add_request_model_to_method(self, path, method_name, request_model):
785827
"""
786828
Adds request model body parameter for this path/method.
@@ -1265,6 +1307,26 @@ def get_path_without_trailing_slash(path):
12651307
# convert greedy paths to such as {greedy+}, {proxy+} to "*"
12661308
return re.sub(r"{([a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+\+|proxy\+)}", "*", path)
12671309

1310+
@staticmethod
1311+
def get_validator_name(validate_body, validate_parameters):
1312+
"""
1313+
Get a readable path name to use as validator name
1314+
1315+
:param boolean validate_body: Boolean if validate body
1316+
:param boolean validate_request: Boolean if validate request
1317+
:return string: Normalized validator name
1318+
"""
1319+
if validate_body and validate_parameters:
1320+
return "body-and-params"
1321+
1322+
if validate_body and not validate_parameters:
1323+
return "body-only"
1324+
1325+
if not validate_body and validate_parameters:
1326+
return "params-only"
1327+
1328+
return "no-validation"
1329+
12681330
@staticmethod
12691331
def _validate_list_property_is_resolved(property_list):
12701332
"""

tests/swagger/test_swagger.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,118 @@ def test_must_add_body_parameter_to_method_openapi_required_true(self):
831831
self.assertEqual(expected, editor.swagger["paths"]["/foo"]["get"]["requestBody"])
832832

833833

834+
class TestSwaggerEditor_add_request_validator_to_method(TestCase):
835+
def setUp(self):
836+
837+
self.original_swagger = {
838+
"swagger": "2.0",
839+
"paths": {
840+
"/foo": {
841+
"get": {
842+
"x-amazon-apigateway-integration": {"test": "must have integration"},
843+
"parameters": [{"test": "existing parameter"}],
844+
}
845+
}
846+
},
847+
}
848+
849+
self.editor = SwaggerEditor(self.original_swagger)
850+
851+
def test_must_add_validator_parameters_to_method_with_validators_true(self):
852+
853+
self.editor.add_request_validator_to_method("/foo", "get", True, True)
854+
expected = {"body-and-params": {"validateRequestBody": True, "validateRequestParameters": True}}
855+
856+
self.assertEqual(expected, self.editor.swagger["x-amazon-apigateway-request-validators"])
857+
self.assertEqual(
858+
"body-and-params", self.editor.swagger["paths"]["/foo"]["get"]["x-amazon-apigateway-request-validator"]
859+
)
860+
861+
def test_must_add_validator_parameters_to_method_with_validators_false(self):
862+
863+
self.editor.add_request_validator_to_method("/foo", "get", False, False)
864+
865+
expected = {"no-validation": {"validateRequestBody": False, "validateRequestParameters": False}}
866+
867+
self.assertEqual(expected, self.editor.swagger["x-amazon-apigateway-request-validators"])
868+
self.assertEqual(
869+
"no-validation", self.editor.swagger["paths"]["/foo"]["get"]["x-amazon-apigateway-request-validator"]
870+
)
871+
872+
def test_must_add_validator_parameters_to_method_with_validators_mixing(self):
873+
874+
self.editor.add_request_validator_to_method("/foo", "get", True, False)
875+
876+
expected = {"body-only": {"validateRequestBody": True, "validateRequestParameters": False}}
877+
878+
self.assertEqual(expected, self.editor.swagger["x-amazon-apigateway-request-validators"])
879+
self.assertEqual(
880+
"body-only", self.editor.swagger["paths"]["/foo"]["get"]["x-amazon-apigateway-request-validator"]
881+
)
882+
883+
def test_must_add_validator_parameters_to_method_and_not_duplicate(self):
884+
self.original_swagger["paths"].update(
885+
{
886+
"/bar": {
887+
"get": {
888+
"x-amazon-apigateway-integration": {"test": "must have integration"},
889+
"parameters": [{"test": "existing parameter"}],
890+
}
891+
},
892+
"/foo-bar": {
893+
"get": {
894+
"x-amazon-apigateway-integration": {"test": "must have integration"},
895+
"parameters": [{"test": "existing parameter"}],
896+
}
897+
},
898+
}
899+
)
900+
901+
editor = SwaggerEditor(self.original_swagger)
902+
903+
editor.add_request_validator_to_method("/foo", "get", True, True)
904+
editor.add_request_validator_to_method("/bar", "get", True, True)
905+
editor.add_request_validator_to_method("/foo-bar", "get", True, True)
906+
907+
expected = {"body-and-params": {"validateRequestBody": True, "validateRequestParameters": True}}
908+
909+
self.assertEqual(expected, editor.swagger["x-amazon-apigateway-request-validators"])
910+
self.assertEqual(
911+
"body-and-params", editor.swagger["paths"]["/foo"]["get"]["x-amazon-apigateway-request-validator"]
912+
)
913+
self.assertEqual(
914+
"body-and-params", editor.swagger["paths"]["/bar"]["get"]["x-amazon-apigateway-request-validator"]
915+
)
916+
self.assertEqual(
917+
"body-and-params", editor.swagger["paths"]["/foo-bar"]["get"]["x-amazon-apigateway-request-validator"]
918+
)
919+
920+
self.assertEqual(1, len(editor.swagger["x-amazon-apigateway-request-validators"].keys()))
921+
922+
@parameterized.expand(
923+
[
924+
param(True, False, "body-only"),
925+
param(True, True, "body-and-params"),
926+
param(False, True, "params-only"),
927+
param(False, False, "no-validation"),
928+
]
929+
)
930+
def test_must_return_validator_names(self, validate_body, validate_request, normalized_name):
931+
normalized_validator_name_conversion = SwaggerEditor.get_validator_name(validate_body, validate_request)
932+
self.assertEqual(normalized_validator_name_conversion, normalized_name)
933+
934+
def test_must_add_validator_parameters_to_method_with_validators_false_by_default(self):
935+
936+
self.editor.add_request_validator_to_method("/foo", "get")
937+
938+
expected = {"no-validation": {"validateRequestBody": False, "validateRequestParameters": False}}
939+
940+
self.assertEqual(expected, self.editor.swagger["x-amazon-apigateway-request-validators"])
941+
self.assertEqual(
942+
"no-validation", self.editor.swagger["paths"]["/foo"]["get"]["x-amazon-apigateway-request-validator"]
943+
)
944+
945+
834946
class TestSwaggerEditor_add_auth(TestCase):
835947
def setUp(self):
836948

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
Resources:
2+
HtmlFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
CodeUri: s3://sam-demo-bucket/member_portal.zip
6+
Handler: index.gethtml
7+
Runtime: nodejs12.x
8+
Events:
9+
GetHtml:
10+
Type: Api
11+
Properties:
12+
RestApiId: HtmlApi
13+
Path: /
14+
Method: get
15+
RequestModel:
16+
Model: User
17+
Required: true
18+
ValidateBody: true
19+
ValidateParameters: true
20+
21+
HtmlFunctionNoValidation:
22+
Type: AWS::Serverless::Function
23+
Properties:
24+
CodeUri: s3://sam-demo-bucket/member_portal.zip
25+
Handler: index.gethtml
26+
Runtime: nodejs12.x
27+
Events:
28+
GetHtml:
29+
Type: Api
30+
Properties:
31+
RestApiId: HtmlApi
32+
Path: /no-validation
33+
Method: get
34+
RequestModel:
35+
Model: User
36+
Required: true
37+
ValidateBody: false
38+
ValidateParameters: false
39+
40+
HtmlFunctionMixinValidation:
41+
Type: AWS::Serverless::Function
42+
Properties:
43+
CodeUri: s3://sam-demo-bucket/member_portal.zip
44+
Handler: index.gethtml
45+
Runtime: nodejs12.x
46+
Events:
47+
GetHtml:
48+
Type: Api
49+
Properties:
50+
RestApiId: HtmlApi
51+
Path: /mixin
52+
Method: get
53+
RequestModel:
54+
Model: User
55+
Required: true
56+
ValidateBody: true
57+
ValidateParameters: false
58+
59+
HtmlFunctionOnlyBodyDefinition:
60+
Type: AWS::Serverless::Function
61+
Properties:
62+
CodeUri: s3://sam-demo-bucket/member_portal.zip
63+
Handler: index.gethtml
64+
Runtime: nodejs12.x
65+
Events:
66+
GetHtml:
67+
Type: Api
68+
Properties:
69+
RestApiId: HtmlApi
70+
Path: /only-body-true
71+
Method: get
72+
RequestModel:
73+
Model: User
74+
Required: true
75+
ValidateBody: true
76+
77+
HtmlFunctionOnlyRequestDefinition:
78+
Type: AWS::Serverless::Function
79+
Properties:
80+
CodeUri: s3://sam-demo-bucket/member_portal.zip
81+
Handler: index.gethtml
82+
Runtime: nodejs12.x
83+
Events:
84+
GetHtml:
85+
Type: Api
86+
Properties:
87+
RestApiId: HtmlApi
88+
Path: /only-request-true
89+
Method: get
90+
RequestModel:
91+
Model: User
92+
Required: true
93+
ValidateParameters: true
94+
95+
HtmlFunctionOnlyRequestDefinitionFalse:
96+
Type: AWS::Serverless::Function
97+
Properties:
98+
CodeUri: s3://sam-demo-bucket/member_portal.zip
99+
Handler: index.gethtml
100+
Runtime: nodejs12.x
101+
Events:
102+
GetHtml:
103+
Type: Api
104+
Properties:
105+
RestApiId: HtmlApi
106+
Path: /only-request-false
107+
Method: get
108+
RequestModel:
109+
Model: User
110+
Required: true
111+
ValidateParameters: false
112+
113+
HtmlFunctionOnlyBodyDefinitionFalse:
114+
Type: AWS::Serverless::Function
115+
Properties:
116+
CodeUri: s3://sam-demo-bucket/member_portal.zip
117+
Handler: index.gethtml
118+
Runtime: nodejs12.x
119+
Events:
120+
GetHtml:
121+
Type: Api
122+
Properties:
123+
RestApiId: HtmlApi
124+
Path: /only-body-false
125+
Method: get
126+
RequestModel:
127+
Model: User
128+
Required: true
129+
ValidateBody: false
130+
131+
HtmlFunctionNotDefinedValidation:
132+
Type: AWS::Serverless::Function
133+
Properties:
134+
CodeUri: s3://sam-demo-bucket/member_portal.zip
135+
Handler: index.gethtml
136+
Runtime: nodejs12.x
137+
Events:
138+
GetHtml:
139+
Type: Api
140+
Properties:
141+
RestApiId: HtmlApi
142+
Path: /not-defined
143+
Method: get
144+
RequestModel:
145+
Model: User
146+
Required: true
147+
148+
HtmlApi:
149+
Type: AWS::Serverless::Api
150+
Properties:
151+
StageName: Prod
152+
Models:
153+
User:
154+
type: object
155+
properties:
156+
username:
157+
type: string

0 commit comments

Comments
 (0)