Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions airflow/api_connexion/endpoints/config_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)])]
Expand Down
52 changes: 52 additions & 0 deletions tests/api_connexion/endpoints/test_config_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
"""\
Expand All @@ -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": [
Expand Down Expand Up @@ -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(
"""\
Expand All @@ -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": [
Expand Down Expand Up @@ -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(
Expand Down