This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new a6d87ba5b01 Skip stored credentials when a connection test overrides
host or port (#69957)
a6d87ba5b01 is described below
commit a6d87ba5b0157baf744995d47dd5d6a47e798edb
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Thu Jul 16 16:06:26 2026 +0200
Skip stored credentials when a connection test overrides host or port
(#69957)
The POST /connections/test endpoint restores an existing connection's stored
credentials (password/extra) when the request sends the masked sentinel, so
a
saved connection can be tested without re-typing secrets. Connection
configuration users have write-only access to those credentials and are not
meant to view them; reusing them while the request points at a
caller-supplied
host or port applies them against a destination the caller chose rather than
the connection's own, which is outside the intent of the retype-free test
flow.
Scope the credential reuse to requests that keep the stored connection's
host
and port. Follows the same hardening as #67620.
---
.../core_api/routes/public/connections.py | 15 +++++
.../core_api/routes/public/test_connections.py | 65 ++++++++++++++++++++++
2 files changed, 80 insertions(+)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
index c1d89c89798..19378dd8aea 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
@@ -349,6 +349,21 @@ def test_connection(
except AirflowNotFoundException:
existing_conn = None
+ if existing_conn is not None:
+ # Stored credentials are only reused to test the connection's own
+ # host/port; testing a different destination must supply its own.
+ fields_set = test_body.model_fields_set
+ if ("host" in fields_set and test_body.host != existing_conn.host)
or (
+ "port" in fields_set and test_body.port != existing_conn.port
+ ):
+ if "password" not in fields_set:
+ raise HTTPException(
+ status.HTTP_400_BAD_REQUEST,
+ "The host or port to test differs from the stored
connection. "
+ "Include the credentials to test in the request body.",
+ )
+ existing_conn = None
+
if existing_conn is not None:
existing_conn.conn_id = transient_conn_id
update_orm_from_pydantic(existing_conn, test_body)
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 7c5ce03654e..231a7ca7b22 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
@@ -1348,6 +1348,71 @@ class TestConnection(TestConnectionEndpoint):
assert json.loads(db_conn.extra) == {"path": "/", "existing_key":
"existing_value"}
assert session.scalar(select(func.count()).select_from(Connection)) ==
initial_count
+ @mock.patch.dict(os.environ, {"AIRFLOW__CORE__TEST_CONNECTION": "Enabled"})
+ @pytest.mark.parametrize(
+ "override",
+ [
+ pytest.param({"host": "other_host"}, id="host-changed"),
+ pytest.param({"host": "stored_host", "port": 9999},
id="port-changed"),
+ ],
+ )
+ def test_should_reject_test_when_target_overridden_without_credentials(
+ self, test_client, session, override
+ ):
+ session.add(
+ Connection(
+ conn_id=TEST_CONN_ID,
+ conn_type="sqlite",
+ host="stored_host",
+ port=1234,
+ password="existing_password",
+ )
+ )
+ session.commit()
+
+ body = {"connection_id": TEST_CONN_ID, "conn_type": "sqlite",
**override}
+ response = test_client.post("/connections/test", json=body)
+
+ assert response.status_code == 400
+
+ @mock.patch.dict(os.environ, {"AIRFLOW__CORE__TEST_CONNECTION": "Enabled"})
+ @pytest.mark.parametrize(
+ ("override", "expected_password"),
+ [
+ pytest.param(
+ {"host": "stored_host", "port": 1234}, "existing_password",
id="same-target-reuses-stored"
+ ),
+ pytest.param(
+ {"host": "other_host", "password": "supplied_password"},
+ "supplied_password",
+ id="overridden-target-uses-supplied-creds",
+ ),
+ ],
+ )
+ def test_stored_secret_reused_only_for_same_target(
+ self, test_client, session, override, expected_password
+ ):
+ session.add(
+ Connection(
+ conn_id=TEST_CONN_ID,
+ conn_type="sqlite",
+ host="stored_host",
+ port=1234,
+ password="existing_password",
+ )
+ )
+ session.commit()
+
+ body = {"connection_id": TEST_CONN_ID, "conn_type": "sqlite",
**override}
+
+ with mock.patch.object(Connection, "test_connection", autospec=True)
as mock_test:
+ mock_test.return_value = (True, "mocked")
+ response = test_client.post("/connections/test", json=body)
+
+ assert response.status_code == 200
+ tested_connection = mock_test.call_args.args[0]
+ assert tested_connection.password == expected_password
+
@mock.patch.dict(os.environ, {"AIRFLOW__CORE__TEST_CONNECTION": "Enabled"})
def test_should_test_new_connection_without_existing(self, test_client):
body = {