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 0b3aaccfa01 Apply the team scope in the Google Secret Manager backend 
(#70869)
0b3aaccfa01 is described below

commit 0b3aaccfa015c69c51c5a68c6469fdfc0d68c16d
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Aug 1 04:07:00 2026 +0200

    Apply the team scope in the Google Secret Manager backend (#70869)
    
    * Apply the team scope in the Google Secret Manager backend
    
    `get_conn_value` and `get_variable` both accept a `team_name`, but the 
helper
    they delegate to, `_get_secret`, has no such parameter, so the argument was
    dropped at the call boundary. The backend built the team agnostic secret 
name in
    every case, and a team scoped lookup resolved whatever the team agnostic 
name
    pointed at.
    
    Thread the team through and resolve the team scoped name first, using the 
same
    `<team><TEAM_SEP><secret id>` convention the other backends use. The team 
scoped
    name is safe by construction: it can only ever build the caller's own 
namespace.
    
    After that lookup misses, an id that already spells out a team scoped name 
is
    refused rather than resolved, because the team agnostic name would otherwise
    land inside some team's namespace.
    
    The id is never parsed to work out which team it names. A team name may 
itself
    contain the separator, so nothing distinguishes team `a` with id `b--c` from
    team `a--b` with id `c`. Comparing the id against the prefix the caller's 
own
    team builds looks equivalent and is not -- a caller in team `a` would match
    `a--b`'s namespace on the prefix and read its secrets.
    
    Generated-by: Claude Opus 5 (1M context) following the guidelines at
    
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
    
    * Apply the team scope in the Google Secret Manager backend
    
    Address review: the underscore normalisation copied from the Azure Key Vault
    backend could manufacture the team separator out of a plain secret id, so a
    caller in one team resolved another team's secret. That backend overrides
    build_path and normalises the whole name; this one inherits the base
    implementation and normalises nothing, so the transplanted naming reopened 
the
    cross-namespace read it was meant to close.
    
    * Log why an ambiguous secret id is refused in the Google backend
    
    A refused id returned None with no trace, which is indistinguishable from
    a secret that simply does not exist. The refusal also applies to
    deployments that use no teams at all, so an operator upgrading with a
    '--' in a connection id needs to be told why the lookup stopped working.
    
    * Use American spelling in the Google secrets backend docs
    
    The docs spellcheck rejects 'normalise'.
    
    * Build the team scope test backend once per test
    
    The store is closed over by reference, so it can be filled after the
    backend exists rather than discarding a throwaway backend built only to
    derive secret names.
    
    * Delete ub2_69231
    
    * Delete ub2_69666
    
    * Delete ub2_69885
    
    * Delete ub2_69908
---
 providers/google/docs/changelog.rst                |  14 ++
 .../google-cloud-secret-manager-backend.rst        |  42 +++++
 .../google/cloud/secrets/secret_manager.py         |  85 ++++++++-
 .../google/cloud/secrets/test_secret_manager.py    | 193 ++++++++++++++++++++-
 4 files changed, 328 insertions(+), 6 deletions(-)

diff --git a/providers/google/docs/changelog.rst 
b/providers/google/docs/changelog.rst
index 6ec953e93dc..450e13becb0 100644
--- a/providers/google/docs/changelog.rst
+++ b/providers/google/docs/changelog.rst
@@ -27,6 +27,20 @@
 Changelog
 ---------
 
+.. note::
+    ``CloudSecretManagerBackend`` now applies the team scope it is given. 
Previously
+    ``get_conn_value`` and ``get_variable`` accepted a ``team_name`` and 
dropped it, so every
+    lookup resolved the team-agnostic secret name regardless of the team.
+
+    In multi-team deployments a team-scoped secret must now be stored under
+    ``{prefix}{sep}{team_name}--{secret_id}``; the team-agnostic name is still 
used as a
+    fallback when no team-scoped secret exists. Secrets that were relied on to 
resolve for a
+    specific team have to be re-stored under the team-scoped name.
+
+    Connection ids and variable keys containing ``--`` are no longer resolved 
in either mode,
+    because a team name may itself contain ``--`` and the resulting name would 
be ambiguous.
+    See :doc:`/secrets-backends/google-cloud-secret-manager-backend` for the 
full convention.
+
 22.3.0
 ......
 
diff --git 
a/providers/google/docs/secrets-backends/google-cloud-secret-manager-backend.rst
 
b/providers/google/docs/secrets-backends/google-cloud-secret-manager-backend.rst
index cce93a1ff8c..7a4328ad3c2 100644
--- 
a/providers/google/docs/secrets-backends/google-cloud-secret-manager-backend.rst
+++ 
b/providers/google/docs/secrets-backends/google-cloud-secret-manager-backend.rst
@@ -196,6 +196,48 @@ To check the variables is correctly read from the backend 
secret, you can use ``
     $ airflow variables get first-variable
     secret_content
 
+Multi-team lookup
+=================
+
+In multi-team mode this backend looks for a team-scoped secret first and falls 
back to the
+team-agnostic name when no team-scoped secret exists.
+
+For connections:
+
+* Team-scoped: ``{connections_prefix}{sep}{team_name}--{conn_id}``
+* Team-agnostic fallback: ``{connections_prefix}{sep}{conn_id}``
+
+For variables:
+
+* Team-scoped: ``{variables_prefix}{sep}{team_name}--{key}``
+* Team-agnostic fallback: ``{variables_prefix}{sep}{key}``
+
+The team name and the secret id are separated by ``--``, while ``sep`` 
(default ``-``) separates
+the prefix from what follows. So with 
``connections_prefix="airflow-connections"``,
+``team_name="team_a"`` and ``conn_id="smtp_default"``, the backend looks up
+``airflow-connections-team_a--smtp_default`` before falling back to
+``airflow-connections-smtp_default``.
+
+.. note::
+
+    Unlike the Azure Key Vault backend, this backend does **not** normalize 
underscores to the
+    separator. The secret id keeps the exact form you request it under, in the 
team-scoped name
+    as well as the team-agnostic one.
+
+Because a team name may itself contain ``--``, an id that contains ``--`` is 
ambiguous — team
+``team_a`` with conn_id ``prod--db`` and team ``team_a--prod`` with conn_id 
``db`` would name the
+same secret. Such an id is therefore never resolved, for team-scoped and 
team-agnostic lookups
+alike, and the backend never parses an id to infer which team it belongs to. A 
warning is logged
+whenever an id is refused for this reason, so the resulting ``None`` is not 
mistaken for a missing
+secret. Avoid ``--`` in connection ids and variable keys.
+
+.. warning::
+
+    This refusal applies whether or not you use teams. If you already store a 
connection or
+    variable whose id contains ``--``, it stops resolving after upgrading and 
you must rename it.
+
+``get_config`` is not team-scoped and is unaffected by any of the above.
+
 Clean up
 ========
 
diff --git 
a/providers/google/src/airflow/providers/google/cloud/secrets/secret_manager.py 
b/providers/google/src/airflow/providers/google/cloud/secrets/secret_manager.py
index 2b505499124..8966e1372ae 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/secrets/secret_manager.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/secrets/secret_manager.py
@@ -37,6 +37,10 @@ log = logging.getLogger(__name__)
 
 SECRET_ID_PATTERN = r"^[a-zA-Z0-9-_]*$"
 
+# Separator between the team name and the secret id in a team scoped secret 
name.
+# Matches the convention the other secrets backends use.
+TEAM_SEP = "--"
+
 
 class CloudSecretManagerBackend(BaseSecretsBackend, LoggingMixin):
     """
@@ -166,7 +170,11 @@ class CloudSecretManagerBackend(BaseSecretsBackend, 
LoggingMixin):
         if self.connections_prefix is None:
             return None
 
-        return self._get_secret(self.connections_prefix, conn_id)
+        if self._names_a_team_namespace(conn_id):
+            self._log_refusal("connection", conn_id)
+            return None
+
+        return self._get_secret(self.connections_prefix, conn_id, team_name)
 
     def get_variable(self, key: str, team_name: str | None = None) -> str | 
None:
         """
@@ -179,7 +187,11 @@ class CloudSecretManagerBackend(BaseSecretsBackend, 
LoggingMixin):
         if self.variables_prefix is None:
             return None
 
-        return self._get_secret(self.variables_prefix, key)
+        if self._names_a_team_namespace(key):
+            self._log_refusal("variable", key)
+            return None
+
+        return self._get_secret(self.variables_prefix, key, team_name)
 
     def get_config(self, key: str) -> str | None:
         """
@@ -193,12 +205,75 @@ class CloudSecretManagerBackend(BaseSecretsBackend, 
LoggingMixin):
 
         return self._get_secret(self.config_prefix, key)
 
-    def _get_secret(self, path_prefix: str, secret_id: str) -> str | None:
+    def _log_refusal(self, kind: str, secret_id: str) -> None:
+        self.log.warning(
+            "%s id %r contains %r, which separates the team name from the 
secret id in a team "
+            "scoped secret name. Such an id is ambiguous and is not looked up. 
Returning None.",
+            kind.capitalize(),
+            secret_id,
+            TEAM_SEP,
+        )
+
+    def _build_team_secret_name(self, path_prefix: str, team_name: str, 
secret_id: str) -> str:
+        """
+        Build a team scoped secret name using a dedicated separator before the 
secret id.
+
+        The secret id is used verbatim. Normalizing it (``_`` -> ``sep``, as 
the Azure Key
+        Vault backend does) would let a plain id manufacture ``TEAM_SEP``: 
team ``a`` asking
+        for ``b__c`` would build the same name as team ``a--b`` asking for 
``c``. It would
+        also contradict this backend's naming, which keeps underscores 
everywhere else --
+        unlike Azure, this backend does not override :meth:`build_path`.
+        """
+        team_prefix = self.build_path(path_prefix, team_name, self.sep)
+        return f"{team_prefix}{TEAM_SEP}{secret_id}"
+
+    def _names_a_team_namespace(self, secret_id: str) -> bool:
+        """
+        Whether ``secret_id`` spells out a team scoped secret name.
+
+        A team scoped secret is named ``<prefix><sep><team><TEAM_SEP><secret 
id>``, so an id
+        that itself contains the team separator makes the built name 
ambiguous: team ``a``
+        with id ``b--c`` and team ``a--b`` with id ``c`` produce the same 
string. Such an id
+        is refused for *every* lookup -- team scoped as well as team agnostic 
-- because the
+        ambiguity exists in both directions and the caller's own namespace is 
not a safe
+        harbor for it.
+
+        The id is never parsed to work out *which* team it names, because it 
cannot be:
+        nothing in the string distinguishes the two readings above. Comparing 
the id against
+        the prefix the caller's own team builds looks equivalent and is not -- 
a caller in
+        team ``a`` would match ``a--b``'s namespace on the prefix and read its 
secrets. Only
+        the caller's own namespace is ever constructed, never parsed.
+
+        The raw id is matched. Routing it through :meth:`build_path` first, as 
the Azure
+        backend does, is wrong here: the inherited implementation prepends a 
separator to an
+        empty prefix (``'' -> '-smtp_default'``) and normalizes nothing, so 
the guard would
+        both mis-anchor and miss ids whose separator only appears after 
normalization.
+        """
+        return TEAM_SEP in secret_id
+
+    def _get_secret(self, path_prefix: str, secret_id: str, team_name: str | 
None = None) -> str | None:
         """
         Get secret value from the SecretManager based on prefix.
 
         :param path_prefix: Prefix for the Path to get Secret
         :param secret_id: Secret Key
+        :param team_name: Team the lookup is scoped to (if any)
         """
-        secret_id = self.build_path(path_prefix, secret_id, self.sep)
-        return self.client.get_secret(secret_id=secret_id, 
project_id=self.project_id)
+        # ``self.client`` builds a new ``_SecretManagerClient`` -- and with it 
a real gRPC
+        # client -- on every access, so it is bound once for both lookups 
below.
+        client = self.client
+
+        # The team scoped name is tried first and is safe by construction: it 
can only ever
+        # build the caller's own namespace. Ids that would make that name 
ambiguous are
+        # refused by the callers before reaching here.
+        if team_name:
+            team_secret = client.get_secret(
+                secret_id=self._build_team_secret_name(path_prefix, team_name, 
secret_id),
+                project_id=self.project_id,
+            )
+            if team_secret is not None:
+                return team_secret
+
+        return client.get_secret(
+            secret_id=self.build_path(path_prefix, secret_id, self.sep), 
project_id=self.project_id
+        )
diff --git 
a/providers/google/tests/unit/google/cloud/secrets/test_secret_manager.py 
b/providers/google/tests/unit/google/cloud/secrets/test_secret_manager.py
index 7d2d381cf7c..090f46b150e 100644
--- a/providers/google/tests/unit/google/cloud/secrets/test_secret_manager.py
+++ b/providers/google/tests/unit/google/cloud/secrets/test_secret_manager.py
@@ -26,7 +26,7 @@ from google.cloud.secretmanager_v1.types import 
AccessSecretVersionResponse
 
 from airflow.models import Connection
 from airflow.providers.common.compat.sdk import AirflowException
-from airflow.providers.google.cloud.secrets.secret_manager import 
CloudSecretManagerBackend
+from airflow.providers.google.cloud.secrets.secret_manager import TEAM_SEP, 
CloudSecretManagerBackend
 
 CREDENTIALS = "test-creds"
 KEY_FILE = "test-file.json"
@@ -34,6 +34,7 @@ PROJECT_ID = "test-project-id"
 OVERRIDDEN_PROJECT_ID = "overridden-test-project-id"
 CONNECTIONS_PREFIX = "test-connections"
 VARIABLES_PREFIX = "test-variables"
+CONFIG_PREFIX = "test-config"
 SEP = "-"
 CONN_ID = "test-postgres"
 CONN_URI = "postgresql://airflow:airflow@host:5432/airflow"
@@ -272,3 +273,193 @@ class TestCloudSecretManagerBackend:
 
         backend = CloudSecretManagerBackend(project_id="explicit-project")
         assert backend.project_id == "explicit-project"
+
+
+class TestCloudSecretManagerBackendTeamScope:
+    """A team scoped secret must only be resolvable for the team it is stored 
for."""
+
+    TEAM = "team_a"
+    OTHER_TEAM = "team_b"
+    # A team name may itself contain the team separator, so one team's 
namespace can start
+    # with another's. Treating a prefix match as ownership is what this guards 
against.
+    EXTENDING_TEAM = "team_a--prod"
+
+    @staticmethod
+    def _backend(mock_client_callable):
+        """
+        Build a backend over an in-memory secret store keyed by full secret 
name.
+
+        The store is returned rather than taken as an argument so tests can 
populate it after
+        building the backend -- the lookup closes over it by reference. Names 
are derived from
+        the backend, so it has to exist first.
+        """
+        store: dict[str, str] = {}
+        client = mock.MagicMock()
+        client.get_secret.side_effect = lambda secret_id, project_id, 
**kwargs: store.get(secret_id)
+        mock_client_callable.return_value = client
+        backend = CloudSecretManagerBackend(
+            connections_prefix=CONNECTIONS_PREFIX,
+            variables_prefix=VARIABLES_PREFIX,
+            config_prefix=CONFIG_PREFIX,
+            project_id=PROJECT_ID,
+        )
+        return backend, store
+
+    @staticmethod
+    def _team_name(backend, team, secret_id, prefix=CONNECTIONS_PREFIX):
+        return backend._build_team_secret_name(prefix, team, secret_id)
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_scoped_secret_is_resolved_for_its_own_team(self, 
mock_client, mock_get_creds):
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        store[self._team_name(backend, self.TEAM, CONN_ID)] = CONN_URI
+
+        assert backend.get_conn_value(conn_id=CONN_ID, team_name=self.TEAM) == 
CONN_URI
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_scoped_secret_is_not_resolved_for_another_team(self, 
mock_client, mock_get_creds):
+        """The whole point: team B must not reach team A's secret by spelling 
out its name."""
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        encoded = f"{self.TEAM}--{CONN_ID}"
+        store[self._team_name(backend, self.TEAM, CONN_ID)] = CONN_URI
+
+        assert backend.get_conn_value(conn_id=encoded, 
team_name=self.OTHER_TEAM) is None
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_scoped_secret_is_not_resolved_without_a_team_scope(self, 
mock_client, mock_get_creds):
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        encoded = f"{self.TEAM}--{CONN_ID}"
+        store[self._team_name(backend, self.TEAM, CONN_ID)] = CONN_URI
+
+        assert backend.get_conn_value(conn_id=encoded) is None
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_whose_name_extends_the_callers_is_not_readable(self, 
mock_client, mock_get_creds):
+        """A prefix match on the caller's namespace is not proof of 
ownership."""
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        store[self._team_name(backend, self.EXTENDING_TEAM, CONN_ID)] = 
CONN_URI
+        encoded = f"{self.EXTENDING_TEAM}--{CONN_ID}"
+
+        assert backend.get_conn_value(conn_id=encoded, team_name=self.TEAM) is 
None
+        # and the owning team still reaches it normally, with the bare id
+        assert backend.get_conn_value(conn_id=CONN_ID, 
team_name=self.EXTENDING_TEAM) == CONN_URI
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_agnostic_secret_is_resolved_for_any_team_scope(self, 
mock_client, mock_get_creds):
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        store[backend.build_path(CONNECTIONS_PREFIX, CONN_ID, SEP)] = CONN_URI
+
+        assert backend.get_conn_value(conn_id=CONN_ID) == CONN_URI
+        assert backend.get_conn_value(conn_id=CONN_ID, team_name=self.TEAM) == 
CONN_URI
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_scoped_variable_is_not_resolved_for_another_team(self, 
mock_client, mock_get_creds):
+        """Variables share the lookup, so they must be scoped identically."""
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        encoded = f"{self.TEAM}--{VAR_KEY}"
+        store[self._team_name(backend, self.TEAM, VAR_KEY, VARIABLES_PREFIX)] 
= VAR_VALUE
+
+        assert backend.get_variable(key=VAR_KEY, team_name=self.TEAM) == 
VAR_VALUE
+        assert backend.get_variable(key=encoded, team_name=self.OTHER_TEAM) is 
None
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_team_secret_name_is_built_literally(self, mock_client, 
mock_get_creds):
+        """
+        Pin the literal name rather than deriving it from the method under 
test.
+
+        Every other test in this class keys its store with 
``_build_team_secret_name``, so it
+        agrees with the implementation by construction and cannot see a naming 
defect.
+        """
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, _ = self._backend(mock_client)
+
+        assert (
+            backend._build_team_secret_name(CONNECTIONS_PREFIX, "team_a", 
"test_postgres")
+            == f"{CONNECTIONS_PREFIX}{SEP}team_a{TEAM_SEP}test_postgres"
+        )
+        # Underscores survive in the secret id, matching the team agnostic 
name this backend
+        # documents -- unlike Azure, nothing here normalises ``_`` to the 
separator.
+        assert (
+            backend._build_team_secret_name(CONNECTIONS_PREFIX, "team_a", 
"smtp_default")
+            == f"{CONNECTIONS_PREFIX}{SEP}team_a{TEAM_SEP}smtp_default"
+        )
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_underscores_cannot_manufacture_a_team_namespace(self, 
mock_client, mock_get_creds):
+        """
+        An id containing ``__`` must not resolve into a team whose name ends 
with that segment.
+
+        Normalising ``_`` to the separator when building the team scoped name 
(as the Azure
+        backend does) made team ``team_a`` asking for ``prod__conn`` build 
exactly the name
+        team ``team_a--prod`` stores ``conn`` under.
+        """
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        victim = backend._build_team_secret_name(CONNECTIONS_PREFIX, 
self.EXTENDING_TEAM, CONN_ID)
+        store[victim] = CONN_URI
+
+        assert backend.get_conn_value(conn_id=f"prod__{CONN_ID}", 
team_name=self.TEAM) is None
+        # the two names must not coincide in the first place
+        assert backend._build_team_secret_name(CONNECTIONS_PREFIX, self.TEAM, 
f"prod__{CONN_ID}") != victim
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_ambiguous_id_is_refused_even_for_its_own_team(self, mock_client, 
mock_get_creds):
+        """
+        An id containing the separator is ambiguous in both directions, so it 
is never resolved.
+
+        ``team_a`` with id ``b--c`` and ``team_a--b`` with id ``c`` name the 
same secret, so
+        honouring the id for its own team would still hand one team the 
other's secret.
+        """
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        ambiguous = f"prod{TEAM_SEP}{CONN_ID}"
+        store[backend._build_team_secret_name(CONNECTIONS_PREFIX, self.TEAM, 
ambiguous)] = CONN_URI
+
+        assert backend.get_conn_value(conn_id=ambiguous, team_name=self.TEAM) 
is None
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_refusing_an_ambiguous_id_is_logged(self, mock_client, 
mock_get_creds, caplog):
+        """A silent ``None`` is indistinguishable from a missing secret, so 
the refusal is logged."""
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, _ = self._backend(mock_client)
+        ambiguous = f"prod{TEAM_SEP}{CONN_ID}"
+
+        assert backend.get_conn_value(conn_id=ambiguous) is None
+        assert backend.get_variable(key=ambiguous) is None
+
+        refusals = [r for r in caplog.records if "is ambiguous and is not 
looked up" in r.getMessage()]
+        assert len(refusals) == 2
+        assert all(r.levelname == "WARNING" for r in refusals)
+        assert all(ambiguous in r.getMessage() for r in refusals)
+
+    @mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
+    @mock.patch(MODULE_NAME + "._SecretManagerClient")
+    def test_config_lookup_is_not_team_scoped(self, mock_client, 
mock_get_creds):
+        """
+        ``get_config`` has no team scope, so the namespace guard must not 
apply to it.
+
+        Refusing config keys containing the separator would be stricter than 
the Azure backend
+        this convention comes from, and there is no team namespace for a 
config key to invade.
+        """
+        mock_get_creds.return_value = CREDENTIALS, PROJECT_ID
+        backend, store = self._backend(mock_client)
+        key = f"some{TEAM_SEP}option"
+        store[backend.build_path(CONFIG_PREFIX, key, SEP)] = "config-value"
+
+        assert backend.get_config(key) == "config-value"

Reply via email to