This is an automated email from the ASF dual-hosted git repository.

shahar1 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 1bcc8e6bdbc Validate the file structure in airflowctl connections 
import (#75176)
1bcc8e6bdbc is described below

commit 1bcc8e6bdbc11f151a38a064a6950156b7a5e78a
Author: Rishabh <[email protected]>
AuthorDate: Sat Oct 31 02:19:34 2026 +0530

    Validate the file structure in airflowctl connections import (#75176)
    
    `airflowctl connections import` assumed the file was a JSON object of
    objects. A file holding a list, a scalar, or an entry that is not an object
    failed with internal Python messages such as
    "'list' object has no attribute 'items'" or
    "'str' object has no attribute 'get'", which do not tell the user what is
    wrong with the file.
    
    Check the structure right after parsing, before building any request, and
    exit with a message that names the file, the offending connection id and the
    expected shape. `variables import` and `pools import` already validate their
    input the same way. No request is sent for a malformed file.
---
 .../airflowctl/ctl/commands/connection_command.py  | 11 ++++++++
 .../ctl/commands/test_connections_command.py       | 30 ++++++++++++++++++++++
 2 files changed, 41 insertions(+)

diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
index 02958740a36..4c4067ffcb7 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
@@ -45,6 +45,17 @@ def import_(args, api_client=NEW_API_CLIENT) -> None:
             connections_json = json.loads(file.read())
         except Exception as e:
             raise SystemExit(f"Error reading connections file {args.file}: 
{e}")
+    if not isinstance(connections_json, dict):
+        raise SystemExit(
+            f"Invalid connections file {args.file}: expected a JSON object 
mapping connection ids "
+            f"to connection details, got {type(connections_json).__name__}"
+        )
+    for connection_id, connection_details in connections_json.items():
+        if not isinstance(connection_details, dict):
+            raise SystemExit(
+                f"Invalid connection {connection_id!r} in {args.file}: 
expected a JSON object "
+                f"of connection details, got 
{type(connection_details).__name__}"
+            )
     try:
         connections_data = {
             k: ConnectionBody(
diff --git 
a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
index 452d6182e7d..f400f07dcfd 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
@@ -217,6 +217,36 @@ class TestCliConnectionCommands:
             api_client=api_client,
         )
 
+    @pytest.mark.parametrize(
+        ("file_content", "expected_message"),
+        [
+            ([{"conn_type": "postgres"}], "expected a JSON object mapping 
connection ids"),
+            ("not-an-object", "expected a JSON object mapping connection ids"),
+            ({"my_conn": "postgres://host"}, "Invalid connection 'my_conn'"),
+            ({"my_conn": None}, "Invalid connection 'my_conn'"),
+        ],
+        ids=["list", "string", "string-entry", "null-entry"],
+    )
+    def test_import_rejects_malformed_file_structure(
+        self, tmp_path, monkeypatch, file_content, expected_message
+    ):
+        monkeypatch.chdir(tmp_path)
+        json_path = tmp_path / self.export_file_name
+        json_path.write_text(json.dumps(file_content))
+        mock_client = mock.MagicMock(spec=Client)
+
+        with pytest.raises(SystemExit) as exc_info:
+            connection_command.import_(
+                self.parser.parse_args(["connections", "import", 
json_path.as_posix()]),
+                api_client=mock_client,
+            )
+
+        message = str(exc_info.value)
+        assert expected_message in message
+        assert json_path.as_posix() in message
+        assert "has no attribute" not in message
+        mock_client.connections.bulk.assert_not_called()
+
     @pytest.mark.parametrize(
         ("action_on_existing_key", "expected_enum"),
         [

Reply via email to