This is an automated email from the ASF dual-hosted git repository.
potiuk 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 a058f7d54eb Only refuse team-scoped-looking env var ids when
multi_team is on (#71079)
a058f7d54eb is described below
commit a058f7d54eb91cdb87dc6e636a4109067b452fce
Author: Amogh Desai <[email protected]>
AuthorDate: Wed Aug 5 09:19:14 2026 +0530
Only refuse team-scoped-looking env var ids when multi_team is on (#71079)
* Only refuse team-scoped-looking env var ids when multi_team is on
* Only refuse team-scoped-looking env var ids when multi_team is on
---
.../src/airflow/secrets/environment_variables.py | 20 +++++++++++++++-----
airflow-core/tests/unit/always/test_secrets.py | 2 ++
.../always/test_secrets_environment_variables.py | 19 +++++++++++++++++++
3 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/airflow-core/src/airflow/secrets/environment_variables.py
b/airflow-core/src/airflow/secrets/environment_variables.py
index 2432c084f5d..6eb36978d98 100644
--- a/airflow-core/src/airflow/secrets/environment_variables.py
+++ b/airflow-core/src/airflow/secrets/environment_variables.py
@@ -21,6 +21,7 @@ from __future__ import annotations
import os
+from airflow.configuration import conf
from airflow.secrets import BaseSecretsBackend
CONN_ENV_PREFIX = "AIRFLOW_CONN_"
@@ -34,10 +35,20 @@ TEAM_SEP = "___"
class EnvironmentVariablesBackend(BaseSecretsBackend):
"""Retrieves Connection object and Variable from environment variable."""
+ @staticmethod
+ def _names_a_team_namespace(secret_id: str) -> bool:
+ """
+ Whether ``secret_id`` spells out a team scoped secret name.
+
+ Only checked in multi-team mode: ``team_name`` is never non-``None``
otherwise, so no
+ team scoped variable can exist to collide with.
+ """
+ if not conf.getboolean("core", "multi_team", fallback=False):
+ return False
+ return TEAM_SEP in secret_id
+
def get_conn_value(self, conn_id: str, team_name: str | None = None) ->
str | None:
- if TEAM_SEP in conn_id:
- # An id containing the separator could collide with another team's
namespace
- # even on the scoped lookup below, so it must be refused before
either runs.
+ if self._names_a_team_namespace(conn_id):
return None
if team_name and (
@@ -56,8 +67,7 @@ class EnvironmentVariablesBackend(BaseSecretsBackend):
:param team_name: Team name associated to the task trying to access
the variable (if any)
:return: Variable Value
"""
- if TEAM_SEP in key:
- # Same collision risk as get_conn_value, see its code comment.
+ if self._names_a_team_namespace(key):
return None
if team_name and (
diff --git a/airflow-core/tests/unit/always/test_secrets.py
b/airflow-core/tests/unit/always/test_secrets.py
index 0c2749ca9d1..ff9aebb8e75 100644
--- a/airflow-core/tests/unit/always/test_secrets.py
+++ b/airflow-core/tests/unit/always/test_secrets.py
@@ -122,6 +122,7 @@ class TestConnectionsFromSecrets:
assert conn.get_uri() == "mysql://airflow:airflow@host:5432/airflow"
@pytest.mark.db_test
+ @conf_vars({("core", "multi_team"): "True"})
@mock.patch.dict(
"os.environ",
{
@@ -218,6 +219,7 @@ class TestVariableFromSecrets:
mock_secret_get.return_value = "a_secret_value"
assert Variable.get(key="not_myvar") == "a_secret_value"
+ @conf_vars({("core", "multi_team"): "True"})
@mock.patch.dict(
"os.environ",
{
diff --git
a/airflow-core/tests/unit/always/test_secrets_environment_variables.py
b/airflow-core/tests/unit/always/test_secrets_environment_variables.py
index 9f8e8379efc..200b545bf03 100644
--- a/airflow-core/tests/unit/always/test_secrets_environment_variables.py
+++ b/airflow-core/tests/unit/always/test_secrets_environment_variables.py
@@ -26,6 +26,8 @@ from airflow.secrets.environment_variables import (
EnvironmentVariablesBackend,
)
+from tests_common.test_utils.config import conf_vars
+
# A team specific secret is stored as ``<PREFIX>_<TEAM_NAME>___<SECRET_ID>``.
Team names may contain
# underscores (they are validated against ``^[a-zA-Z0-9_-]{3,50}$``), so both
shapes are exercised.
TEAM_NAMES = ["team_a", "teama"]
@@ -57,6 +59,11 @@ def lookup(env_prefix: str, method: str, secret_id: str,
team_name: str | None)
class TestEnvironmentVariablesBackendTeamScope:
"""A team specific secret must only be resolvable for the team it is
stored for."""
+ @pytest.fixture(autouse=True)
+ def _multi_team_enabled(self):
+ with conf_vars({("core", "multi_team"): "True"}):
+ yield
+
@pytest.mark.parametrize(("env_prefix", "method"), LOOKUPS)
@pytest.mark.parametrize("team_name", TEAM_NAMES)
def test_team_scoped_secret_is_not_resolved_without_a_team_scope(
@@ -186,3 +193,15 @@ class TestEnvironmentVariablesBackendTeamScope:
monkeypatch.delenv(env_prefix + SECRET_ID.upper(), raising=False)
assert lookup(env_prefix, method, SECRET_ID, team_name) is None
+
+
+class TestEnvironmentVariablesBackendMultiTeamDisabled:
+ """No team scoped variable can exist without multi-team mode, so there is
no ambiguity
+ to refuse -- an ordinary id containing the separator must resolve
normally."""
+
+ @pytest.mark.parametrize(("env_prefix", "method"), LOOKUPS)
+ def test_ambiguous_id_resolves_when_multi_team_is_disabled(self,
monkeypatch, env_prefix, method):
+ secret_id = f"prod{TEAM_SEP}{SECRET_ID}"
+ monkeypatch.setenv(env_prefix + secret_id.upper(), GLOBAL_VALUE)
+
+ assert lookup(env_prefix, method, secret_id, None) == GLOBAL_VALUE