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

vincbeck 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 fa514dfe140 fix keycloak missing resource error check (#69028)
fa514dfe140 is described below

commit fa514dfe140751a6a7de98b0067349cdb25f3415
Author: stephen-bracken <[email protected]>
AuthorDate: Mon Jun 29 14:00:54 2026 +0100

    fix keycloak missing resource error check (#69028)
    
    The error raised by keycloak that should be handled is 
`{"error":"invalid_resource", "error_description": "Resource with id 
[Dag:team-a] does not exist."}`. This returns a 400 client error which is then 
converted into a 500 error by Airflow.
---
 providers/keycloak/docs/auth-manager/index.rst     |  7 ++++++
 .../keycloak/auth_manager/keycloak_auth_manager.py | 27 +++++++++++-----------
 .../auth_manager/test_keycloak_auth_manager.py     | 20 +++++++++-------
 3 files changed, 33 insertions(+), 21 deletions(-)

diff --git a/providers/keycloak/docs/auth-manager/index.rst 
b/providers/keycloak/docs/auth-manager/index.rst
index e5726490744..dc967c4b395 100644
--- a/providers/keycloak/docs/auth-manager/index.rst
+++ b/providers/keycloak/docs/auth-manager/index.rst
@@ -54,3 +54,10 @@ It enables you to manage users, roles, groups, and 
permissions entirely within K
     :maxdepth: 2
 
     manage/login
+
+**Note on Multi Team Clients**
+When using the Keycloak auth manager with a multi-team deployment, you will 
need to keep the keycloak client up to date by creating the required policies 
and resources for each new team that is added.
+
+If a team has dags added to Airflow but they are not setup in the keycloak 
client, keycloak will return a 400 error on the ``/dags`` screen:
+``{"error":"invalid_resource", "error_description": "Resource with id 
[Dag:team-a] does not exist."}``
+The keycloak auth manager will handle this error by registering it as a 'deny' 
response to preserve access to the ``/dags`` screen for other teams.
diff --git 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
index 545f7803994..64f12603ce6 100644
--- 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
+++ 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
@@ -78,11 +78,6 @@ if TYPE_CHECKING:
 log = logging.getLogger(__name__)
 
 RESOURCE_ID_ATTRIBUTE_NAME = "resource_id"
-KEYCLOAK_RESOURCE_NOT_FOUND_ERROR = "resource not found:"
-
-
-def _is_missing_keycloak_resource_response(status_code: int, text: Any) -> 
bool:
-    return status_code == 500 and isinstance(text, str) and 
KEYCLOAK_RESOURCE_NOT_FOUND_ERROR in text.lower()
 
 
 TEAM_SCOPED_RESOURCES = frozenset(
@@ -407,16 +402,19 @@ class 
KeycloakAuthManager(BaseAuthManager[KeycloakAuthManagerUser]):
         server_url = conf.get(CONF_SECTION_NAME, CONF_SERVER_URL_KEY)
 
         context_attributes = prune_dict(attributes or {})
+
+        is_team_resource = bool(
+            team_name
+            and conf.getboolean("core", "multi_team", fallback=False)
+            and resource_type in TEAM_SCOPED_RESOURCES
+        )
+
         if resource_id:
             context_attributes[RESOURCE_ID_ATTRIBUTE_NAME] = resource_id
         elif method == "GET":
             method = "LIST"
 
-        if (
-            team_name
-            and conf.getboolean("core", "multi_team", fallback=False)
-            and resource_type in TEAM_SCOPED_RESOURCES
-        ):
+        if is_team_resource:
             resource_name = f"{resource_type.value}:{team_name}"
         else:
             resource_name = resource_type.value
@@ -438,12 +436,15 @@ class 
KeycloakAuthManager(BaseAuthManager[KeycloakAuthManagerUser]):
             return False
         if resp.status_code == 400:
             error = json.loads(resp.text)
+            if is_team_resource and error.get("error") == "invalid_resource":
+                # filter_authorized_dag_ids will return this error if team 
resources have not been added to the Keycloak Client.
+                log.warning(
+                    "Keycloak authorization resource is missing; denying 
access. Response: %s", resp.text
+                )
+                return False
             raise AirflowException(
                 f"Request not recognized by Keycloak. {error.get('error')}. 
{error.get('error_description')}"
             )
-        if _is_missing_keycloak_resource_response(resp.status_code, resp.text):
-            log.warning("Keycloak authorization resource is missing; denying 
access. Response: %s", resp.text)
-            return False
         raise AirflowException(f"Unexpected error: {resp.status_code} - 
{resp.text}")
 
     def filter_authorized_dag_ids(
diff --git 
a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
 
b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
index 347e9944e5e..48791d11d38 100644
--- 
a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
+++ 
b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
@@ -38,6 +38,7 @@ from 
airflow.api_fastapi.auth.managers.models.resource_details import (
     VariableDetails,
 )
 
+from tests_common.test_utils.config import conf_vars
 from tests_common.test_utils.version_compat import AIRFLOW_V_3_1_7_PLUS, 
AIRFLOW_V_3_2_PLUS
 
 if AIRFLOW_V_3_2_PLUS:
@@ -65,8 +66,6 @@ from 
airflow.providers.keycloak.auth_manager.keycloak_auth_manager import (
 )
 from airflow.providers.keycloak.auth_manager.user import 
KeycloakAuthManagerUser
 
-from tests_common.test_utils.config import conf_vars
-
 
 def _build_access_token(payload: dict[str, object]) -> str:
     header = {"alg": "none", "typ": "JWT"}
@@ -386,18 +385,22 @@ class TestKeycloakAuthManager:
 
             assert "Unexpected error" in str(e.value)
 
+    @pytest.mark.skipif(not AIRFLOW_V_3_2_PLUS, reason="team_name not 
supported before Airflow 3.2.0")
+    @conf_vars({("core", "multi_team"): "True"})
     def test_is_authorized_missing_keycloak_resource(self, auth_manager, user, 
caplog):
         resp = Mock()
-        resp.status_code = 500
-        resp.text = "resource not found: Dag:team-a"
+        resp.status_code = 400
+        resp.text = '{"error": "invalid_resource", "error_description": 
"Resource with id [Dag:team-a] does not exist."}'
         auth_manager.http_session.post = Mock(return_value=resp)
         caplog.set_level("WARNING", 
logger="airflow.providers.keycloak.auth_manager.keycloak_auth_manager")
 
-        result = auth_manager.is_authorized_dag(method="GET", 
details=DagDetails(id="dag_0"), user=user)
+        result = auth_manager.is_authorized_dag(
+            method="GET", details=DagDetails(id="dag_0", team_name="team-a"), 
user=user
+        )
 
         assert result is False
         assert "Keycloak authorization resource is missing; denying access" in 
caplog.text
-        assert "resource not found: Dag:team-a" in caplog.text
+        assert "Resource with id [Dag:team-a] does not exist." in caplog.text
 
     @pytest.mark.parametrize(
         "function",
@@ -666,14 +669,15 @@ class TestKeycloakAuthManager:
         assert result == {"dag-a"}
 
     @pytest.mark.skipif(not AIRFLOW_V_3_2_PLUS, reason="team_name not 
supported before Airflow 3.2.0")
+    @conf_vars({("core", "multi_team"): "True"})
     def test_filter_authorized_dag_ids_missing_keycloak_resource(self, 
auth_manager_multi_team, user, caplog):
         def post_response(*_, data, **__):
             claims = json.loads(base64.b64decode(data["claim_token"]).decode())
             dag_id = claims[RESOURCE_ID_ATTRIBUTE_NAME][0]
             response = Mock()
             if dag_id == "dag-missing":
-                response.status_code = 500
-                response.text = "resource not found: Dag:team-a"
+                response.status_code = 400
+                response.text = '{"error":"invalid_resource", 
"error_description": "Resource with id [Dag:team-a] does not exist."}'
             else:
                 response.status_code = 200
             return response

Reply via email to