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 447c20fa2cf Fix DAG named "DAGs" colliding with the global DAGs
permission resource (#69106)
447c20fa2cf is described below
commit 447c20fa2cf702ae3f76e5e7695187f8fa934f19
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon Jul 6 15:28:28 2026 +0200
Fix DAG named "DAGs" colliding with the global DAGs permission resource
(#69106)
resource_name() returned the dag_id unchanged when it equalled a reserved
resource name. Because a real dag_id can be "DAGs" (it passes the dag_id
validator), a DAG named "DAGs" resolved to the global DAGs resource instead
of its own per-DAG "DAG:DAGs" resource, so permissions for that DAG were
applied to the wrong resource.
Drop the reserved-name short-circuit so a dag_id is always prefixed. The
already-prefixed branch (e.g. "DAG:foo") still short-circuits, which is safe
because the "DAG:" / "DAG Run:" prefixes contain a colon that the dag_id
validator (KEY_REGEX = ^[\w.-]+$) forbids. Mirrored in the airflow-core
copy.
---
airflow-core/src/airflow/security/permissions.py | 2 --
.../tests/unit/security/test_permissions.py | 26 ++++++++++++++++++++++
.../providers/fab/www/security/permissions.py | 2 --
.../tests/unit/fab/auth_manager/test_security.py | 6 +++++
4 files changed, 32 insertions(+), 4 deletions(-)
diff --git a/airflow-core/src/airflow/security/permissions.py
b/airflow-core/src/airflow/security/permissions.py
index 867ea7b6bdf..cb6aa7fe889 100644
--- a/airflow-core/src/airflow/security/permissions.py
+++ b/airflow-core/src/airflow/security/permissions.py
@@ -106,8 +106,6 @@ PREFIX_RESOURCES_MAP = {details["prefix"]: resource for
resource, details in RES
def resource_name(dag_id: str, resource: str) -> str:
"""Return the resource name for a DAG id."""
- if dag_id in RESOURCE_DETAILS_MAP.keys():
- return dag_id
if dag_id.startswith(tuple(PREFIX_RESOURCES_MAP.keys())):
return dag_id
return f"{RESOURCE_DETAILS_MAP[resource]['prefix']}{dag_id}"
diff --git a/airflow-core/tests/unit/security/test_permissions.py
b/airflow-core/tests/unit/security/test_permissions.py
new file mode 100644
index 00000000000..b212bd1e2d0
--- /dev/null
+++ b/airflow-core/tests/unit/security/test_permissions.py
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from airflow.security import permissions
+
+
+def test_resource_name_does_not_collide_with_reserved_resource_names():
+ # Regression: a Dag literally named "DAGs" (the global resource name, and
a valid
+ # dag_id) must resolve to its own per-DAG resource, never the global one.
+ assert permissions.resource_name(permissions.RESOURCE_DAG,
permissions.RESOURCE_DAG) == "DAG:DAGs"
diff --git
a/providers/fab/src/airflow/providers/fab/www/security/permissions.py
b/providers/fab/src/airflow/providers/fab/www/security/permissions.py
index 37ce89ba18c..630a95ea541 100644
--- a/providers/fab/src/airflow/providers/fab/www/security/permissions.py
+++ b/providers/fab/src/airflow/providers/fab/www/security/permissions.py
@@ -96,8 +96,6 @@ PREFIX_RESOURCES_MAP = {details["prefix"]: resource for
resource, details in RES
def resource_name(dag_id: str, resource: str) -> str:
"""Return the resource name for a DAG id."""
- if dag_id in RESOURCE_DETAILS_MAP.keys():
- return dag_id
if dag_id.startswith(tuple(PREFIX_RESOURCES_MAP.keys())):
return dag_id
return f"{RESOURCE_DETAILS_MAP[resource]['prefix']}{dag_id}"
diff --git a/providers/fab/tests/unit/fab/auth_manager/test_security.py
b/providers/fab/tests/unit/fab/auth_manager/test_security.py
index e80e5f3b077..f2520fbd884 100644
--- a/providers/fab/tests/unit/fab/auth_manager/test_security.py
+++ b/providers/fab/tests/unit/fab/auth_manager/test_security.py
@@ -1273,3 +1273,9 @@ def test_add_user_uses_configured_hash_method(mock_hash,
app, security_manager):
mock_hash.assert_called_with("plaintext",
method="pbkdf2:sha256")
finally:
delete_user(app, "hash_method_add_test")
+
+
+def test_resource_name_does_not_collide_with_reserved_resource_names():
+ # Regression: a Dag literally named "DAGs" (the global resource name, and
a valid
+ # dag_id) must resolve to its own per-DAG resource, never the global one.
+ assert permissions.resource_name(permissions.RESOURCE_DAG,
permissions.RESOURCE_DAG) == "DAG:DAGs"