Skip to content

Commit ffa3135

Browse files
committed
fix: validate v1 compat attribute names
Signed-off-by: MickeyWzt <289528356+MickeyWzt@users.noreply.github.com>
1 parent 4a689cb commit ffa3135

8 files changed

Lines changed: 83 additions & 7 deletions

File tree

src/cloudevents/core/spec.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,20 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from typing import Literal
14+
import re
15+
from typing import Final, Literal
1516

1617
SpecVersion = Literal["1.0", "0.3"]
1718
SPECVERSION_V1_0 = "1.0"
1819
SPECVERSION_V0_3 = "0.3"
20+
21+
_ATTRIBUTE_NAME_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9]+$")
22+
23+
24+
def is_valid_attribute_name(name: str) -> bool:
25+
"""
26+
Return whether a name follows the CloudEvents attribute naming convention.
27+
28+
See https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#attribute-naming-convention
29+
"""
30+
return bool(_ATTRIBUTE_NAME_PATTERN.fullmatch(name))

src/cloudevents/core/v03/event.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
import re
1615
import uuid
1716
from collections import defaultdict
1817
from datetime import datetime, timezone
@@ -27,7 +26,7 @@
2726
InvalidAttributeValueError,
2827
MissingRequiredAttributeError,
2928
)
30-
from cloudevents.core.spec import SPECVERSION_V0_3
29+
from cloudevents.core.spec import SPECVERSION_V0_3, is_valid_attribute_name
3130

3231
REQUIRED_ATTRIBUTES: Final[list[str]] = ["id", "source", "type", "specversion"]
3332
OPTIONAL_ATTRIBUTES: Final[list[str]] = [
@@ -274,7 +273,7 @@ def _validate_extension_attributes(
274273
msg=f"Extension attribute name must be at least 1 character long but was '{extension_attribute}'",
275274
)
276275
)
277-
if not re.match(r"^[a-z0-9]+$", extension_attribute):
276+
if not is_valid_attribute_name(extension_attribute):
278277
errors[extension_attribute].append(
279278
CustomExtensionAttributeError(
280279
attribute_name=extension_attribute,

src/cloudevents/core/v1/event.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
import re
1615
import uuid
1716
from collections import defaultdict
1817
from datetime import datetime, timezone
@@ -27,7 +26,7 @@
2726
InvalidAttributeValueError,
2827
MissingRequiredAttributeError,
2928
)
30-
from cloudevents.core.spec import SPECVERSION_V1_0
29+
from cloudevents.core.spec import SPECVERSION_V1_0, is_valid_attribute_name
3130

3231
REQUIRED_ATTRIBUTES: Final[list[str]] = ["id", "source", "type", "specversion"]
3332
OPTIONAL_ATTRIBUTES: Final[list[str]] = [
@@ -259,7 +258,7 @@ def _validate_extension_attributes(
259258
msg=f"Extension attribute name must be at least 1 character long but was '{extension_attribute}'",
260259
)
261260
)
262-
if not re.match(r"^[a-z0-9]+$", extension_attribute):
261+
if not is_valid_attribute_name(extension_attribute):
263262
errors[extension_attribute].append(
264263
CustomExtensionAttributeError(
265264
attribute_name=extension_attribute,

src/cloudevents/v1/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class InvalidRequiredFields(GenericException):
2525
pass
2626

2727

28+
class InvalidAttributeName(GenericException):
29+
pass
30+
31+
2832
class InvalidStructuredJSON(GenericException):
2933
pass
3034

src/cloudevents/v1/http/event.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import uuid
1818

1919
import cloudevents.v1.exceptions as cloud_exceptions
20+
from cloudevents.core.spec import is_valid_attribute_name
2021
from cloudevents.v1 import abstract
2122
from cloudevents.v1.sdk.event import v03, v1
2223

@@ -26,6 +27,14 @@
2627
}
2728

2829

30+
def _validate_attribute_name(name: str) -> None:
31+
if not is_valid_attribute_name(name):
32+
raise cloud_exceptions.InvalidAttributeName(
33+
f"Invalid CloudEvent attribute name '{name}': "
34+
"attribute names must only contain lowercase ASCII letters and digits"
35+
)
36+
37+
2938
class CloudEvent(abstract.CloudEvent):
3039
"""
3140
Python-friendly cloudevent class supporting v1 events
@@ -59,6 +68,8 @@ def __init__(self, attributes: typing.Mapping[str, str], data: typing.Any = None
5968
:type data: typing.Any
6069
"""
6170
self._attributes = {k.lower(): v for k, v in attributes.items()}
71+
for attribute_name in self._attributes:
72+
_validate_attribute_name(attribute_name)
6273
self.data = data
6374
if "specversion" not in self._attributes:
6475
self._attributes["specversion"] = "1.0"
@@ -88,6 +99,7 @@ def get_data(self) -> typing.Optional[typing.Any]:
8899
return self.data
89100

90101
def __setitem__(self, key: str, value: typing.Any) -> None:
102+
_validate_attribute_name(key)
91103
self._attributes[key] = value
92104

93105
def __delitem__(self, key: str) -> None:

tests/test_core/test_v03/test_event.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,19 @@ def test_required_attributes_null_or_empty(
435435
]
436436
},
437437
),
438+
(
439+
"example-extension",
440+
{
441+
"example-extension": [
442+
str(
443+
CustomExtensionAttributeError(
444+
"example-extension",
445+
"Extension attribute 'example-extension' should only contain lowercase letters and numbers",
446+
)
447+
)
448+
]
449+
},
450+
),
438451
],
439452
)
440453
def test_custom_extension(extension_name: str, expected_error: dict) -> None:

tests/test_core/test_v1/test_event.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,19 @@ def test_required_attributes_null_or_empty(
371371
]
372372
},
373373
),
374+
(
375+
"example-extension",
376+
{
377+
"example-extension": [
378+
str(
379+
CustomExtensionAttributeError(
380+
"example-extension",
381+
"Extension attribute 'example-extension' should only contain lowercase letters and numbers",
382+
)
383+
)
384+
]
385+
},
386+
),
374387
],
375388
)
376389
def test_custom_extension(extension_name: str, expected_error: dict) -> None:

tests/test_v1_compat/test_event_extensions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import pytest
1818

19+
import cloudevents.v1.exceptions as cloud_exceptions
1920
from cloudevents.v1.http import CloudEvent, from_http, to_binary, to_structured
2021

2122
test_data = json.dumps({"data-key": "val"})
@@ -32,6 +33,29 @@ def test_cloudevent_access_extensions(specversion):
3233
assert event["ext1"] == "testval"
3334

3435

36+
def test_cloudevent_rejects_invalid_extension_attribute_name():
37+
with pytest.raises(cloud_exceptions.InvalidAttributeName) as exc:
38+
CloudEvent(
39+
{
40+
"type": "com.example.string",
41+
"source": "https://example.com/event-producer",
42+
"example-extension": "testval",
43+
},
44+
test_data,
45+
)
46+
47+
assert "example-extension" in str(exc.value)
48+
49+
50+
def test_cloudevent_rejects_invalid_extension_attribute_setitem():
51+
event = CloudEvent(test_attributes, test_data)
52+
53+
with pytest.raises(cloud_exceptions.InvalidAttributeName) as exc:
54+
event["example-extension"] = "testval"
55+
56+
assert "example-extension" in str(exc.value)
57+
58+
3559
@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
3660
def test_to_binary_extensions(specversion):
3761
event = CloudEvent(test_attributes, test_data)

0 commit comments

Comments
 (0)