This is an automated email from the ASF dual-hosted git repository.
Lee-W pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 37f842157de [v3-3-test] Reject invalid partition keys in the create
asset events API (#69453) (#69581)
37f842157de is described below
commit 37f842157de0eac8a3326b26e6940057ccb30be0
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Jul 8 14:05:59 2026 +0800
[v3-3-test] Reject invalid partition keys in the create asset events API
(#69453) (#69581)
Co-authored-by: Wei Lee <[email protected]>
---
.../api_fastapi/core_api/datamodels/assets.py | 20 ++++++++++++++----
.../core_api/openapi/v2-rest-api-generated.yaml | 2 ++
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 4 +++-
.../core_api/routes/public/test_assets.py | 24 ++++++++++++++++++++++
.../src/airflowctl/api/datamodels/generated.py | 6 +++++-
5 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
index 753ea94f1d9..3be0707bee1 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
@@ -19,14 +19,24 @@ from __future__ import annotations
from collections.abc import Iterable
from datetime import datetime
-from typing import TYPE_CHECKING
-
-from pydantic import AliasPath, AwareDatetime, ConfigDict, Field, JsonValue,
NonNegativeInt, field_validator
+from typing import TYPE_CHECKING, Annotated
+
+from pydantic import (
+ AliasPath,
+ AwareDatetime,
+ ConfigDict,
+ Field,
+ JsonValue,
+ NonNegativeInt,
+ StringConstraints,
+ field_validator,
+)
from airflow._shared.secrets_masker import redact
from airflow._shared.timezones import timezone
from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
from airflow.api_fastapi.core_api.datamodels.dag_run import
TriggerDAGRunPostBody
+from airflow.models.base import ID_LEN
from airflow.utils.types import DagRunType
if TYPE_CHECKING:
@@ -190,7 +200,9 @@ class CreateAssetEventsBody(StrictBaseModel):
"""Create asset events request."""
asset_id: int
- partition_key: str | None = None
+ # pattern (not strip_whitespace) so the value isn't mutated — must stay
byte-identical to what
+ # `_validate_outlet_event_partition_keys` in the Execution API accepts for
the same raw input.
+ partition_key: Annotated[str, StringConstraints(pattern=r"\S",
max_length=ID_LEN)] | None = None
extra: dict = Field(default_factory=dict)
access_control: AssetEventAccessControl | None = None
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index ea6f0a70fcd..abb77e1dfe9 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -13147,6 +13147,8 @@ components:
partition_key:
anyOf:
- type: string
+ maxLength: 250
+ pattern: \S
- type: 'null'
title: Partition Key
extra:
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 9685fd6b3cb..690c79c1314 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -2504,7 +2504,9 @@ export const $CreateAssetEventsBody = {
partition_key: {
anyOf: [
{
- type: 'string'
+ type: 'string',
+ maxLength: 250,
+ pattern: '\\S'
},
{
type: 'null'
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
index 3487fa28598..f28915973af 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
@@ -37,6 +37,7 @@ from airflow.models.asset import (
DagScheduleAssetReference,
TaskOutletAssetReference,
)
+from airflow.models.base import ID_LEN
from airflow.models.dagrun import DagRun
from airflow.models.serialized_dag import SerializedDagModel
from airflow.models.trigger import Trigger
@@ -1336,6 +1337,29 @@ class TestPostAssetEvents(TestAssets):
assert response.status_code == 422
+ @pytest.mark.parametrize(
+ ("partition_key", "expected_status_code"),
+ [
+ pytest.param("", 422, id="empty"),
+ pytest.param(" ", 422, id="whitespace_only"),
+ pytest.param("a" * (ID_LEN + 1), 422, id="too_long"),
+ pytest.param("2026-03-23", 200, id="valid"),
+ pytest.param(None, 200, id="none"),
+ ],
+ )
+ def test_partition_key_validation(self, test_client, session,
partition_key, expected_status_code):
+ (asset,) = self.create_assets(num=1, session=session)
+ event_payload = {"asset_id": asset.id, "partition_key": partition_key}
+ response = test_client.post("/assets/events", json=event_payload)
+ assert response.status_code == expected_status_code
+
+ def test_partition_key_preserves_surrounding_whitespace(self, test_client,
session):
+ (asset,) = self.create_assets(num=1, session=session)
+ event_payload = {"asset_id": asset.id, "partition_key": " 2026-03-23
"}
+ response = test_client.post("/assets/events", json=event_payload)
+ assert response.status_code == 200
+ assert response.json()["partition_key"] == " 2026-03-23 "
+
@pytest.mark.usefixtures("time_freezer")
@pytest.mark.enable_redact
def test_should_mask_sensitive_extra(self, test_client, session):
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index bd7ce6dd448..746ba1e02da 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -398,6 +398,10 @@ class ConnectionTestResponse(BaseModel):
message: Annotated[str, Field(title="Message")]
+class PartitionKey(RootModel[str]):
+ root: Annotated[str, Field(max_length=250, pattern="\\S", title="Partition
Key")]
+
+
class CreateAssetEventsBody(BaseModel):
"""
Create asset events request.
@@ -407,7 +411,7 @@ class CreateAssetEventsBody(BaseModel):
extra="forbid",
)
asset_id: Annotated[int, Field(title="Asset Id")]
- partition_key: Annotated[str | None, Field(title="Partition Key")] = None
+ partition_key: Annotated[PartitionKey | None, Field(title="Partition
Key")] = None
extra: Annotated[dict[str, Any] | None, Field(title="Extra")] = None
access_control: AssetEventAccessControl | None = None