diff --git a/airflow/api_connexion/endpoints/config_endpoint.py b/airflow/api_connexion/endpoints/config_endpoint.py index b7bb81194b415..1fe2eff777086 100644 --- a/airflow/api_connexion/endpoints/config_endpoint.py +++ b/airflow/api_connexion/endpoints/config_endpoint.py @@ -74,12 +74,19 @@ def get_config(*, section: str | None = None) -> Response: "application/json": _config_to_json, } return_type = request.accept_mimetypes.best_match(serializer.keys()) + if conf.get("webserver", "expose_config").lower() == "non-sensitive-only": + expose_config = True + display_sensitive = False + else: + expose_config = conf.getboolean("webserver", "expose_config") + display_sensitive = True + if return_type not in serializer: return Response(status=HTTPStatus.NOT_ACCEPTABLE) - elif conf.getboolean("webserver", "expose_config"): + elif expose_config: if section and not conf.has_section(section): raise NotFound("section not found.", detail=f"section={section} not found.") - conf_dict = conf.as_dict(display_source=False, display_sensitive=True) + conf_dict = conf.as_dict(display_source=False, display_sensitive=display_sensitive) if section: conf_section_value = conf_dict[section] conf_dict.clear() @@ -103,14 +110,24 @@ def get_value(section: str, option: str) -> Response: "application/json": _config_to_json, } return_type = request.accept_mimetypes.best_match(serializer.keys()) + if conf.get("webserver", "expose_config").lower() == "non-sensitive-only": + expose_config = True + else: + expose_config = conf.getboolean("webserver", "expose_config") + if return_type not in serializer: return Response(status=HTTPStatus.NOT_ACCEPTABLE) - elif conf.getboolean("webserver", "expose_config"): + elif expose_config: if not conf.has_option(section, option): raise NotFound( "Config not found.", detail=f"The option [{section}/{option}] is not found in config." ) - value = conf.get(section, option) + + print(conf.sensitive_config_values) + if (section, option) in conf.sensitive_config_values: + value = "< hidden >" + else: + value = conf.get(section, option) config = Config( sections=[ConfigSection(name=section, options=[ConfigOption(key=option, value=value)])] diff --git a/tests/api_connexion/endpoints/test_config_endpoint.py b/tests/api_connexion/endpoints/test_config_endpoint.py index 1233c24e8d4b2..c8d309b0bdf62 100644 --- a/tests/api_connexion/endpoints/test_config_endpoint.py +++ b/tests/api_connexion/endpoints/test_config_endpoint.py @@ -35,6 +35,14 @@ }, } +MOCK_CONF_WITH_SENSITIVE_VALUE = { + "core": {"parallelism": "1024", "sql_alchemy_conn": "mock_conn"}, + "smtp": { + "smtp_host": "localhost", + "smtp_mail_from": "airflow@example.com", + }, +} + @pytest.fixture(scope="module") def configured_app(minimal_app_for_api): @@ -65,6 +73,27 @@ def test_should_respond_200_text_plain(self, mock_as_dict): response = self.client.get( "/api/v1/config", headers={"Accept": "text/plain"}, environ_overrides={"REMOTE_USER": "test"} ) + mock_as_dict.assert_called_with(display_source=False, display_sensitive=True) + assert response.status_code == 200 + expected = textwrap.dedent( + """\ + [core] + parallelism = 1024 + + [smtp] + smtp_host = localhost + smtp_mail_from = airflow@example.com + """ + ) + assert expected == response.data.decode() + + @patch("airflow.api_connexion.endpoints.config_endpoint.conf.as_dict", return_value=MOCK_CONF) + @conf_vars({("webserver", "expose_config"): "non-sensitive-only"}) + def test_should_respond_200_text_plain_with_non_sensitive_only(self, mock_as_dict): + response = self.client.get( + "/api/v1/config", headers={"Accept": "text/plain"}, environ_overrides={"REMOTE_USER": "test"} + ) + mock_as_dict.assert_called_with(display_source=False, display_sensitive=False) assert response.status_code == 200 expected = textwrap.dedent( """\ @@ -85,6 +114,7 @@ def test_should_respond_200_application_json(self, mock_as_dict): headers={"Accept": "application/json"}, environ_overrides={"REMOTE_USER": "test"}, ) + mock_as_dict.assert_called_with(display_source=False, display_sensitive=True) assert response.status_code == 200 expected = { "sections": [ @@ -112,6 +142,7 @@ def test_should_respond_200_single_section_as_text_plain(self, mock_as_dict): headers={"Accept": "text/plain"}, environ_overrides={"REMOTE_USER": "test"}, ) + mock_as_dict.assert_called_with(display_source=False, display_sensitive=True) assert response.status_code == 200 expected = textwrap.dedent( """\ @@ -129,6 +160,7 @@ def test_should_respond_200_single_section_as_json(self, mock_as_dict): headers={"Accept": "application/json"}, environ_overrides={"REMOTE_USER": "test"}, ) + mock_as_dict.assert_called_with(display_source=False, display_sensitive=True) assert response.status_code == 200 expected = { "sections": [ @@ -210,6 +242,26 @@ def test_should_respond_200_text_plain(self, mock_as_dict): ) assert expected == response.data.decode() + @patch( + "airflow.api_connexion.endpoints.config_endpoint.conf.as_dict", + return_value=MOCK_CONF_WITH_SENSITIVE_VALUE, + ) + @conf_vars({("webserver", "expose_config"): "non-sensitive-only"}) + def test_should_respond_200_text_plain_with_non_sensitive_only(self, mock_as_dict): + response = self.client.get( + "/api/v1/config/section/core/option/sql_alchemy_conn", + headers={"Accept": "text/plain"}, + environ_overrides={"REMOTE_USER": "test"}, + ) + assert response.status_code == 200 + expected = textwrap.dedent( + """\ + [core] + sql_alchemy_conn = < hidden > + """ + ) + assert expected == response.data.decode() + @patch("airflow.api_connexion.endpoints.config_endpoint.conf.as_dict", return_value=MOCK_CONF) def test_should_respond_200_application_json(self, mock_as_dict): response = self.client.get(