apache-airflow-providers-hashicorp 4.3.0rc1__tar.gz → 4.3.1rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of apache-airflow-providers-hashicorp might be problematic. Click here for more details.

Files changed (35) hide show
  1. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/PKG-INFO +8 -9
  2. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/README.rst +4 -4
  3. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/changelog.rst +17 -0
  4. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/index.rst +3 -3
  5. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/provider.yaml +2 -1
  6. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/pyproject.toml +4 -5
  7. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/__init__.py +1 -1
  8. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/_internal_client/vault_client.py +40 -3
  9. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/hooks/vault.py +5 -1
  10. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/_internal_client/test_vault_client.py +161 -28
  11. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/hooks/test_vault.py +70 -19
  12. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/.latest-doc-only-change.txt +0 -0
  13. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/commits.rst +0 -0
  14. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/conf.py +0 -0
  15. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/connections/vault.rst +0 -0
  16. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/installing-providers-from-sources.rst +0 -0
  17. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/integration-logos/Hashicorp-Vault.png +0 -0
  18. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/redirects.txt +0 -0
  19. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/secrets-backends/hashicorp-vault.rst +0 -0
  20. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/docs/security.rst +0 -0
  21. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/__init__.py +0 -0
  22. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/__init__.py +0 -0
  23. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/LICENSE +0 -0
  24. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/_internal_client/__init__.py +0 -0
  25. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/get_provider_info.py +0 -0
  26. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/hooks/__init__.py +0 -0
  27. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/secrets/__init__.py +0 -0
  28. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/src/airflow/providers/hashicorp/secrets/vault.py +0 -0
  29. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/conftest.py +0 -0
  30. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/__init__.py +0 -0
  31. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/__init__.py +0 -0
  32. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/_internal_client/__init__.py +0 -0
  33. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/hooks/__init__.py +0 -0
  34. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/secrets/__init__.py +0 -0
  35. {apache_airflow_providers_hashicorp-4.3.0rc1 → apache_airflow_providers_hashicorp-4.3.1rc1}/tests/unit/hashicorp/secrets/test_vault.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apache-airflow-providers-hashicorp
3
- Version: 4.3.0rc1
3
+ Version: 4.3.1rc1
4
4
  Summary: Provider package apache-airflow-providers-hashicorp for Apache Airflow
5
5
  Keywords: airflow-provider,hashicorp,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
7
7
  Maintainer-email: Apache Software Foundation <dev@airflow.apache.org>
8
- Requires-Python: ~=3.9
8
+ Requires-Python: ~=3.10
9
9
  Description-Content-Type: text/x-rst
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: System Administrators
15
15
  Classifier: Framework :: Apache Airflow
16
16
  Classifier: Framework :: Apache Airflow :: Provider
17
17
  Classifier: License :: OSI Approved :: Apache Software License
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
@@ -25,8 +24,8 @@ Requires-Dist: hvac>=1.1.0
25
24
  Requires-Dist: boto3>=1.33.0 ; extra == "boto3"
26
25
  Requires-Dist: apache-airflow-providers-google ; extra == "google"
27
26
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
28
- Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0/changelog.html
29
- Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0
27
+ Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1/changelog.html
28
+ Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1
30
29
  Project-URL: Mastodon, https://fosstodon.org/@airflow
31
30
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
32
31
  Project-URL: Source Code, https://github.com/apache/airflow
@@ -59,7 +58,7 @@ Provides-Extra: google
59
58
 
60
59
  Package ``apache-airflow-providers-hashicorp``
61
60
 
62
- Release: ``4.3.0``
61
+ Release: ``4.3.1``
63
62
 
64
63
 
65
64
  Hashicorp including `Hashicorp Vault <https://www.vaultproject.io/>`__
@@ -72,7 +71,7 @@ This is a provider package for ``hashicorp`` provider. All classes for this prov
72
71
  are in ``airflow.providers.hashicorp`` python package.
73
72
 
74
73
  You can find package information and changelog for the provider
75
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0/>`_.
74
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1/>`_.
76
75
 
77
76
  Installation
78
77
  ------------
@@ -81,7 +80,7 @@ You can install this package on top of an existing Airflow 2 installation (see `
81
80
  for the minimum Airflow version supported) via
82
81
  ``pip install apache-airflow-providers-hashicorp``
83
82
 
84
- The package supports the following python versions: 3.9,3.10,3.11,3.12
83
+ The package supports the following python versions: 3.10,3.11,3.12
85
84
 
86
85
  Requirements
87
86
  ------------
@@ -113,5 +112,5 @@ Dependent package
113
112
  ==================================================================================================== ==========
114
113
 
115
114
  The changelog for the provider package can be found in the
116
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0/changelog.html>`_.
115
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1/changelog.html>`_.
117
116
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  Package ``apache-airflow-providers-hashicorp``
25
25
 
26
- Release: ``4.3.0``
26
+ Release: ``4.3.1``
27
27
 
28
28
 
29
29
  Hashicorp including `Hashicorp Vault <https://www.vaultproject.io/>`__
@@ -36,7 +36,7 @@ This is a provider package for ``hashicorp`` provider. All classes for this prov
36
36
  are in ``airflow.providers.hashicorp`` python package.
37
37
 
38
38
  You can find package information and changelog for the provider
39
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0/>`_.
39
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1/>`_.
40
40
 
41
41
  Installation
42
42
  ------------
@@ -45,7 +45,7 @@ You can install this package on top of an existing Airflow 2 installation (see `
45
45
  for the minimum Airflow version supported) via
46
46
  ``pip install apache-airflow-providers-hashicorp``
47
47
 
48
- The package supports the following python versions: 3.9,3.10,3.11,3.12
48
+ The package supports the following python versions: 3.10,3.11,3.12
49
49
 
50
50
  Requirements
51
51
  ------------
@@ -77,4 +77,4 @@ Dependent package
77
77
  ==================================================================================================== ==========
78
78
 
79
79
  The changelog for the provider package can be found in the
80
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0/changelog.html>`_.
80
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1/changelog.html>`_.
@@ -27,6 +27,23 @@
27
27
  Changelog
28
28
  ---------
29
29
 
30
+ 4.3.1
31
+ .....
32
+
33
+ Bug Fixes
34
+ ~~~~~~~~~
35
+
36
+ * ``Fix gcp auth in hashicorp vault provider. (#51991)``
37
+
38
+ Misc
39
+ ~~~~
40
+
41
+ * ``Move 'BaseHook' implementation to task SDK (#51873)``
42
+ * ``Drop support for Python 3.9 (#52072)``
43
+
44
+ .. Below changes are excluded from the changelog. Move them to
45
+ appropriate section above if needed. Do not delete the lines(!):
46
+
30
47
  4.3.0
31
48
  .....
32
49
 
@@ -69,7 +69,7 @@ apache-airflow-providers-hashicorp package
69
69
  Hashicorp including `Hashicorp Vault <https://www.vaultproject.io/>`__
70
70
 
71
71
 
72
- Release: 4.3.0
72
+ Release: 4.3.1
73
73
 
74
74
  Provider package
75
75
  ----------------
@@ -121,5 +121,5 @@ Downloading official packages
121
121
  You can download officially released packages and verify their checksums and signatures from the
122
122
  `Official Apache Download site <https://downloads.apache.org/airflow/providers/>`_
123
123
 
124
- * `The apache-airflow-providers-hashicorp 4.3.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.0.tar.gz.sha512>`__)
125
- * `The apache-airflow-providers-hashicorp 4.3.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.0-py3-none-any.whl.sha512>`__)
124
+ * `The apache-airflow-providers-hashicorp 4.3.1 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.1.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.1.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.1.tar.gz.sha512>`__)
125
+ * `The apache-airflow-providers-hashicorp 4.3.1 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.1-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.1-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_hashicorp-4.3.1-py3-none-any.whl.sha512>`__)
@@ -22,12 +22,13 @@ description: |
22
22
  Hashicorp including `Hashicorp Vault <https://www.vaultproject.io/>`__
23
23
 
24
24
  state: ready
25
- source-date-epoch: 1749896774
25
+ source-date-epoch: 1751473545
26
26
  # Note that those versions are maintained by release manager - do not update them manually
27
27
  # with the exception of case where other provider in sources has >= new provider version.
28
28
  # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have
29
29
  # to be done in the same PR
30
30
  versions:
31
+ - 4.3.1
31
32
  - 4.3.0
32
33
  - 4.2.0
33
34
  - 4.1.1
@@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi"
25
25
 
26
26
  [project]
27
27
  name = "apache-airflow-providers-hashicorp"
28
- version = "4.3.0rc1"
28
+ version = "4.3.1rc1"
29
29
  description = "Provider package apache-airflow-providers-hashicorp for Apache Airflow"
30
30
  readme = "README.rst"
31
31
  authors = [
@@ -44,13 +44,12 @@ classifiers = [
44
44
  "Framework :: Apache Airflow",
45
45
  "Framework :: Apache Airflow :: Provider",
46
46
  "License :: OSI Approved :: Apache Software License",
47
- "Programming Language :: Python :: 3.9",
48
47
  "Programming Language :: Python :: 3.10",
49
48
  "Programming Language :: Python :: 3.11",
50
49
  "Programming Language :: Python :: 3.12",
51
50
  "Topic :: System :: Monitoring",
52
51
  ]
53
- requires-python = "~=3.9"
52
+ requires-python = "~=3.10"
54
53
 
55
54
  # The dependencies should be modified in place in the generated file.
56
55
  # Any change in the dependencies is preserved when the file is regenerated
@@ -107,8 +106,8 @@ apache-airflow-providers-common-sql = {workspace = true}
107
106
  apache-airflow-providers-standard = {workspace = true}
108
107
 
109
108
  [project.urls]
110
- "Documentation" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0"
111
- "Changelog" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.0/changelog.html"
109
+ "Documentation" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1"
110
+ "Changelog" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-hashicorp/4.3.1/changelog.html"
112
111
  "Bug Tracker" = "https://github.com/apache/airflow/issues"
113
112
  "Source Code" = "https://github.com/apache/airflow"
114
113
  "Slack Chat" = "https://s.apache.org/airflow-slack"
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "4.3.0"
32
+ __version__ = "4.3.1"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.10.0"
@@ -153,6 +153,15 @@ class _VaultClient(LoggingMixin):
153
153
  raise VaultError("The 'radius' authentication type requires 'radius_host'")
154
154
  if not radius_secret:
155
155
  raise VaultError("The 'radius' authentication type requires 'radius_secret'")
156
+ if auth_type == "gcp":
157
+ if not gcp_scopes:
158
+ raise VaultError("The 'gcp' authentication type requires 'gcp_scopes'")
159
+ if not role_id:
160
+ raise VaultError("The 'gcp' authentication type requires 'role_id'")
161
+ if not gcp_key_path and not gcp_keyfile_dict:
162
+ raise VaultError(
163
+ "The 'gcp' authentication type requires 'gcp_key_path' or 'gcp_keyfile_dict'"
164
+ )
156
165
 
157
166
  self.kv_engine_version = kv_engine_version or 2
158
167
  self.url = url
@@ -303,13 +312,41 @@ class _VaultClient(LoggingMixin):
303
312
  )
304
313
 
305
314
  scopes = _get_scopes(self.gcp_scopes)
306
- credentials, _ = get_credentials_and_project_id(
315
+ credentials, project_id = get_credentials_and_project_id(
307
316
  key_path=self.gcp_key_path, keyfile_dict=self.gcp_keyfile_dict, scopes=scopes
308
317
  )
318
+
319
+ import json
320
+ import time
321
+
322
+ import googleapiclient
323
+
324
+ if self.gcp_keyfile_dict:
325
+ creds = self.gcp_keyfile_dict
326
+ elif self.gcp_key_path:
327
+ with open(self.gcp_key_path) as f:
328
+ creds = json.load(f)
329
+
330
+ service_account = creds["client_email"]
331
+
332
+ # Generate a payload for subsequent "signJwt()" call
333
+ # Reference: https://googleapis.dev/python/google-auth/latest/reference/google.auth.jwt.html#google.auth.jwt.Credentials
334
+ now = int(time.time())
335
+ expires = now + 900 # 15 mins in seconds, can't be longer.
336
+ payload = {"iat": now, "exp": expires, "sub": credentials, "aud": f"vault/{self.role_id}"}
337
+ body = {"payload": json.dumps(payload)}
338
+ name = f"projects/{project_id}/serviceAccounts/{service_account}"
339
+
340
+ # Perform the GCP API call
341
+ iam = googleapiclient.discovery.build("iam", "v1", credentials=credentials)
342
+ request = iam.projects().serviceAccounts().signJwt(name=name, body=body)
343
+ resp = request.execute()
344
+ jwt = resp["signedJwt"]
345
+
309
346
  if self.auth_mount_point:
310
- _client.auth.gcp.configure(credentials=credentials, mount_point=self.auth_mount_point)
347
+ _client.auth.gcp.login(role=self.role_id, jwt=jwt, mount_point=self.auth_mount_point)
311
348
  else:
312
- _client.auth.gcp.configure(credentials=credentials)
349
+ _client.auth.gcp.login(role=self.role_id, jwt=jwt)
313
350
 
314
351
  def _auth_azure(self, _client: hvac.Client) -> None:
315
352
  if self.auth_mount_point:
@@ -23,12 +23,16 @@ from typing import TYPE_CHECKING, Any
23
23
 
24
24
  from hvac.exceptions import VaultError
25
25
 
26
- from airflow.hooks.base import BaseHook
27
26
  from airflow.providers.hashicorp._internal_client.vault_client import (
28
27
  DEFAULT_KUBERNETES_JWT_PATH,
29
28
  DEFAULT_KV_ENGINE_VERSION,
30
29
  _VaultClient,
31
30
  )
31
+
32
+ try:
33
+ from airflow.sdk import BaseHook
34
+ except ImportError:
35
+ from airflow.hooks.base import BaseHook # type: ignore[attr-defined,no-redef]
32
36
  from airflow.utils.helpers import merge_dicts
33
37
 
34
38
  if TYPE_CHECKING:
@@ -16,6 +16,8 @@
16
16
  # under the License.
17
17
  from __future__ import annotations
18
18
 
19
+ import json
20
+ import time
19
21
  from unittest import mock
20
22
  from unittest.mock import mock_open, patch
21
23
 
@@ -253,86 +255,217 @@ class TestVaultClient:
253
255
  secret_id="pass",
254
256
  )
255
257
 
258
+ @mock.patch("builtins.open", create=True)
256
259
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider._get_scopes")
257
260
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider.get_credentials_and_project_id")
258
- @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
259
- def test_gcp(self, mock_hvac, mock_get_credentials, mock_get_scopes):
261
+ @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac.Client")
262
+ @mock.patch("googleapiclient.discovery.build")
263
+ def test_gcp(self, mock_google_build, mock_hvac_client, mock_get_credentials, mock_get_scopes, mock_open):
264
+ # Mock the content of the file 'path.json'
265
+ mock_file = mock.MagicMock()
266
+ mock_file.read.return_value = '{"client_email": "service_account_email"}'
267
+ mock_open.return_value.__enter__.return_value = mock_file
268
+
260
269
  mock_client = mock.MagicMock()
261
- mock_hvac.Client.return_value = mock_client
270
+ mock_hvac_client.return_value = mock_client
262
271
  mock_get_scopes.return_value = ["scope1", "scope2"]
263
272
  mock_get_credentials.return_value = ("credentials", "project_id")
273
+
274
+ # Mock the current time to use for iat and exp
275
+ current_time = int(time.time())
276
+ iat = current_time
277
+ exp = iat + 3600 # 1 hour after iat
278
+
279
+ # Mock the signJwt API to return the expected payload
280
+ mock_sign_jwt = (
281
+ mock_google_build.return_value.projects.return_value.serviceAccounts.return_value.signJwt
282
+ )
283
+ mock_sign_jwt.return_value.execute.return_value = {"signedJwt": "mocked_jwt"}
284
+
264
285
  vault_client = _VaultClient(
265
286
  auth_type="gcp",
266
287
  gcp_key_path="path.json",
267
288
  gcp_scopes="scope1,scope2",
289
+ role_id="role",
268
290
  url="http://localhost:8180",
269
291
  session=None,
270
292
  )
271
- client = vault_client.client
272
- mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
293
+
294
+ # Preserve the original json.dumps
295
+ original_json_dumps = json.dumps
296
+
297
+ # Inject the mocked payload into the JWT signing process
298
+ with mock.patch("json.dumps") as mock_json_dumps:
299
+
300
+ def mocked_json_dumps(payload):
301
+ # Override the payload to inject controlled iat and exp values
302
+ payload["iat"] = iat
303
+ payload["exp"] = exp
304
+ return original_json_dumps(payload) # Use the original json.dumps
305
+
306
+ mock_json_dumps.side_effect = mocked_json_dumps
307
+
308
+ client = vault_client.client # Trigger the Vault client creation
309
+
310
+ # Validate that the HVAC client and other mocks are called correctly
311
+ mock_hvac_client.assert_called_with(url="http://localhost:8180", session=None)
273
312
  mock_get_scopes.assert_called_with("scope1,scope2")
274
313
  mock_get_credentials.assert_called_with(
275
314
  key_path="path.json", keyfile_dict=None, scopes=["scope1", "scope2"]
276
315
  )
277
- mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
278
- client.auth.gcp.configure.assert_called_with(
279
- credentials="credentials",
280
- )
316
+
317
+ # Extract the arguments passed to the mocked signJwt API
318
+ args, kwargs = mock_sign_jwt.call_args
319
+ payload = json.loads(kwargs["body"]["payload"])
320
+
321
+ # Assert iat and exp values are as expected
322
+ assert payload["iat"] == iat
323
+ assert payload["exp"] == exp
324
+ assert abs(payload["exp"] - (payload["iat"] + 3600)) < 10 # Validate exp is 3600 seconds after iat
325
+
326
+ client.auth.gcp.login.assert_called_with(role="role", jwt="mocked_jwt")
281
327
  client.is_authenticated.assert_called_with()
282
328
  assert vault_client.kv_engine_version == 2
283
329
 
330
+ @mock.patch("builtins.open", create=True)
284
331
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider._get_scopes")
285
332
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider.get_credentials_and_project_id")
286
- @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
287
- def test_gcp_different_auth_mount_point(self, mock_hvac, mock_get_credentials, mock_get_scopes):
288
- mock_client = mock.MagicMock()
289
- mock_hvac.Client.return_value = mock_client
333
+ @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac.Client")
334
+ @mock.patch("googleapiclient.discovery.build")
335
+ def test_gcp_different_auth_mount_point(
336
+ self, mock_google_build, mock_hvac_client, mock_get_credentials, mock_get_scopes, mock_open
337
+ ):
338
+ # Mock the content of the file 'path.json'
339
+ mock_file = mock.MagicMock()
340
+ mock_file.read.return_value = '{"client_email": "service_account_email"}'
341
+ mock_open.return_value.__enter__.return_value = mock_file
342
+
343
+ mock_client = mock.MagicMock()
344
+ mock_hvac_client.return_value = mock_client
290
345
  mock_get_scopes.return_value = ["scope1", "scope2"]
291
346
  mock_get_credentials.return_value = ("credentials", "project_id")
347
+
348
+ mock_sign_jwt = (
349
+ mock_google_build.return_value.projects.return_value.serviceAccounts.return_value.signJwt
350
+ )
351
+ mock_sign_jwt.return_value.execute.return_value = {"signedJwt": "mocked_jwt"}
352
+
353
+ # Generate realistic iat and exp values
354
+ current_time = int(time.time())
355
+ iat = current_time
356
+ exp = current_time + 3600 # 1 hour later
357
+
292
358
  vault_client = _VaultClient(
293
359
  auth_type="gcp",
294
360
  gcp_key_path="path.json",
295
361
  gcp_scopes="scope1,scope2",
362
+ role_id="role",
296
363
  url="http://localhost:8180",
297
364
  auth_mount_point="other",
298
365
  session=None,
299
366
  )
300
- client = vault_client.client
301
- mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
367
+
368
+ # Preserve the original json.dumps
369
+ original_json_dumps = json.dumps
370
+
371
+ # Inject the mocked payload into the JWT signing process
372
+ with mock.patch("json.dumps") as mock_json_dumps:
373
+
374
+ def mocked_json_dumps(payload):
375
+ # Override the payload to inject controlled iat and exp values
376
+ payload["iat"] = iat
377
+ payload["exp"] = exp
378
+ return original_json_dumps(payload) # Use the original json.dumps
379
+
380
+ mock_json_dumps.side_effect = mocked_json_dumps
381
+
382
+ client = vault_client.client # Trigger the Vault client creation
383
+
384
+ # Assertions
385
+ mock_hvac_client.assert_called_with(url="http://localhost:8180", session=None)
302
386
  mock_get_scopes.assert_called_with("scope1,scope2")
303
387
  mock_get_credentials.assert_called_with(
304
388
  key_path="path.json", keyfile_dict=None, scopes=["scope1", "scope2"]
305
389
  )
306
- mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
307
- client.auth.gcp.configure.assert_called_with(credentials="credentials", mount_point="other")
390
+ # Extract the arguments passed to the mocked signJwt API
391
+ args, kwargs = mock_sign_jwt.call_args
392
+ payload = json.loads(kwargs["body"]["payload"])
393
+
394
+ # Assert iat and exp values are as expected
395
+ assert payload["iat"] == iat
396
+ assert payload["exp"] == exp
397
+ assert abs(payload["exp"] - (payload["iat"] + 3600)) < 10 # Validate exp is 3600 seconds after iat
398
+
399
+ client.auth.gcp.login.assert_called_with(role="role", jwt="mocked_jwt", mount_point="other")
308
400
  client.is_authenticated.assert_called_with()
309
401
  assert vault_client.kv_engine_version == 2
310
402
 
403
+ @mock.patch(
404
+ "builtins.open", new_callable=mock_open, read_data='{"client_email": "service_account_email"}'
405
+ )
311
406
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider._get_scopes")
312
407
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider.get_credentials_and_project_id")
313
- @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
314
- def test_gcp_dict(self, mock_hvac, mock_get_credentials, mock_get_scopes):
408
+ @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac.Client")
409
+ @mock.patch("googleapiclient.discovery.build")
410
+ def test_gcp_dict(
411
+ self, mock_google_build, mock_hvac_client, mock_get_credentials, mock_get_scopes, mock_file
412
+ ):
315
413
  mock_client = mock.MagicMock()
316
- mock_hvac.Client.return_value = mock_client
414
+ mock_hvac_client.return_value = mock_client
317
415
  mock_get_scopes.return_value = ["scope1", "scope2"]
318
416
  mock_get_credentials.return_value = ("credentials", "project_id")
417
+
418
+ mock_sign_jwt = (
419
+ mock_google_build.return_value.projects.return_value.serviceAccounts.return_value.signJwt
420
+ )
421
+ mock_sign_jwt.return_value.execute.return_value = {"signedJwt": "mocked_jwt"}
422
+
423
+ # Generate realistic iat and exp values
424
+ current_time = int(time.time())
425
+ iat = current_time
426
+ exp = current_time + 3600 # 1 hour later
427
+
319
428
  vault_client = _VaultClient(
320
429
  auth_type="gcp",
321
- gcp_keyfile_dict={"key": "value"},
430
+ gcp_keyfile_dict={"client_email": "service_account_email"},
322
431
  gcp_scopes="scope1,scope2",
432
+ role_id="role",
323
433
  url="http://localhost:8180",
324
434
  session=None,
325
435
  )
326
- client = vault_client.client
327
- mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
436
+
437
+ # Preserve the original json.dumps
438
+ original_json_dumps = json.dumps
439
+
440
+ # Inject the mocked payload into the JWT signing process
441
+ with mock.patch("json.dumps") as mock_json_dumps:
442
+
443
+ def mocked_json_dumps(payload):
444
+ # Override the payload to inject controlled iat and exp values
445
+ payload["iat"] = iat
446
+ payload["exp"] = exp
447
+ return original_json_dumps(payload) # Use the original json.dumps
448
+
449
+ mock_json_dumps.side_effect = mocked_json_dumps
450
+
451
+ client = vault_client.client # Trigger the Vault client creation
452
+
453
+ # Assertions
454
+ mock_hvac_client.assert_called_with(url="http://localhost:8180", session=None)
328
455
  mock_get_scopes.assert_called_with("scope1,scope2")
329
456
  mock_get_credentials.assert_called_with(
330
- key_path=None, keyfile_dict={"key": "value"}, scopes=["scope1", "scope2"]
331
- )
332
- mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
333
- client.auth.gcp.configure.assert_called_with(
334
- credentials="credentials",
457
+ key_path=None, keyfile_dict={"client_email": "service_account_email"}, scopes=["scope1", "scope2"]
335
458
  )
459
+ # Extract the arguments passed to the mocked signJwt API
460
+ args, kwargs = mock_sign_jwt.call_args
461
+ payload = json.loads(kwargs["body"]["payload"])
462
+
463
+ # Assert iat and exp values are as expected
464
+ assert payload["iat"] == iat
465
+ assert payload["exp"] == exp
466
+ assert abs(payload["exp"] - (payload["iat"] + 3600)) < 10 # Validate exp is 3600 seconds after iat
467
+
468
+ client.auth.gcp.login.assert_called_with(role="role", jwt="mocked_jwt")
336
469
  client.is_authenticated.assert_called_with()
337
470
  assert vault_client.kv_engine_version == 2
338
471
 
@@ -18,7 +18,7 @@ from __future__ import annotations
18
18
 
19
19
  import re
20
20
  from unittest import mock
21
- from unittest.mock import PropertyMock, mock_open, patch
21
+ from unittest.mock import MagicMock, PropertyMock, mock_open, patch
22
22
 
23
23
  import pytest
24
24
  from hvac.exceptions import VaultError
@@ -431,7 +431,10 @@ class TestVaultHook:
431
431
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider.get_credentials_and_project_id")
432
432
  @mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
433
433
  @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
434
- def test_gcp_init_params(self, mock_hvac, mock_get_connection, mock_get_credentials, mock_get_scopes):
434
+ @mock.patch("googleapiclient.discovery.build")
435
+ def test_gcp_init_params(
436
+ self, mock_build, mock_hvac, mock_get_connection, mock_get_credentials, mock_get_scopes
437
+ ):
435
438
  mock_client = mock.MagicMock()
436
439
  mock_hvac.Client.return_value = mock_client
437
440
  mock_connection = self.get_mock_connection()
@@ -439,6 +442,17 @@ class TestVaultHook:
439
442
  mock_get_scopes.return_value = ["scope1", "scope2"]
440
443
  mock_get_credentials.return_value = ("credentials", "project_id")
441
444
 
445
+ # Mock googleapiclient.discovery.build chain
446
+ mock_service = MagicMock()
447
+ mock_projects = MagicMock()
448
+ mock_service_accounts = MagicMock()
449
+ mock_sign_jwt = MagicMock()
450
+ mock_sign_jwt.execute.return_value = {"signedJwt": "mocked_jwt"}
451
+ mock_service_accounts.signJwt.return_value = mock_sign_jwt
452
+ mock_projects.serviceAccounts.return_value = mock_service_accounts
453
+ mock_service.projects.return_value = mock_projects
454
+ mock_build.return_value = mock_service
455
+
442
456
  connection_dict = {}
443
457
 
444
458
  mock_connection.extra_dejson.get.side_effect = connection_dict.get
@@ -447,20 +461,24 @@ class TestVaultHook:
447
461
  "auth_type": "gcp",
448
462
  "gcp_key_path": "path.json",
449
463
  "gcp_scopes": "scope1,scope2",
464
+ "role_id": "role",
450
465
  "session": None,
451
466
  }
452
467
 
453
- test_hook = VaultHook(**kwargs)
454
- test_client = test_hook.get_conn()
468
+ with patch(
469
+ "builtins.open", mock_open(read_data='{"client_email": "service_account_email"}')
470
+ ) as mock_file:
471
+ test_hook = VaultHook(**kwargs)
472
+ test_client = test_hook.get_conn()
473
+ mock_file.assert_called_with("path.json")
474
+
455
475
  mock_get_connection.assert_called_with("vault_conn_id")
456
476
  mock_get_scopes.assert_called_with("scope1,scope2")
457
477
  mock_get_credentials.assert_called_with(
458
478
  key_path="path.json", keyfile_dict=None, scopes=["scope1", "scope2"]
459
479
  )
460
480
  mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
461
- test_client.auth.gcp.configure.assert_called_with(
462
- credentials="credentials",
463
- )
481
+ test_client.auth.gcp.login.assert_called_with(role="role", jwt="mocked_jwt")
464
482
  test_client.is_authenticated.assert_called_with()
465
483
  assert test_hook.vault_client.kv_engine_version == 2
466
484
 
@@ -468,7 +486,10 @@ class TestVaultHook:
468
486
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider.get_credentials_and_project_id")
469
487
  @mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
470
488
  @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
471
- def test_gcp_dejson(self, mock_hvac, mock_get_connection, mock_get_credentials, mock_get_scopes):
489
+ @mock.patch("googleapiclient.discovery.build")
490
+ def test_gcp_dejson(
491
+ self, mock_build, mock_hvac, mock_get_connection, mock_get_credentials, mock_get_scopes
492
+ ):
472
493
  mock_client = mock.MagicMock()
473
494
  mock_hvac.Client.return_value = mock_client
474
495
  mock_connection = self.get_mock_connection()
@@ -476,29 +497,45 @@ class TestVaultHook:
476
497
  mock_get_scopes.return_value = ["scope1", "scope2"]
477
498
  mock_get_credentials.return_value = ("credentials", "project_id")
478
499
 
500
+ # Mock googleapiclient.discovery.build chain
501
+ mock_service = MagicMock()
502
+ mock_projects = MagicMock()
503
+ mock_service_accounts = MagicMock()
504
+ mock_sign_jwt = MagicMock()
505
+ mock_sign_jwt.execute.return_value = {"signedJwt": "mocked_jwt"}
506
+ mock_service_accounts.signJwt.return_value = mock_sign_jwt
507
+ mock_projects.serviceAccounts.return_value = mock_service_accounts
508
+ mock_service.projects.return_value = mock_projects
509
+ mock_build.return_value = mock_service
510
+
479
511
  connection_dict = {
480
512
  "auth_type": "gcp",
481
513
  "gcp_key_path": "path.json",
482
514
  "gcp_scopes": "scope1,scope2",
515
+ "role_id": "role",
483
516
  }
484
517
 
485
518
  mock_connection.extra_dejson.get.side_effect = connection_dict.get
486
519
  kwargs = {
487
520
  "vault_conn_id": "vault_conn_id",
488
521
  "session": None,
522
+ "role_id": "role",
489
523
  }
490
524
 
491
- test_hook = VaultHook(**kwargs)
492
- test_client = test_hook.get_conn()
525
+ with patch(
526
+ "builtins.open", mock_open(read_data='{"client_email": "service_account_email"}')
527
+ ) as mock_file:
528
+ test_hook = VaultHook(**kwargs)
529
+ test_client = test_hook.get_conn()
530
+ mock_file.assert_called_with("path.json")
531
+
493
532
  mock_get_connection.assert_called_with("vault_conn_id")
494
533
  mock_get_scopes.assert_called_with("scope1,scope2")
495
534
  mock_get_credentials.assert_called_with(
496
535
  key_path="path.json", keyfile_dict=None, scopes=["scope1", "scope2"]
497
536
  )
498
537
  mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
499
- test_client.auth.gcp.configure.assert_called_with(
500
- credentials="credentials",
501
- )
538
+ test_client.auth.gcp.login.assert_called_with(role="role", jwt="mocked_jwt")
502
539
  test_client.is_authenticated.assert_called_with()
503
540
  assert test_hook.vault_client.kv_engine_version == 2
504
541
 
@@ -506,7 +543,10 @@ class TestVaultHook:
506
543
  @mock.patch("airflow.providers.google.cloud.utils.credentials_provider.get_credentials_and_project_id")
507
544
  @mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
508
545
  @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
509
- def test_gcp_dict_dejson(self, mock_hvac, mock_get_connection, mock_get_credentials, mock_get_scopes):
546
+ @mock.patch("googleapiclient.discovery.build")
547
+ def test_gcp_dict_dejson(
548
+ self, mock_build, mock_hvac, mock_get_connection, mock_get_credentials, mock_get_scopes
549
+ ):
510
550
  mock_client = mock.MagicMock()
511
551
  mock_hvac.Client.return_value = mock_client
512
552
  mock_connection = self.get_mock_connection()
@@ -514,16 +554,29 @@ class TestVaultHook:
514
554
  mock_get_scopes.return_value = ["scope1", "scope2"]
515
555
  mock_get_credentials.return_value = ("credentials", "project_id")
516
556
 
557
+ # Mock googleapiclient.discovery.build chain
558
+ mock_service = MagicMock()
559
+ mock_projects = MagicMock()
560
+ mock_service_accounts = MagicMock()
561
+ mock_sign_jwt = MagicMock()
562
+ mock_sign_jwt.execute.return_value = {"signedJwt": "mocked_jwt"}
563
+ mock_service_accounts.signJwt.return_value = mock_sign_jwt
564
+ mock_projects.serviceAccounts.return_value = mock_service_accounts
565
+ mock_service.projects.return_value = mock_projects
566
+ mock_build.return_value = mock_service
567
+
517
568
  connection_dict = {
518
569
  "auth_type": "gcp",
519
- "gcp_keyfile_dict": '{"key": "value"}',
570
+ "gcp_keyfile_dict": '{"client_email": "service_account_email"}',
520
571
  "gcp_scopes": "scope1,scope2",
572
+ "role_id": "role",
521
573
  }
522
574
 
523
575
  mock_connection.extra_dejson.get.side_effect = connection_dict.get
524
576
  kwargs = {
525
577
  "vault_conn_id": "vault_conn_id",
526
578
  "session": None,
579
+ "role_id": "role",
527
580
  }
528
581
 
529
582
  test_hook = VaultHook(**kwargs)
@@ -531,12 +584,10 @@ class TestVaultHook:
531
584
  mock_get_connection.assert_called_with("vault_conn_id")
532
585
  mock_get_scopes.assert_called_with("scope1,scope2")
533
586
  mock_get_credentials.assert_called_with(
534
- key_path=None, keyfile_dict={"key": "value"}, scopes=["scope1", "scope2"]
587
+ key_path=None, keyfile_dict={"client_email": "service_account_email"}, scopes=["scope1", "scope2"]
535
588
  )
536
589
  mock_hvac.Client.assert_called_with(url="http://localhost:8180", session=None)
537
- test_client.auth.gcp.configure.assert_called_with(
538
- credentials="credentials",
539
- )
590
+ test_client.auth.gcp.login.assert_called_with(role="role", jwt="mocked_jwt")
540
591
  test_client.is_authenticated.assert_called_with()
541
592
  assert test_hook.vault_client.kv_engine_version == 2
542
593