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