-
Notifications
You must be signed in to change notification settings - Fork 17.6k
Only refuse team scoped like secret ids when multi_team is on #71078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ | |
|
|
||
| from google.auth.exceptions import DefaultCredentialsError | ||
|
|
||
| from airflow.providers.common.compat.sdk import AirflowException | ||
| from airflow.providers.common.compat.sdk import AirflowException, conf | ||
| from airflow.providers.google.cloud._internal_client.secret_manager_client import _SecretManagerClient | ||
| from airflow.providers.google.cloud.utils.credentials_provider import ( | ||
| _get_target_principal_and_delegates, | ||
|
|
@@ -248,7 +248,12 @@ def _names_a_team_namespace(self, secret_id: str) -> bool: | |
| 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. | ||
|
|
||
| Only checked in multi-team mode: ``team_name`` is never non-``None`` otherwise, so no | ||
| team scoped secret can exist to collide with. | ||
| """ | ||
| if not conf.getboolean("core", "multi_team", fallback=False): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Google backend docs still say the opposite.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, here you go: #71106 |
||
| return False | ||
| return TEAM_SEP in secret_id | ||
|
|
||
| def _get_secret(self, path_prefix: str, secret_id: str, team_name: str | None = None) -> str | None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| from azure.identity import ClientSecretCredential, DefaultAzureCredential | ||
| from azure.keyvault.secrets import SecretClient | ||
|
|
||
| from airflow.providers.common.compat.sdk import conf | ||
| from airflow.providers.microsoft.azure.utils import get_sync_default_azure_credential | ||
| from airflow.secrets import BaseSecretsBackend | ||
| from airflow.utils.log.logging_mixin import LoggingMixin | ||
|
|
@@ -247,7 +248,12 @@ def _names_a_team_namespace(self, secret_id: str) -> bool: | |
| The id is normalised first because :meth:`build_path` maps ``_`` onto the separator | ||
| everywhere in this backend, so ``b__c`` reaches Key Vault as ``b--c`` and would | ||
| otherwise manufacture the team separator from an id that does not visibly contain it. | ||
|
|
||
| Only checked in multi-team mode: ``team_name`` is never non-``None`` otherwise, so no | ||
| team scoped secret can exist to collide with. | ||
| """ | ||
| if not conf.getboolean("core", "multi_team", fallback=False): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| return False | ||
| return TEAM_SEP in self.build_path("", secret_id, self.sep) | ||
|
|
||
| def _log_refusal(self, kind: str, secret_id: str) -> None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,10 @@ | |
|
|
||
| from airflow.providers.microsoft.azure.secrets.key_vault import AzureKeyVaultBackend | ||
|
|
||
| from tests_common.test_utils.config import conf_vars | ||
|
|
||
| KEY_VAULT_MODULE = "airflow.providers.microsoft.azure.secrets.key_vault" | ||
| multi_team_enabled = conf_vars({("core", "multi_team"): "True"}) | ||
|
|
||
|
|
||
| class TestAzureKeyVaultBackend: | ||
|
|
@@ -107,13 +110,15 @@ def test_get_variable_uses_team_secret_with_custom_prefix(self, mock_client): | |
| assert secret_val == "team-value" | ||
| mock_client.get_secret.assert_called_once_with(name="custom-variables-team-a--hello") | ||
|
|
||
| @multi_team_enabled | ||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_get_variable_returns_none_for_team_scoped_key_without_team_name(self, mock_client): | ||
| backend = AzureKeyVaultBackend() | ||
|
|
||
| assert backend.get_variable("teama--hello") is None | ||
| mock_client.get_secret.assert_not_called() | ||
|
|
||
| @multi_team_enabled | ||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_another_teams_secret_is_not_reachable(self, mock_client): | ||
| """A caller scoped to one team must not reach another team's secret by naming it. | ||
|
|
@@ -133,6 +138,7 @@ def only_the_target_exists(name): | |
|
|
||
| assert backend.get_conn_value("teama--my_db", team_name="teamb") is None | ||
|
|
||
| @multi_team_enabled | ||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_team_whose_name_extends_the_callers_is_not_reachable(self, mock_client): | ||
| """A prefix match on the caller's own namespace is not proof of ownership. | ||
|
|
@@ -153,6 +159,7 @@ def only_the_target_exists(name): | |
|
|
||
| assert backend.get_conn_value("teama--prod--my_db", team_name="teama") is None | ||
|
|
||
| @multi_team_enabled | ||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_team_scoped_lookup_cannot_reach_a_longer_teams_namespace(self, mock_client): | ||
| """The team scoped name is not safe by construction -- the id can extend it. | ||
|
|
@@ -174,6 +181,7 @@ def only_the_target_exists(name): | |
|
|
||
| assert backend.get_conn_value("prod--my_db", team_name="teama") is None | ||
|
|
||
| @multi_team_enabled | ||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_underscores_cannot_manufacture_a_team_namespace(self, mock_client): | ||
| """``build_path`` maps ``_`` onto the separator, so ``__`` becomes the team separator. | ||
|
|
@@ -193,6 +201,7 @@ def only_the_target_exists(name): | |
|
|
||
| assert backend.get_conn_value("prod__my_db", team_name="teama") is None | ||
|
|
||
| @multi_team_enabled | ||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_refusing_an_ambiguous_id_is_logged(self, mock_client, caplog): | ||
| """A silent ``None`` is indistinguishable from a missing secret, so the refusal is logged.""" | ||
|
|
@@ -215,6 +224,17 @@ def test_refusing_an_ambiguous_id_is_logged(self, mock_client, caplog): | |
| assert sum(refused_id in r.getMessage() for r in refusals) == 1 | ||
| mock_client.get_secret.assert_not_called() | ||
|
|
||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend.client") | ||
| def test_ambiguous_id_resolves_when_multi_team_is_disabled(self, mock_client): | ||
| """No team scoped secret can exist without multi-team mode, so there is no ambiguity | ||
| to refuse -- an ordinary id containing the separator must resolve normally.""" | ||
| mock_client.get_secret.return_value = mock.Mock(value="world") | ||
| backend = AzureKeyVaultBackend() | ||
|
|
||
| assert backend.get_conn_value("prod--my_db") == "world" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in #71106 |
||
| assert backend.get_variable("prod__hello") == "world" | ||
| assert backend.get_config("prod--sql_alchemy_conn") == "world" | ||
|
|
||
| @mock.patch(f"{KEY_VAULT_MODULE}.AzureKeyVaultBackend._get_secret") | ||
| def test_variable_prefix_none_value(self, mock_get_secret): | ||
| """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other four test files in this PR define
multi_team_enabled = conf_vars({("core", "multi_team"): "True"})at module level. Worth doing the same here rather than repeating the literal on six tests.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handled in #71106