This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-0-test by this push:
new 7be9da37c2b [v3-0-test] Fix editing connection with sensitive extra
field (#52403) (#52445)
7be9da37c2b is described below
commit 7be9da37c2bd453bd85d15c60de3350a0bfdefc2
Author: Jens Scheffler <[email protected]>
AuthorDate: Sun Jun 29 12:19:32 2025 +0200
[v3-0-test] Fix editing connection with sensitive extra field (#52403)
(#52445)
* Handle unchanges json
* Remove the redact from connections
* Fix the static checks
(cherry picked from commit 8ef792dafef6e64faf5daa376bfa5238b1c14c87)
Co-authored-by: Shubham Raj
<[email protected]>
---
.../api_fastapi/core_api/datamodels/connections.py | 25 +------
.../ui/src/pages/Connections/ConnectionForm.tsx | 12 +++-
.../airflow/ui/src/queries/useEditConnection.tsx | 4 +-
.../core_api/routes/public/test_connections.py | 81 ++++------------------
4 files changed, 26 insertions(+), 96 deletions(-)
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 fee330e1fd1..f2ac1f7a940 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
@@ -17,15 +17,12 @@
from __future__ import annotations
-import json
from collections import abc
from typing import Annotated
-from pydantic import Field, field_validator
-from pydantic_core.core_schema import ValidationInfo
+from pydantic import Field
from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
-from airflow.sdk.execution_time.secrets_masker import redact
# Response Models
@@ -42,26 +39,6 @@ class ConnectionResponse(BaseModel):
password: str | None
extra: str | None
- @field_validator("password", mode="after")
- @classmethod
- def redact_password(cls, v: str | None, field_info: ValidationInfo) -> str
| None:
- if v is None:
- return None
- return redact(v, field_info.field_name)
-
- @field_validator("extra", mode="before")
- @classmethod
- def redact_extra(cls, v: str | None) -> str | None:
- if v is None:
- return None
- try:
- extra_dict = json.loads(v)
- redacted_dict = redact(extra_dict)
- return json.dumps(redacted_dict)
- except json.JSONDecodeError:
- # we can't redact fields in an unstructured `extra`
- return v
-
class ConnectionCollectionResponse(BaseModel):
"""Connection Collection serializer for responses."""
diff --git
a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
index 866d555c068..8452e738445 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
@@ -56,7 +56,7 @@ const ConnectionForm = ({
const { conf: extra, setConf } = useParamStore();
const {
control,
- formState: { isValid },
+ formState: { isDirty, isValid },
handleSubmit,
reset,
watch,
@@ -92,6 +92,14 @@ const ConnectionForm = ({
mutateConnection(data);
};
+ const hasChanges = () => {
+ if (isDirty) {
+ return true;
+ }
+
+ return JSON.stringify(JSON.parse(extra)) !==
JSON.stringify(JSON.parse(initialConnection.extra));
+ };
+
const validateAndPrettifyJson = (value: string) => {
try {
const parsedJson = JSON.parse(value) as JSON;
@@ -232,7 +240,7 @@ const ConnectionForm = ({
<Spacer />
<Button
colorPalette="blue"
- disabled={Boolean(errors.conf) || formErrors || isPending ||
!isValid}
+ disabled={Boolean(errors.conf) || formErrors || isPending ||
!isValid || !hasChanges()}
onClick={() => void handleSubmit(onSubmit)()}
>
<FiSave /> Save
diff --git a/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx
b/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx
index 6f7face3a32..d76e34bd57b 100644
--- a/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx
@@ -60,7 +60,9 @@ export const useEditConnection = (
const editConnection = (requestBody: ConnectionBody) => {
const updateMask: Array<string> = [];
- if (requestBody.extra !== initialConnection.extra) {
+ if (
+ JSON.stringify(JSON.parse(requestBody.extra)) !==
JSON.stringify(JSON.parse(initialConnection.extra))
+ ) {
updateMask.push("extra");
}
if (requestBody.conn_type !== initialConnection.conn_type) {
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 ddff799431c..4741a03f67c 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
@@ -158,35 +158,6 @@ class TestGetConnection(TestConnectionEndpoint):
assert body["conn_type"] == TEST_CONN_TYPE
assert body["extra"] == '{"extra_key": "extra_value"}'
- @pytest.mark.enable_redact
- def test_get_should_respond_200_with_extra_redacted(self, test_client,
session):
- self.create_connection()
- connection = session.query(Connection).first()
- connection.extra = '{"password": "test-password"}'
- session.commit()
- response = test_client.get(f"/connections/{TEST_CONN_ID}")
- assert response.status_code == 200
- body = response.json()
- assert body["connection_id"] == TEST_CONN_ID
- assert body["conn_type"] == TEST_CONN_TYPE
- assert body["extra"] == '{"password": "***"}'
-
- @pytest.mark.enable_redact
- def test_get_should_not_overmask_short_password_value_in_extra(self,
test_client, session):
- connection = Connection(
- conn_id=TEST_CONN_ID, conn_type="generic", login="a",
password="a", extra='{"key": "value"}'
- )
- session.add(connection)
- session.commit()
-
- response = test_client.get(f"/connections/{TEST_CONN_ID}")
- assert response.status_code == 200
- body = response.json()
- assert body["connection_id"] == TEST_CONN_ID
- assert body["conn_type"] == "generic"
- assert body["login"] == "a"
- assert body["extra"] == '{"key": "value"}'
-
class TestGetConnections(TestConnectionEndpoint):
@pytest.mark.parametrize(
@@ -308,7 +279,6 @@ class TestPostConnection(TestConnectionEndpoint):
assert "detail" in response_json
assert list(response_json["detail"].keys()) == ["reason", "statement",
"orig_error", "message"]
- @pytest.mark.enable_redact
@pytest.mark.parametrize(
"body, expected_response",
[
@@ -321,21 +291,7 @@ class TestPostConnection(TestConnectionEndpoint):
"extra": None,
"host": None,
"login": None,
- "password": "***",
- "port": None,
- "schema": None,
- },
- ),
- (
- {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"password": "?>@#+!_%()#"},
- {
- "connection_id": TEST_CONN_ID,
- "conn_type": TEST_CONN_TYPE,
- "description": None,
- "extra": None,
- "host": None,
- "login": None,
- "password": "***",
+ "password": "test-password",
"port": None,
"schema": None,
},
@@ -351,21 +307,23 @@ class TestPostConnection(TestConnectionEndpoint):
"connection_id": TEST_CONN_ID,
"conn_type": TEST_CONN_TYPE,
"description": None,
- "extra": '{"password": "***"}',
+ "extra": '{"password": "test-password"}',
"host": None,
"login": None,
- "password": "***",
+ "password": "A!rF|0wi$aw3s0m3",
"port": None,
"schema": None,
},
),
],
)
- def test_post_should_response_201_redacted_password(self, test_client,
body, expected_response, session):
+ def test_post_should_response_201_password_not_masked(
+ self, test_client, body, expected_response, session
+ ):
response = test_client.post("/connections", json=body)
assert response.status_code == 201
assert response.json() == expected_response
- _check_last_log(session, dag_id=None, event="post_connection",
logical_date=None, check_masked=True)
+ _check_last_log(session, dag_id=None, event="post_connection",
logical_date=None)
class TestPatchConnection(TestConnectionEndpoint):
@@ -775,22 +733,7 @@ class TestPatchConnection(TestConnectionEndpoint):
"extra": None,
"host": "some_host_a",
"login": "some_login",
- "password": "***",
- "port": 8080,
- "schema": None,
- },
- {"update_mask": ["password"]},
- ),
- (
- {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"password": "?>@#+!_%()#"},
- {
- "connection_id": TEST_CONN_ID,
- "conn_type": TEST_CONN_TYPE,
- "description": "some_description_a",
- "extra": None,
- "host": "some_host_a",
- "login": "some_login",
- "password": "***",
+ "password": "test-password",
"port": 8080,
"schema": None,
},
@@ -807,10 +750,10 @@ class TestPatchConnection(TestConnectionEndpoint):
"connection_id": TEST_CONN_ID,
"conn_type": TEST_CONN_TYPE,
"description": "some_description_a",
- "extra": '{"password": "***"}',
+ "extra": '{"password": "test-password"}',
"host": "some_host_a",
"login": "some_login",
- "password": "***",
+ "password": "A!rF|0wi$aw3s0m3",
"port": 8080,
"schema": None,
},
@@ -818,14 +761,14 @@ class TestPatchConnection(TestConnectionEndpoint):
),
],
)
- def test_patch_should_response_200_redacted_password(
+ def test_patch_should_response_200_password_not_masked(
self, test_client, session, body, expected_response, update_mask
):
self.create_connections()
response = test_client.patch(f"/connections/{TEST_CONN_ID}",
json=body, params=update_mask)
assert response.status_code == 200
assert response.json() == expected_response
- _check_last_log(session, dag_id=None, event="patch_connection",
logical_date=None, check_masked=True)
+ _check_last_log(session, dag_id=None, event="patch_connection",
logical_date=None)
class TestConnection(TestConnectionEndpoint):