Skip to content

Commit 075afe5

Browse files
author
Mehul Goyal
authored
allow impersonation_chain to be set on Google Cloud connection (#33715)
1 parent 3ef770e commit 075afe5

File tree

3 files changed

+45
-3
lines changed

3 files changed

+45
-3
lines changed

β€Žairflow/providers/google/common/hooks/base_google.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ def get_connection_form_widgets() -> dict[str, Any]:
216216
widget=BS3TextFieldWidget(),
217217
default=5,
218218
),
219+
"impersonation_chain": StringField(
220+
lazy_gettext("Impersonation Chain"), widget=BS3TextFieldWidget()
221+
),
219222
}
220223

221224
@staticmethod
@@ -262,6 +265,9 @@ def get_credentials_and_project_id(self) -> tuple[google.auth.credentials.Creden
262265

263266
credential_config_file: str | None = self._get_field("credential_config_file", None)
264267

268+
if not self.impersonation_chain:
269+
self.impersonation_chain = self._get_field("impersonation_chain", None)
270+
265271
target_principal, delegates = _get_target_principal_and_delegates(self.impersonation_chain)
266272

267273
credentials, project_id = get_credentials_and_project_id(

β€Ždocs/apache-airflow-providers-google/connections/gcp.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ Number of Retries
125125
represents the last request. If zero (default), we attempt the
126126
request only once.
127127

128+
Impersonation Chain
129+
Optional service account to impersonate using short-term
130+
credentials, or chained list of accounts required to get the access_token
131+
of the last account in the list, which will be impersonated in all requests leveraging this connection.
132+
If set as a string, the account must grant the originating account
133+
the Service Account Token Creator IAM role.
134+
If set as a sequence, the identities from the list must grant
135+
Service Account Token Creator IAM role to the directly preceding identity, with first
136+
account from the list granting this role to the originating account.
137+
128138
When specifying the connection in environment variable you should specify
129139
it using URI syntax, with the following requirements:
130140

@@ -142,6 +152,7 @@ Number of Retries
142152
* ``scope`` - Scopes
143153
* ``num_retries`` - Number of Retries
144154

155+
145156
Note that all components of the URI should be URL-encoded.
146157

147158
For example, with URI format:
@@ -165,6 +176,8 @@ Google operators support `direct impersonation of a service account
165176
<https://cloud.google.com/iam/docs/understanding-service-accounts#directly_impersonating_a_service_account>`_
166177
via ``impersonation_chain`` argument (``google_impersonation_chain`` in case of operators
167178
that also communicate with services of other cloud providers).
179+
The impersonation chain can also be configured directly on the Google Cloud Connection
180+
as described above, though the ``impersonation_chain`` passed to the operator takes precedence.
168181

169182
For example:
170183

β€Žtests/providers/google/common/hooks/test_base_google.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -661,29 +661,52 @@ def test_authorize_assert_http_timeout_is_present(self, mock_get_credentials):
661661
assert http_authorized.timeout is not None
662662

663663
@pytest.mark.parametrize(
664-
"impersonation_chain, target_principal, delegates",
664+
"impersonation_chain, impersonation_chain_from_conn, target_principal, delegates",
665665
[
666-
pytest.param("ACCOUNT_1", "ACCOUNT_1", None, id="string"),
667-
pytest.param(["ACCOUNT_1"], "ACCOUNT_1", [], id="single_element_list"),
666+
pytest.param("ACCOUNT_1", None, "ACCOUNT_1", None, id="string"),
667+
pytest.param(None, "ACCOUNT_1", "ACCOUNT_1", None, id="string_in_conn"),
668+
pytest.param("ACCOUNT_2", "ACCOUNT_1", "ACCOUNT_2", None, id="string_with_override"),
669+
pytest.param(["ACCOUNT_1"], None, "ACCOUNT_1", [], id="single_element_list"),
670+
pytest.param(None, ["ACCOUNT_1"], "ACCOUNT_1", [], id="single_element_list_in_conn"),
671+
pytest.param(
672+
["ACCOUNT_1"], ["ACCOUNT_2"], "ACCOUNT_1", [], id="single_element_list_with_override"
673+
),
668674
pytest.param(
669675
["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"],
676+
None,
670677
"ACCOUNT_3",
671678
["ACCOUNT_1", "ACCOUNT_2"],
672679
id="multiple_elements_list",
673680
),
681+
pytest.param(
682+
None,
683+
["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"],
684+
"ACCOUNT_3",
685+
["ACCOUNT_1", "ACCOUNT_2"],
686+
id="multiple_elements_list_in_conn",
687+
),
688+
pytest.param(
689+
["ACCOUNT_2", "ACCOUNT_3", "ACCOUNT_4"],
690+
["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"],
691+
"ACCOUNT_4",
692+
["ACCOUNT_2", "ACCOUNT_3"],
693+
id="multiple_elements_list_with_override",
694+
),
674695
],
675696
)
676697
@mock.patch(MODULE_NAME + ".get_credentials_and_project_id")
677698
def test_get_credentials_and_project_id_with_impersonation_chain(
678699
self,
679700
mock_get_creds_and_proj_id,
680701
impersonation_chain,
702+
impersonation_chain_from_conn,
681703
target_principal,
682704
delegates,
683705
):
684706
mock_credentials = mock.MagicMock()
685707
mock_get_creds_and_proj_id.return_value = (mock_credentials, PROJECT_ID)
686708
self.instance.impersonation_chain = impersonation_chain
709+
self.instance.extras = {"impersonation_chain": impersonation_chain_from_conn}
687710
result = self.instance.get_credentials_and_project_id()
688711
mock_get_creds_and_proj_id.assert_called_once_with(
689712
key_path=None,

0 commit comments

Comments
 (0)