8000 fix validator and model check during invocation (#7846) · codeperl/localstack@92dca7d · GitHub
[go: up one dir, main page]

Skip to content

Commit 92dca7d

Browse files
authored
fix validator and model check during invocation (localstack#7846)
1 parent 1dfc637 commit 92dca7d

File tree

7 files changed

+455
-15
lines changed

7 files changed

+455
-15
lines changed

localstack/services/apigateway/helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
# special tag name to allow specifying a custom ID for new REST APIs
6969
TAG_KEY_CUSTOM_ID = "_custom_id_"
7070

71+
EMPTY_MODEL = "Empty"
72+
7173
# TODO: make the CRUD operations in this file generic for the different model types (authorizes, validators, ...)
7274

7375

localstack/services/apigateway/invocations.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from localstack.services.apigateway import helpers
1111
from localstack.services.apigateway.context import ApiInvocationContext
1212
from localstack.services.apigateway.helpers import (
13+
EMPTY_MODEL,
1314
extract_path_params,
1415
extract_query_string_params,
1516
get_cors_response,
@@ -58,11 +59,15 @@ def is_request_valid(self) -> bool:
5859
return True
5960

6061
# check if there is a validator for this request
61-
validator = self.apigateway_client.get_request_validator(
62-
restApiId=self.context.api_id, requestValidatorId=resource["requestValidatorId"]
63-
)
64-
if validator is None:
65-
return True
62+
try:
63+
validator = self.apigateway_client.get_request_validator(
64+
restApiId=self.context.api_id, requestValidatorId=resource["requestValidatorId"]
65+
)
66+
except ClientError as e:
67+
if "NotFoundException" in e:
68+
return True
69+
70+
raise
6671

6772
# are we validating the body?
6873
if self.should_validate_body(validator):
@@ -78,11 +83,13 @@ def is_request_valid(self) -> bool:
7883
return True
7984

8085
def validate_body(self, resource):
81-
# we need a model to validate the body
82-
if "requestModels" not in resource or not resource["requestModels"]:
83-
return False
86+
# if there's no model to validate the body, use the Empty model
87+
# https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-apigateway.EmptyModel.html
88+
if not (request_models := resource.get("requestModels")):
89+
schema_name = EMPTY_MODEL
90+
else:
91+
schema_name = request_models.get(APPLICATION_JSON, EMPTY_MODEL)
8492

85-
schema_name = resource["requestModels"].get(APPLICATION_JSON)
8693
try:
8794
model = self.apigateway_client.get_model(
8895
restApiId=self.context.api_id,
@@ -96,7 +103,11 @@ def validate_body(self, resource):
96103
validate(instance=json.loads(self.context.data), schema=json.loads(model["schema"]))
97104
return True
98105
except ValidationError as e:
99-
LOG.warning("failed to validate request body", e)
106+
LOG.warning("failed to validate request body %s", e)
107+
return False
108+
except json.JSONDecodeError as e:
109+
# TODO: for now, it could also be the loading of the schema failing but it will be validated at some point
110+
LOG.warning("failed to validate request body, request data is not valid JSON %s", e)
100111
return False
101112

102113
# TODO implement parameters and headers

localstack/services/apigateway/provider.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from localstack.aws.forwarder import NotImplementedAvoidFallbackError, create_aws_request_context
6262
from localstack.constants import APPLICATION_JSON
6363
from localstack.services.apigateway.helpers import (
64+
EMPTY_MODEL,
6465
OpenApiExporter,
6566
apply_json_patch_safe,
6667
get_apigateway_store,
@@ -137,7 +138,7 @@ def create_rest_api(self, context: RequestContext, request: CreateRestApiRequest
137138
rest_api_container = RestApiContainer(rest_api=response)
138139
store.rest_apis[result["id"]] = rest_api_container
139140
# add the 2 default models
140-
rest_api_container.models["Empty"] = DEFAULT_EMPTY_MODEL
141+
rest_api_container.models[EMPTY_MODEL] = DEFAULT_EMPTY_MODEL
141142
rest_api_container.models["Error"] = DEFAULT_ERROR_MODEL
142143

143144
return response
@@ -442,7 +443,7 @@ def put_method(
442443
if request_models:
443444
for content_type, model_name in request_models.items():
444445
# FIXME: add Empty model to rest api at creation
445-
if model_name == "Empty":
446+
if model_name == EMPTY_MODEL:
446447
continue
447448
if model_name not in rest_api_container.models:
448449
raise BadRequestException(f"Invalid model identifier specified: {model_name}")
@@ -513,10 +514,15 @@ def update_method(
513514
continue
514515

515516
elif path == "/requestValidatorId" and value not in rest_api.validators:
517+
if not value:
518+
# you can remove a requestValidator by passing an empty string as a value
519+
patch_op = {"op": "remove", "path": path, "value": value}
520+
applicable_patch_operations.append(patch_op)
521+
continue
516522
raise BadRequestException("Invalid Request Validator identifier specified")
517523

518524
elif path.startswith("/requestModels/"):
519-
if value != "Empty" and value not in rest_api.models:
525+
if value != EMPTY_MODEL and value not in rest_api.models:
520526
raise BadRequestException(f"Invalid model identifier specified: {value}")
521527

522528
applicable_patch_operations.append(patch_operation)
@@ -1499,7 +1505,7 @@ def to_response_json(model_type, data, api_id=None, self_link=None, id_attr=None
14991505

15001506
DEFAULT_EMPTY_MODEL = Model(
15011507
id=short_uid()[:6],
1502-
name="Empty",
1508+
name=EMPTY_MODEL,
15031509
contentType="application/json",
15041510
description="This is a default empty schema model",
15051511
schema=json.dumps(

tests/integration/apigateway/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,17 @@ def _factory(
139139
return api_id
140140

141141
yield _factory
142+
143+
144+
@pytest.fixture
145+
def apigw_redeploy_api(apigateway_client):
146+
def _factory(rest_api_id: str, stage_name: str):
147+
deployment_id = apigateway_client.create_deployment(restApiId=rest_api_id)["id"]
148+
149+
apigateway_client.update_stage(
150+
restApiId=rest_api_id,
151+
stageName=stage_name,
152+
patchOperations=[{"op": "replace", "path": "/deploymentId", "value": deployment_id}],
153+
)
154+
155+
return _factory
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import json
2+
3+
import pytest
4+
import requests
5+
6+
from localstack.services.awslambda.lambda_utils import LAMBDA_RUNTIME_PYTHON39
7+
from localstack.utils.aws.arns import parse_arn
8+
from localstack.utils.strings import short_uid
9+
from tests.integration.apigateway.apigateway_fixtures import api_invoke_url
10+
from tests.integration.awslambda.test_lambda import TEST_LAMBDA_AWS_PROXY
11+
12+
13+
class TestApiGatewayCommon:
14+
"""
15+
In this class we won't test individual CRUD API calls but how those will affect the integrations and
16+
requests/responses from the API.
17+
"""
18+
19+
@pytest.mark.aws_validated
20+
@pytest.mark.skip_snapshot_verify(
21+
paths=[
22+
"$.invalid-request-body.Type",
23+
"$..methodIntegration.cacheNamespace",
24+
"$..methodIntegration.integrationResponses",
25+
"$..methodIntegration.passthroughBehavior",
26+
"$..methodIntegration.requestParameters",
27+
"$..methodIntegration.timeoutInMillis",
28+
]
29+
)
30+
def test_api_gateway_request_validator(
31+
self,
32+
apigateway_client,
33+
create_lambda_function,
34+
create_rest_apigw,
35+
apigw_redeploy_api,
36+
lambda_client,
37+
snapshot,
38+
):
39+
# TODO: create fixture which will provide basic integrations where we can test behaviour
40+
# see once we have more cases how we can regroup functionality into one or several fixtures
41+
# example: create a basic echo lambda + integrations + deploy stage
42+
snapshot.add_transformers_list(
43+
[
44+
snapshot.transform.key_value("requestValidatorId"),
45+
snapshot.transform.key_value("id"), # deployment id
46+
snapshot.transform.key_value("fn_name"), # lambda name
47+
snapshot.transform.key_value("fn_arn"), # lambda arn
48+
]
49+
)
50+
51+
fn_name = f"test-{short_uid()}"
52+
create_lambda_function(
53+
func_name=fn_name,
54+
handler_file=TEST_LAMBDA_AWS_PROXY,
55+
runtime=LAMBDA_RUNTIME_PYTHON39,
56+
)
57+
lambda_arn = lambda_client.get_function(FunctionName=fn_name)["Configuration"][
58+
"FunctionArn"
59+
]
60+
# matching on lambda id for reference replacement in snapshots
61+
snapshot.match("register-lambda", {"fn_name": fn_name, "fn_arn": lambda_arn})
62+
63+
parsed_arn = parse_arn(lambda_arn)
64+
region = parsed_arn["region"]
65+
account_id = parsed_arn["account"]
66+
67+
api_id, _, root = create_rest_apigw(name="aws lambda api")
68+
69+
resource_1 = apigateway_client.create_resource(
70+
restApiId=api_id, parentId=root, pathPart="test"
71+
)["id"]
72+
73+
resource_id = apigateway_client.create_resource(
74+
restApiId=api_id, parentId=resource_1, pathPart="{test}"
75+
)["id"]
76+
77+
validator_id = apigateway_client.create_request_validator(
78+
restApiId=api_id,
79+
name="test-validator",
80+
validateRequestParameters=True,
81+
validateRequestBody=True,
82+
)["id"]
83+
84+
apigateway_client.put_method(
85+
restApiId=api_id,
86+
resourceId=resource_id,
87+
httpMethod="POST",
88+
authorizationType="NONE",
89+
requestValidatorId=validator_id,
90+
requestParameters={"method.request.path.test": True},
91+
)
92+
93+
apigateway_client.put_integration(
94+
restApiId=api_id,
95+
resourceId=resource_id,
96+
httpMethod="POST",
97+
integrationHttpMethod="POST",
98+
type="AWS_PROXY",
99+
uri=f"arn:aws:apigateway:{region}:lambda:path//2015-03-31/functions/"
100+
f"{lambda_arn}/invocations",
101+
)
102+
apigateway_client.put_method_response(
103+
restApiId=api_id,
104+
resourceId=resource_id,
105+
httpMethod="POST",
106+
statusCode="200",
107+
)
108+
apigateway_client.put_integration_response(
109+
restApiId=api_id,
110+
resourceId=resource_id,
111+
httpMethod="POST",
112+
statusCode="200",
113+
)
114+
115+
stage_name = "local"
116+
deploy_1 = apigateway_client.create_deployment(restApiId=api_id, stageName=stage_name)
117+
snapshot.match("deploy-1", deploy_1)
118+
119+
source_arn = f"arn:aws:execute-api:{region}:{account_id}:{api_id}/*/*/test/*"
120+
121+
lambda_client.add_permission(
122+
FunctionName=lambda_arn,
123+
StatementId=str(short_uid()),
124+
Action="lambda:InvokeFunction",
125+
Principal="apigateway.amazonaws.com",
126+
SourceArn=source_arn,
127+
)
128+
129+
url = api_invoke_url(api_id, stage=stage_name, path="/test/value")
130+
response = requests.post(url, json={"test": "test"})
131+
assert response.ok
132+
assert json.loads(response.json()["body"]) == {"test": "test"}
133+
134+
response = apigateway_client.update_method(
135+
restApiId=api_id,
136+
resourceId=resource_id,
137+
httpMethod="POST",
138+
patchOperations=[
139+
{
140+
"op": "add",
141+
"path": "/requestParameters/method.request.path.issuer",
142+
"value": "true",
143+
},
144+
{
145+
"op": "remove",
146+
"path": "/requestParameters/method.request.path.test",
147+
"value": "true",
148+
},
149+
],
150+
)
151+
snapshot.match("change-request-path-names", response)
152+
153+
apigw_redeploy_api(rest_api_id=api_id, stage_name=stage_name)
154+
155+
response = requests.post(url, json={"test": "test"})
156+
# FIXME: for now, not implemented in LocalStack, we don't validate RequestParameters yet
157+
# assert response.status_code == 400
158+
if response.status_code == 400:
159+
snapshot.match("missing-required-request-params", response.json())
160+
161+
# create Model schema to validate body
162+
apigateway_client.create_model(
163+
restApiId=api_id,
164+
name="testSchema",
165+
contentType="application/json",
166+
schema=json.dumps(
167+
{
168+
"title": "testSchema",
169+
"type": "object",
170+
"properties": {
171+
"a": {"type": "number"},
172+
"b": {"type": "number"},
173+
},
174+
"required": ["a", "b"],
175+
}
176+
),
177+
)
178+
# then attach the schema to the method
179+
response = apigateway_client.update_method(
180+
restApiId=api_id,
181+
resourceId=resource_id,
182+
httpMethod="POST",
183+
patchOperations=[
184+
{"op": "add", "path": "/requestModels/application~1json", "value": "testSchema"},
185+
],
186+
)
187+
snapshot.match("add-schema", response)
188+
189+
response = apigateway_client.update_method(
190+
restApiId=api_id,
191+
resourceId=resource_id,
192+
httpMethod="POST",
193+
patchOperations=[
194+
{
195+
"op": "add",
196+
"path": "/requestParameters/method.request.path.test",
197+
"value": "true",
198+
},
199+
{
200+
"op": "remove",
201+
"path": "/requestParameters/method.request.path.issuer",
202+
"value": "true",
203+
},
204+
],
205+
)
206+
snapshot.match("revert-request-path-names", response)
207+
208+
apigw_redeploy_api(rest_api_id=api_id, stage_name=stage_name)
209+
210+
# the validator should then check against this schema and fail
211+
response = requests.post(url, json={"test": "test"})
212+
assert response.status_code == 400
213+
snapshot.match("invalid-request-body", response.json())
214+
215+
# remove the validator from the method
216+
response = apigateway_client.update_method(
217+
restApiId=api_id,
218+
resourceId=resource_id,
219+
httpMethod="POST",
220+
patchOperations=[
221+
{
222+
"op": "replace",
223+
& 3E26 quot;path": "/requestValidatorId",
224+
"value": "",
225+
},
226+
],
227+
)
228+
snapshot.match("remove-validator", response)
229+
230+
apigw_redeploy_api(rest_api_id=api_id, stage_name=stage_name)
231+
232+
response = requests.post(url, json={"test": "test"})
233+
assert response.ok
234+
assert json.loads(response.json()["body"]) == {"test": "test"}

0 commit comments

Comments
 (0)
0