Skip to content

Commit c1ea9a4

Browse files
NRL-512 Initial implementation for rejecting HEAD requests
1 parent 961c4ae commit c1ea9a4

File tree

6 files changed

+138
-1
lines changed

6 files changed

+138
-1
lines changed

terraform/infrastructure/modules/api_gateway/api_gateway.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ resource "aws_api_gateway_deployment" "api_gateway_deployment" {
6060
}
6161

6262
depends_on = [
63-
aws_api_gateway_rest_api.api_gateway_rest_api
63+
aws_api_gateway_rest_api.api_gateway_rest_api,
64+
aws_api_gateway_integration_response.head_integration_response
6465
]
6566
}
6667

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Define a reusable map of allowed methods for each path
2+
locals {
3+
endpoint_allowed_methods = {
4+
"/DocumentReference" = "GET,POST,PUT"
5+
# "/DocumentReference/_search" = "POST"
6+
# "/DocumentReference/{id}" = "GET"
7+
}
8+
}
9+
10+
data "aws_api_gateway_resource" "resource" {
11+
for_each = local.endpoint_allowed_methods
12+
rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
13+
path = each.key
14+
15+
depends_on = [aws_api_gateway_rest_api.api_gateway_rest_api]
16+
}
17+
18+
# Add HEAD method to each resource with 405 response
19+
resource "aws_api_gateway_method" "head_method" {
20+
for_each = local.endpoint_allowed_methods
21+
22+
rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
23+
resource_id = data.aws_api_gateway_resource.resource[each.key].id
24+
http_method = "HEAD"
25+
authorization = "NONE"
26+
}
27+
28+
resource "aws_api_gateway_integration" "head_integration" {
29+
for_each = local.endpoint_allowed_methods
30+
31+
rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
32+
resource_id = data.aws_api_gateway_resource.resource[each.key].id
33+
http_method = aws_api_gateway_method.head_method[each.key].http_method
34+
type = "MOCK"
35+
# passthrough_behavior = "WHEN_NO_TEMPLATES"
36+
passthrough_behavior = "WHEN_NO_MATCH"
37+
38+
request_templates = {
39+
"application/json" = <<-EOF
40+
{ "statusCode": 405 }
41+
EOF
42+
"application/json+fhir" = <<-EOF
43+
{ "statusCode": 405 }
44+
EOF
45+
"application/fhir+json" = <<-EOF
46+
{ "statusCode": 405 }
47+
EOF
48+
}
49+
}
50+
51+
resource "aws_api_gateway_method_response" "head_method_response" {
52+
for_each = local.endpoint_allowed_methods
53+
54+
rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
55+
resource_id = data.aws_api_gateway_resource.resource[each.key].id
56+
http_method = aws_api_gateway_method.head_method[each.key].http_method
57+
status_code = "405"
58+
59+
response_parameters = {
60+
"method.response.header.Allow" = true
61+
}
62+
}
63+
64+
resource "aws_api_gateway_integration_response" "head_integration_response" {
65+
for_each = local.endpoint_allowed_methods
66+
67+
rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
68+
resource_id = data.aws_api_gateway_resource.resource[each.key].id
69+
http_method = aws_api_gateway_method.head_method[each.key].http_method
70+
status_code = aws_api_gateway_method_response.head_method_response[each.key].status_code
71+
selection_pattern = "" # default catch-all
72+
73+
response_parameters = {
74+
"method.response.header.Allow" = "'${each.value}'"
75+
}
76+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Feature: Consumer - HEAD Requests
2+
3+
@custom_tag
4+
Scenario: DocumentReference with HEAD fails
5+
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
6+
When consumer 'RX898' sends HEAD request to DocumentReference endpoint
7+
Then the response status code is 405
8+
And the response has an empty body
9+
And the Allow header is 'GET'

tests/features/steps/2_request.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ def consumer_read_document_reference_step(
7676
context.response = client.read(doc_ref_id)
7777

7878

79+
@when("consumer '{ods_code}' sends HEAD request to {endpoint} endpoint")
80+
def consumer_head_request_step(context: Context, ods_code: str, endpoint: str):
81+
client = consumer_client_from_context(context, ods_code)
82+
context.response = client.head(endpoint)
83+
84+
85+
@when(
86+
"consumer '{ods_code}' sends HEAD request to '{endpoint}' endpoint with parameters"
87+
)
88+
def consumer_head_request_step_with_parameters(
89+
context: Context, ods_code: str, endpoint: str
90+
):
91+
if not context.table:
92+
raise ValueError("No search query table provided")
93+
94+
items = {row["parameter"]: row["value"] for row in context.table}
95+
96+
client = consumer_client_from_context(context, ods_code)
97+
context.response = client.head(endpoint, items)
98+
99+
79100
@when("producer '{ods_code}' creates a DocumentReference with values")
80101
def create_post_document_reference_step(context: Context, ods_code: str):
81102
client = producer_client_from_context(context, ods_code)

tests/features/steps/3_assert.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ def assert_bundle_step(context: Context, bundle_type: str):
5151
context.bundle = Bundle.model_validate(body)
5252

5353

54+
@then("the response has an empty body")
55+
def assert_empty_body_step(context: Context):
56+
"""
57+
Asserts that the response body is empty.
58+
"""
59+
assert context.response.text == "", format_error(
60+
"Response body is not empty",
61+
"empty",
62+
context.response.text,
63+
context.response.text,
64+
)
65+
context.bundle = None
66+
67+
5468
@then("the Bundle has a total of {total}")
5569
def assert_bundle_total_step(context: Context, total: str):
5670
assert (

tests/utilities/api_clients.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,22 @@ def search_post(
192192
cert=self.config.client_cert,
193193
)
194194

195+
@retry_if([502])
196+
def head(
197+
self,
198+
endpoint: str,
199+
extra_params: dict[str, str] | None = None,
200+
) -> Response:
201+
params = {**(extra_params or {})}
202+
url = f"{self.api_url}/{endpoint}"
203+
headers = {**self.request_headers, "Content-Type": "application/json"}
204+
return requests.head(
205+
url,
206+
params=params,
207+
headers=headers,
208+
cert=self.config.client_cert,
209+
)
210+
195211
@retry_if([502])
196212
def read_capability_statement(self) -> Response:
197213
return requests.get(

0 commit comments

Comments
 (0)