diff --git a/airflow-core/newsfragments/70264.bugfix.rst b/airflow-core/newsfragments/70264.bugfix.rst new file mode 100644 index 0000000000000..9647401b0e896 --- /dev/null +++ b/airflow-core/newsfragments/70264.bugfix.rst @@ -0,0 +1 @@ +The ``port`` field on the Connection API (``POST``/``PATCH /connections``) now validates that the value is a valid TCP/UDP port number (between 1 and 65535). Previously, negative numbers, ``0``, and values above 65535 were silently accepted. \ No newline at end of file diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py index 34cfe47334893..ec3d01889b78f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py @@ -191,7 +191,12 @@ class ConnectionBody(StrictBaseModel): host: str | None = Field(default=None) login: str | None = Field(default=None) schema_: str | None = Field(None, alias="schema") - port: int | None = Field(default=None) + port: int | None = Field( + default=None, + ge=1, + le=65535, + description="TCP/UDP port number. Must be between 1 and 65535.", + ) password: str | None = Field(default=None) extra: str | None = Field(default=None) team_name: str | None = Field(max_length=50, default=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 3ddcf0b1c8d82..53d4b9fef0a06 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 @@ -13251,8 +13251,11 @@ components: port: anyOf: - type: integer + maximum: 65535.0 + minimum: 1.0 - type: 'null' title: Port + description: TCP/UDP port number. Must be between 1 and 65535. password: anyOf: - type: string @@ -13323,8 +13326,11 @@ components: port: anyOf: - type: integer + maximum: 65535.0 + minimum: 1.0 - type: 'null' title: Port + description: TCP/UDP port number. Must be between 1 and 65535. password: anyOf: - type: string @@ -13406,8 +13412,11 @@ components: port: anyOf: - type: integer + maximum: 65535.0 + minimum: 1.0 - type: 'null' title: Port + description: TCP/UDP port number. Must be between 1 and 65535. password: anyOf: - type: string diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py index 231a7ca7b22ef..5ec9e7c687a27 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py @@ -368,6 +368,28 @@ def test_post_should_respond_422_for_invalid_conn_id(self, test_client, body): ] } + @pytest.mark.parametrize( + "port", + [-1, 0, 65536, 99999, 123456789], + ) + def test_post_should_respond_422_for_invalid_port(self, test_client, port): + body = {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE, "port": port} + response = test_client.post("/connections", json=body) + assert response.status_code == 422 + detail = response.json()["detail"][0] + assert detail["loc"] == ["body", "port"] + assert detail["input"] == port + + @pytest.mark.parametrize( + "port", + [1, 22, 8080, 65535], + ) + def test_post_should_respond_201_for_valid_port(self, test_client, session, port): + body = {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE, "port": port} + response = test_client.post("/connections", json=body) + assert response.status_code == 201 + assert response.json()["port"] == port + @conf_vars({("core", "multi_team"): "False"}) def test_post_rejects_team_name_when_multi_team_disabled(self, test_client): response = test_client.post( @@ -1013,6 +1035,19 @@ def test_patch_with_update_mask_rejects_extra_fields(self, test_client): ) assert response.status_code == 422 + @pytest.mark.parametrize( + "port", + [-1, 0, 65536, 99999], + ) + def test_patch_should_respond_422_for_invalid_port(self, test_client, port): + self.create_connection() + response = test_client.patch( + f"/connections/{TEST_CONN_ID}", + json={"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE, "port": port}, + params={"update_mask": ["port"]}, + ) + assert response.status_code == 422 + @conf_vars({("core", "multi_team"): "False"}) def test_patch_rejects_team_name_when_multi_team_disabled(self, test_client): self.create_connection()