You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by we...@apache.org on 2025/03/07 09:33:46 UTC

(airflow) branch main updated: AIP-84 | Add Auth for Config (#47208)

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

weilee 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 ab3a1869c57 AIP-84 | Add Auth for Config (#47208)
ab3a1869c57 is described below

commit ab3a1869c57def3ee74a925709cece4c7e07b891
Author: LIU ZHE YOU <68...@users.noreply.github.com>
AuthorDate: Fri Mar 7 17:33:39 2025 +0800

    AIP-84 | Add Auth for Config (#47208)
    
    * AIP-84 | Add Auth for Config
    
    * Fix requires_access_configuration schema
    
    * Refactor requires_access_configuration with lambda
---
 .../api_fastapi/core_api/openapi/v1-generated.yaml   |  4 ++++
 airflow/api_fastapi/core_api/routes/public/config.py |  5 ++++-
 airflow/api_fastapi/core_api/security.py             | 19 +++++++++++++++++++
 .../core_api/routes/public/test_config.py            | 20 ++++++++++++++++++++
 4 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index 3ae3391c1a4..1c73f2af0d1 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 13edf5f821b..1df15825915 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 @@ from __future__ import annotations
 
 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 @@ from airflow.api_fastapi.core_api.datamodels.config import (
     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 @@ config_router = AirflowRouter(tags=["Config"], prefix="/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 0c9be4be646..a8837a93c42 100644
--- a/airflow/api_fastapi/core_api/security.py
+++ b/airflow/api_fastapi/core_api/security.py
@@ -27,6 +27,7 @@ from airflow.api_fastapi.app import get_auth_manager
 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 requires_access_connection(method: ResourceMethod) -> Callable[[Request, Bas
     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 90009d1b541..914cd5fcb3f 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 @@ class TestGetConfig(TestConfigEndpoint):
             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 @@ class TestGetConfigValue(TestConfigEndpoint):
         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