This is an automated email from the ASF dual-hosted git repository.
bugraoz 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 395cb1e759e Add integration tests for config sensitive masking in
airflowctl (#61105)
395cb1e759e is described below
commit 395cb1e759e8d93c2bea697d0e0b472660853467
Author: Subham <[email protected]>
AuthorDate: Wed Feb 11 13:13:31 2026 +0530
Add integration tests for config sensitive masking in airflowctl (#61105)
* Add integration tests for config sensitive masking in airflowctl
* Fix static check failures: Sort imports in
test_config_sensitive_masking.py
* Refactor integration tests to reduce duplication by moving shared logic
to conftest.py
* Fix static checks in conftest.py
* Address PR review feedback: Remove flaky decorator and fix CI test
discovery
- Remove @pytest.mark.flaky decorator from test_config_sensitive_masking.py
as suggested
- Fix CI test discovery by changing hardcoded path in run_tests.py from
'test_airflowctl_commands.py' to 'airflowctl_tests' directory to allow
pytest to discover all test files in the directory
---
.../tests/airflowctl_tests/conftest.py | 75 ++++++++++++++++++++++
.../tests/airflowctl_tests/constants.py | 3 +
.../airflowctl_tests/test_airflowctl_commands.py | 67 +------------------
.../test_config_sensitive_masking.py | 51 +++++++++++++++
dev/breeze/src/airflow_breeze/utils/run_tests.py | 2 +-
5 files changed, 133 insertions(+), 65 deletions(-)
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
index 3ac692722f2..9e6ef75b7fa 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
@@ -20,6 +20,7 @@ import os
import subprocess
import sys
+import pytest
from python_on_whales import DockerClient, docker
from airflowctl_tests import console
@@ -27,11 +28,85 @@ from airflowctl_tests.constants import (
AIRFLOW_ROOT_PATH,
DOCKER_COMPOSE_FILE_PATH,
DOCKER_IMAGE,
+ LOGIN_COMMAND,
+ LOGIN_OUTPUT,
)
from tests_common.test_utils.fernet import generate_fernet_key_string
[email protected]
+def run_command():
+ """Fixture that provides a helper to run airflowctl commands."""
+
+ def _run_command(command: str, skip_login: bool = False) -> str:
+ import os
+ from subprocess import PIPE, STDOUT, Popen
+
+ host_envs = os.environ.copy()
+ host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
+
+ command_from_config = f"airflowctl {command}"
+
+ # We need to run auth login first for all commands except login itself
(unless skipped)
+ if not skip_login and command != LOGIN_COMMAND:
+ run_cmd = f"airflowctl {LOGIN_COMMAND} && {command_from_config}"
+ else:
+ run_cmd = command_from_config
+
+ console.print(f"[yellow]Running command: {command}")
+
+ # Give some time for the command to execute and output to be ready
+ proc = Popen(run_cmd.encode(), stdout=PIPE, stderr=STDOUT, stdin=PIPE,
shell=True, env=host_envs)
+ stdout_bytes, stderr_result = proc.communicate(timeout=60)
+
+ # CLI command gave errors
+ assert not stderr_result, (
+ f"Errors while executing command
'{command_from_config}':\n{stderr_result.decode()}"
+ )
+
+ # Decode the output
+ stdout_result = stdout_bytes.decode()
+
+ # We need to trim auth login output if the command is not login itself
and clean backspaces
+ if not skip_login and command != LOGIN_COMMAND:
+ assert LOGIN_OUTPUT in stdout_result, (
+ f"❌ Login output not found before command output for
'{command_from_config}'\nFull output:\n{stdout_result}"
+ )
+ stdout_result = stdout_result.split(f"{LOGIN_OUTPUT}\n")[1].strip()
+ else:
+ stdout_result = stdout_result.strip()
+
+ # Check for non-zero exit code
+ assert proc.returncode == 0, (
+ f"❌ Command '{command_from_config}' exited with code
{proc.returncode}\nOutput:\n{stdout_result}"
+ )
+
+ # Error patterns to detect failures that might otherwise slip through
+ # Please ensure it is aligning with
airflowctl.api.client.get_json_error
+ error_patterns = [
+ "Server error",
+ "command error",
+ "unrecognized arguments",
+ "invalid choice",
+ "Traceback (most recent call last):",
+ ]
+ matched_error = next((error for error in error_patterns if error in
stdout_result), None)
+ assert not matched_error, (
+ f"❌ Output contained unexpected text for command
'{command_from_config}'\n"
+ f"Matched error pattern: {matched_error}\n"
+ f"Output:\n{stdout_result}"
+ )
+
+ console.print(f"[green]✅ Output did not contain unexpected text for
command '{command_from_config}'")
+ console.print(f"[cyan]Result:\n{stdout_result}\n")
+ proc.kill()
+
+ return stdout_result
+
+ return _run_command
+
+
class _CtlTestState:
docker_client: DockerClient | None = None
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/constants.py
b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
index 549a9ad8c74..4ddf2636c7f 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/constants.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
@@ -30,3 +30,6 @@ DOCKER_COMPOSE_HOST_PORT = os.environ.get("HOST_PORT",
"localhost:8080")
DOCKER_COMPOSE_FILE_PATH = (
AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / "howto" / "docker-compose" /
"docker-compose.yaml"
)
+
+LOGIN_COMMAND = "auth login --username airflow --password airflow"
+LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
diff --git
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index ceb29f0143d..228321ac7e8 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -16,12 +16,9 @@
# under the License.
from __future__ import annotations
-import os
-from subprocess import PIPE, STDOUT, Popen
-
import pytest
-from airflowctl_tests import console
+from airflowctl_tests.constants import LOGIN_COMMAND
def date_param():
@@ -47,8 +44,6 @@ def date_param():
return random_dt.isoformat()
-LOGIN_COMMAND = "auth login --username airflow --password airflow"
-LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
ONE_DATE_PARAM = date_param()
TEST_COMMANDS = [
# Passing password via command line is insecure but acceptable for testing
purposes
@@ -134,62 +129,6 @@ TEST_COMMANDS = [
@pytest.mark.parametrize(
"command", TEST_COMMANDS, ids=[" ".join(command.split(" ", 2)[:2]) for
command in TEST_COMMANDS]
)
-def test_airflowctl_commands(command: str):
+def test_airflowctl_commands(command: str, run_command):
"""Test airflowctl commands using docker-compose environment."""
- host_envs = os.environ.copy()
- host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
-
- command_from_config = f"airflowctl {command}"
- # We need to run auth login first for all commands except login itself
- if command != LOGIN_COMMAND:
- run_command = f"airflowctl {LOGIN_COMMAND} && {command_from_config}"
- else:
- run_command = command_from_config
- console.print(f"[yellow]Running command: {command}")
-
- # Give some time for the command to execute and output to be ready
- proc = Popen(run_command.encode(), stdout=PIPE, stderr=STDOUT, stdin=PIPE,
shell=True, env=host_envs)
- stdout_bytes, stderr_result = proc.communicate(timeout=60)
-
- # CLI command gave errors
- assert not stderr_result, (
- f"Errors while executing command
'{command_from_config}':\n{stderr_result.decode()}"
- )
-
- # Decode the output
- stdout_result = stdout_bytes.decode()
- # We need to trim auth login output if the command is not login itself and
clean backspaces
- if command != LOGIN_COMMAND:
- assert LOGIN_OUTPUT in stdout_result, (
- f"❌ Login output not found before command output for
'{command_from_config}'",
- f"\nFull output:\n{stdout_result}",
- )
- stdout_result = stdout_result.split(f"{LOGIN_OUTPUT}\n")[1].strip()
- else:
- stdout_result = stdout_result.strip()
-
- # Check for non-zero exit code
- assert proc.returncode == 0, (
- f"❌ Command '{command_from_config}' exited with code
{proc.returncode}",
- f"\nOutput:\n{stdout_result}",
- )
-
- # Error patterns to detect failures that might otherwise slip through
- # Please ensure it is aligning with airflowctl.api.client.get_json_error
- error_patterns = [
- "Server error",
- "command error",
- "unrecognized arguments",
- "invalid choice",
- "Traceback (most recent call last):",
- ]
- matched_error = next((error for error in error_patterns if error in
stdout_result), None)
- assert not matched_error, (
- f"❌ Output contained unexpected text for command
'{command_from_config}'",
- f"\nMatched error pattern: {matched_error}",
- f"\nOutput:\n{stdout_result}",
- )
-
- console.print(f"[green]✅ Output did not contain unexpected text for
command '{command_from_config}'")
- console.print(f"[cyan]Result:\n{stdout_result}\n")
- proc.kill()
+ run_command(command)
diff --git
a/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
new file mode 100644
index 00000000000..84719bb4cc3
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+
+# Test commands for config sensitive masking verification
+SENSITIVE_CONFIG_COMMANDS = [
+ # Test that config list shows masked sensitive values
+ "config list",
+ # Test that getting specific sensitive config values are masked
+ "config get --section core --option fernet_key",
+ "config get --section database --option sql_alchemy_conn",
+]
+
+
[email protected](
+ "command",
+ SENSITIVE_CONFIG_COMMANDS,
+ ids=[" ".join(command.split(" ", 2)[:2]) for command in
SENSITIVE_CONFIG_COMMANDS],
+)
+def test_config_sensitive_masking(command: str, run_command):
+ """
+ Test that sensitive config values are properly masked by airflowctl.
+
+ This integration test verifies that when airflowctl retrieves config data
from the
+ Airflow API, sensitive values (like fernet_key, sql_alchemy_conn) appear
masked
+ as '< hidden >' and do not leak actual secret values.
+ """
+ stdout_result = run_command(command)
+
+ # CRITICAL: Verify that sensitive values are masked
+ # The Airflow API returns masked values as "< hidden >" for sensitive
configs
+ assert "< hidden >" in stdout_result, (
+ f"❌ Expected masked value '< hidden >' not found in output for
'airflowctl {command}'\n"
+ f"Output:\n{stdout_result}"
+ )
diff --git a/dev/breeze/src/airflow_breeze/utils/run_tests.py
b/dev/breeze/src/airflow_breeze/utils/run_tests.py
index 9dff5d3935b..211384eeafe 100644
--- a/dev/breeze/src/airflow_breeze/utils/run_tests.py
+++ b/dev/breeze/src/airflow_breeze/utils/run_tests.py
@@ -152,7 +152,7 @@ def run_docker_compose_tests(
test_path = Path("tests") / "airflow_e2e_tests" / f"{test_mode}_tests"
cwd = AIRFLOW_E2E_TESTS_ROOT_PATH.as_posix()
elif test_type == "airflow-ctl-integration":
- test_path = Path("tests") / "airflowctl_tests" /
"test_airflowctl_commands.py"
+ test_path = Path("tests") / "airflowctl_tests"
cwd = AIRFLOW_CTL_TESTS_ROOT_PATH.as_posix()
else:
test_path = Path("tests") / "docker_tests" /
"test_docker_compose_quick_start.py"