diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 3ae3391c1a4b0..1c73f2af0d104 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -2832,6 +2832,8 @@ paths: - Config summary: Get Config operationId: get_config + security: + - OAuth2PasswordBearer: [] parameters: - name: section in: query @@ -2912,6 +2914,8 @@ paths: - Config summary: Get Config Value operationId: get_config_value + security: + - OAuth2PasswordBearer: [] parameters: - name: section in: path diff --git a/airflow/api_fastapi/core_api/routes/public/config.py b/airflow/api_fastapi/core_api/routes/public/config.py index 13edf5f821b4c..1df1582591581 100644 --- a/airflow/api_fastapi/core_api/routes/public/config.py +++ b/airflow/api_fastapi/core_api/routes/public/config.py @@ -18,7 +18,7 @@ import textwrap -from fastapi import HTTPException, status +from fastapi import Depends, HTTPException, status from fastapi.responses import Response from airflow.api_fastapi.common.headers import HeaderAcceptJsonOrText @@ -30,6 +30,7 @@ ConfigSection, ) from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc +from airflow.api_fastapi.core_api.security import requires_access_configuration from airflow.configuration import conf text_example_response_for_get_config_value = { @@ -108,6 +109,7 @@ def _response_based_on_accept(accept: Mimetype, config: Config): }, }, response_model=Config, + dependencies=[Depends(requires_access_configuration("GET"))], ) def get_config( accept: HeaderAcceptJsonOrText, @@ -153,6 +155,7 @@ def get_config( }, }, response_model=Config, + dependencies=[Depends(requires_access_configuration("GET"))], ) def get_config_value( section: str, diff --git a/airflow/api_fastapi/core_api/security.py b/airflow/api_fastapi/core_api/security.py index 0c9be4be64667..a8837a93c4225 100644 --- a/airflow/api_fastapi/core_api/security.py +++ b/airflow/api_fastapi/core_api/security.py @@ -27,6 +27,7 @@ from airflow.auth.managers.models.base_user import BaseUser from airflow.auth.managers.models.resource_details import ( AssetDetails, + ConfigurationDetails, ConnectionDetails, DagAccessEntity, DagDetails, @@ -123,6 +124,24 @@ def inner( return inner +def requires_access_configuration(method: ResourceMethod) -> Callable[[Request, BaseUser | None], None]: + def inner( + request: Request, + user: Annotated[BaseUser | None, Depends(get_user)] = None, + ) -> None: + section: str | None = request.query_params.get("section") or request.path_params.get("section") + + _requires_access( + is_authorized_callback=lambda: get_auth_manager().is_authorized_configuration( + method=method, + details=ConfigurationDetails(section=section), + user=user, + ) + ) + + return inner + + def requires_access_variable(method: ResourceMethod) -> Callable[[Request, BaseUser | None], None]: def inner( request: Request, diff --git a/tests/api_fastapi/core_api/routes/public/test_config.py b/tests/api_fastapi/core_api/routes/public/test_config.py index 90009d1b54152..914cd5fcb3f0a 100644 --- a/tests/api_fastapi/core_api/routes/public/test_config.py +++ b/tests/api_fastapi/core_api/routes/public/test_config.py @@ -305,6 +305,14 @@ def test_get_config_non_sensitive_only( response = test_client.get("/public/config", headers=headers) self._validate_response(headers, expected_response, expected_status_code, response) + def test_get_config_should_response_401(self, unauthenticated_test_client): + response = unauthenticated_test_client.get("/public/config") + assert response.status_code == 401 + + def test_get_config_should_response_403(self, unauthorized_test_client): + response = unauthorized_test_client.get("/public/config") + assert response.status_code == 403 + class TestGetConfigValue(TestConfigEndpoint): @pytest.mark.parametrize( @@ -474,3 +482,15 @@ def test_get_config_value_non_sensitive_only( with conf_vars(AIRFLOW_CONFIG_NON_SENSITIVE_ONLY_CONFIG): response = test_client.get(f"/public/config/section/{section}/option/{option}", headers=headers) self._validate_response(headers, expected_response, expected_status_code, response) + + def test_get_config_value_should_response_401(self, unauthenticated_test_client): + response = unauthenticated_test_client.get( + f"/public/config/section/{SECTION_DATABASE}/option/{OPTION_KEY_SQL_ALCHEMY_CONN}" + ) + assert response.status_code == 401 + + def test_get_config_value_should_response_403(self, unauthorized_test_client): + response = unauthorized_test_client.get( + f"/public/config/section/{SECTION_DATABASE}/option/{OPTION_KEY_SQL_ALCHEMY_CONN}" + ) + assert response.status_code == 403