Skip to content

Commit 6e40b3d

Browse files
feat(parser): add support for sourceIp with port (#7315)
* Fix sourceIp resolution * Fix sourceIp resolution * Remove ValidationError * Remove ValidationError
1 parent 997607a commit 6e40b3d

File tree

6 files changed

+78
-6
lines changed

6 files changed

+78
-6
lines changed

aws_lambda_powertools/utilities/parser/functions.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from typing import TYPE_CHECKING, Any
66

7-
from pydantic import TypeAdapter
7+
from pydantic import IPvAnyNetwork, TypeAdapter
88

99
from aws_lambda_powertools.shared.cache_dict import LRUDict
1010

@@ -82,3 +82,28 @@ def _parse_and_validate_event(data: dict[str, Any] | Any, adapter: TypeAdapter):
8282
data = json.loads(data)
8383

8484
return adapter.validate_python(data)
85+
86+
87+
def _validate_source_ip(value):
88+
"""
89+
Handle sourceIp that may come with port (e.g., "10.1.15.242:39870")
90+
in certain network configurations like Cloudflare + CloudFront + API Gateway.
91+
Validates the IP part while preserving the original format.
92+
See: https://github.com/aws-powertools/powertools-lambda-python/issues/7288
93+
"""
94+
95+
if value == "test-invoke-source-ip":
96+
return value
97+
98+
try:
99+
# The value is always an instance of str before Pydantic validation occurs.
100+
# So the first thing to do is try to convert it.
101+
IPvAnyNetwork(value)
102+
except ValueError:
103+
try:
104+
ip_part = value.split(":")[0]
105+
IPvAnyNetwork(ip_part)
106+
except (ValueError, IndexError) as e:
107+
raise ValueError(f"Invalid IP address in sourceIp: {ip_part}") from e
108+
109+
return value

aws_lambda_powertools/utilities/parser/models/apigw.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from datetime import datetime
22
from typing import Any, Dict, List, Literal, Optional, Type, Union
33

4-
from pydantic import BaseModel, model_validator
4+
from pydantic import BaseModel, field_validator, model_validator
55
from pydantic.networks import IPvAnyNetwork
66

7+
from aws_lambda_powertools.utilities.parser.functions import _validate_source_ip
8+
79

810
class ApiGatewayUserCertValidity(BaseModel):
911
notBefore: str
@@ -31,12 +33,17 @@ class APIGatewayEventIdentity(BaseModel):
3133
principalOrgId: Optional[str] = None
3234
# see #1562, temp workaround until API Gateway fixes it the Test button payload
3335
# removing it will not be considered a regression in the future
34-
sourceIp: Union[IPvAnyNetwork, Literal["test-invoke-source-ip"]]
36+
sourceIp: Union[IPvAnyNetwork, str]
3537
user: Optional[str] = None
3638
userAgent: Optional[str] = None
3739
userArn: Optional[str] = None
3840
clientCert: Optional[ApiGatewayUserCert] = None
3941

42+
@field_validator("sourceIp", mode="before")
43+
@classmethod
44+
def _validate_source_ip(cls, value):
45+
return _validate_source_ip(value=value)
46+
4047

4148
class APIGatewayEventAuthorizer(BaseModel):
4249
claims: Optional[Dict[str, Any]] = None

aws_lambda_powertools/utilities/parser/models/apigwv2.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from datetime import datetime
22
from typing import Any, Dict, List, Literal, Optional, Type, Union
33

4-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel, Field, field_validator
55
from pydantic.networks import IPvAnyNetwork
66

7+
from aws_lambda_powertools.utilities.parser.functions import _validate_source_ip
8+
79

810
class RequestContextV2AuthorizerIamCognito(BaseModel):
911
amr: List[str]
@@ -36,9 +38,14 @@ class RequestContextV2Http(BaseModel):
3638
method: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
3739
path: str
3840
protocol: str
39-
sourceIp: IPvAnyNetwork
41+
sourceIp: Union[IPvAnyNetwork, str]
4042
userAgent: str
4143

44+
@field_validator("sourceIp", mode="before")
45+
@classmethod
46+
def _validate_source_ip(cls, value):
47+
return _validate_source_ip(value=value)
48+
4249

4350
class RequestContextV2(BaseModel):
4451
accountId: str

tests/unit/parser/_pydantic/test_apigw.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ def test_apigw_event():
105105
assert identity.apiKeyId is None
106106

107107

108+
def test_apigw_event_and_source_ip_with_port():
109+
raw_event = load_event("apiGatewayProxyEvent.json")
110+
raw_event["requestContext"]["identity"]["sourceIp"] = "10.10.10.10:1235"
111+
112+
APIGatewayProxyEventModel(**raw_event)
113+
114+
115+
def test_apigw_event_and_source_ip_with_random_string():
116+
raw_event = load_event("apiGatewayProxyEvent.json")
117+
raw_event["requestContext"]["identity"]["sourceIp"] = "NON_IP_WITH_OR_WITHOUT_PORT_STRING"
118+
119+
with pytest.raises(ValidationError):
120+
APIGatewayProxyEventModel(**raw_event)
121+
122+
108123
def test_apigw_event_with_invalid_websocket_request():
109124
# GIVEN an event with an eventType != MESSAGE and has a messageId
110125
event = {

tests/unit/parser/_pydantic/test_apigwv2.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
14
from aws_lambda_powertools.utilities.parser import envelopes, parse
25
from aws_lambda_powertools.utilities.parser.models import (
36
ApiGatewayAuthorizerRequestV2,
@@ -71,6 +74,21 @@ def test_apigw_v2_event_empty_jwt_scopes():
7174
APIGatewayProxyEventV2Model(**raw_event)
7275

7376

77+
def test_apigw_v2_event_and_source_ip_with_port():
78+
raw_event = load_event("apiGatewayProxyV2Event.json")
79+
raw_event["requestContext"]["http"]["sourceIp"] = "10.10.10.10:1235"
80+
81+
APIGatewayProxyEventV2Model(**raw_event)
82+
83+
84+
def test_apigw_v2_event_and_source_ip_with_random_string():
85+
raw_event = load_event("apiGatewayProxyV2Event.json")
86+
raw_event["requestContext"]["http"]["sourceIp"] = "NON_IP_WITH_OR_WITHOUT_PORT_STRING"
87+
88+
with pytest.raises(ValidationError):
89+
APIGatewayProxyEventV2Model(**raw_event)
90+
91+
7492
def test_api_gateway_proxy_v2_event_lambda_authorizer():
7593
raw_event = load_event("apiGatewayProxyV2LambdaAuthorizerEvent.json")
7694
parsed_event: APIGatewayProxyEventV2Model = APIGatewayProxyEventV2Model(**raw_event)

tests/unit/test_shared_functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
from aws_lambda_powertools.shared import constants
1212
from aws_lambda_powertools.shared.functions import (
1313
abs_lambda_path,
14-
slice_dictionary,
1514
extract_event_from_common_models,
1615
powertools_debug_is_set,
1716
powertools_dev_is_set,
1817
resolve_env_var_choice,
1918
resolve_max_age,
2019
resolve_truthy_env_var_choice,
2120
sanitize_xray_segment_name,
21+
slice_dictionary,
2222
strtobool,
2323
)
2424
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper

0 commit comments

Comments
 (0)