risk-custom-secrets-provider 1.0.0__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 risk-custom-secrets-provider might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.1
2
+ Name: risk-custom-secrets-provider
3
+ Version: 1.0.0
4
+ Requires-Dist: hvac==2.3.0
5
+ Requires-Dist: apache-airflow-providers-google==10.23.0
6
+ Requires-Dist: apache-airflow==2.9.3
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.1
2
+ Name: risk-custom-secrets-provider
3
+ Version: 1.0.0
4
+ Requires-Dist: hvac==2.3.0
5
+ Requires-Dist: apache-airflow-providers-google==10.23.0
6
+ Requires-Dist: apache-airflow==2.9.3
@@ -0,0 +1,9 @@
1
+ setup.py
2
+ risk_custom_secrets_provider.egg-info/PKG-INFO
3
+ risk_custom_secrets_provider.egg-info/SOURCES.txt
4
+ risk_custom_secrets_provider.egg-info/dependency_links.txt
5
+ risk_custom_secrets_provider.egg-info/entry_points.txt
6
+ risk_custom_secrets_provider.egg-info/requires.txt
7
+ risk_custom_secrets_provider.egg-info/top_level.txt
8
+ risk_secret_provider/__init__.py
9
+ risk_secret_provider/my_secrets.py
@@ -0,0 +1,2 @@
1
+ [apache_airflow_provider]
2
+ risk_custom_secrets_provider = risk_secret_provider.my_secrets:MySecretManager
@@ -0,0 +1,3 @@
1
+ hvac==2.3.0
2
+ apache-airflow-providers-google==10.23.0
3
+ apache-airflow==2.9.3
@@ -0,0 +1,173 @@
1
+ from airflow.providers.google.cloud.secrets.secret_manager import CloudSecretManagerBackend
2
+ from airflow.secrets import BaseSecretsBackend
3
+ from airflow.models import Variable
4
+ from typing import Optional, Dict, List
5
+
6
+ import hvac
7
+ import time
8
+
9
+ class MyVaultManager:
10
+ _configs = {
11
+ 'prod': {
12
+ 'data_dbx': {
13
+ 'workspace_url': '4460961511039885.5.gcp.databricks.com',
14
+ 'sp': 'risk-prod-airflow',
15
+ },
16
+ 'risk_dbx': {
17
+ 'workspace_url': '2234182411525940.0.gcp.databricks.com',
18
+ 'sp': 'risk-prod-airflow',
19
+ },
20
+ },
21
+ 'preprod': {
22
+ 'data_dbx': {
23
+ 'workspace_url': '3683033261333782.2.gcp.databricks.com',
24
+ 'sp': 'risk-preprod-airflow',
25
+ },
26
+ 'risk_dbx': {
27
+ 'workspace_url': '305193242708798.8.gcp.databricks.com',
28
+ 'sp': 'risk-preprod-airflow',
29
+ },
30
+ }
31
+ }
32
+ _supported_connections = [
33
+ # 'risk_dbx',
34
+ 'data_dbx',
35
+ ]
36
+ _dbx_connection_template = "databricks://https://{sp_client_id}:{sp_secret}@{workspace_url}/?service_principal_oauth=true"
37
+ def __init__(self, cloud_secret_manager) -> None:
38
+ self.vault_client = None
39
+ self.env = None
40
+ self.role_id = None
41
+ self.secret_id = None
42
+ self.cloud_secret_manager = cloud_secret_manager
43
+ super().__init__()
44
+
45
+ def _lazy_init(self) -> None:
46
+ self.role_id = self.cloud_secret_manager.get_variable("vault_role_id")
47
+ self.secret_id = self.cloud_secret_manager.get_variable("vault_secret_id")
48
+ self.env = 'prod' if Variable.get("env") == 'prod' else 'preprod'
49
+ self.config = self._configs[self.env]
50
+ self.vault_client = VaultClient(role_id=self.role_id, secret_id=self.secret_id)
51
+
52
+ def _get_secret_path(self, conn_id: str) -> str:
53
+ sp = self.config[conn_id]['sp']
54
+ return f'databricks/service_principal_secret/{self.env}/{sp}'
55
+
56
+ def get_conn_value(self, conn_id: str) -> str | None:
57
+ if conn_id not in self._supported_connections:
58
+ return None
59
+ if self.vault_client is None:
60
+ self._lazy_init()
61
+ vault_secrets = self.vault_client.get_credentials(secret_path=self._get_secret_path(conn_id))
62
+ return self._dbx_connection_template.format(
63
+ sp_client_id=vault_secrets['client_id'],
64
+ sp_secret=vault_secrets['secret'],
65
+ workspace_url=self.config[conn_id]['workspace_url']
66
+ )
67
+
68
+ class VaultClient:
69
+ """
70
+ A client for interacting with HashiCorp Vault to retrieve secrets.
71
+ This class is a Python equivalent of the Java VaultClient, using the 'hvac' library.
72
+ """
73
+
74
+ def __init__(self, role_id: str, secret_id: str, vault_url: str = "https://vault-toolbox-pci.awx.im", max_vault_conn_retry: int = 30):
75
+ """
76
+ Initializes the VaultClient.
77
+
78
+ Args:
79
+ role_id (str): The AppRole Role ID for authentication.
80
+ secret_id (str): The AppRole Secret ID for authentication.
81
+ vault_url (str, optional): The URL of the Vault server. Defaults to "https://vault-toolbox-pci.awx.im".
82
+ max_vault_conn_retry (int, optional): Maximum number of retries for connecting to Vault. Defaults to 30.
83
+ """
84
+ self.role_id = role_id
85
+ self.secret_id = secret_id
86
+ self.vault_url = vault_url
87
+ self.max_vault_conn_retry = max_vault_conn_retry
88
+ self._client_token = self._get_vault_token()
89
+ self.client = self._get_vault_client(self._client_token)
90
+
91
+
92
+ def _get_vault_client(self, client_token: Optional[str] = None) -> hvac.Client:
93
+ """
94
+ Creates and configures an hvac.Client instance.
95
+
96
+ Args:
97
+ client_token (Optional[str]): A Vault client token. If None, the client is unauthenticated.
98
+
99
+ Returns:
100
+ hvac.Client: A configured hvac client instance.
101
+ """
102
+ # In hvac, the equivalent of 'prefixPathDepth' is handled by mounting paths.
103
+ # We will assume standard secret engine paths.
104
+ client = hvac.Client(
105
+ url=self.vault_url,
106
+ token=client_token,
107
+ timeout=30
108
+ )
109
+ return client
110
+
111
+ def _get_vault_token(self) -> str:
112
+ """
113
+ Logs into Vault using AppRole and retrieves an authentication token.
114
+ Returns:
115
+ str: The Vault authentication token.
116
+ """
117
+ unauthenticated_client = self._get_vault_client()
118
+ for i in range(1, self.max_vault_conn_retry + 1):
119
+ try:
120
+ print(f"Attempting to connect to Vault and get auth token #{i}")
121
+ response = unauthenticated_client.auth.approle.login(
122
+ role_id=self.role_id,
123
+ secret_id=self.secret_id,
124
+ )
125
+ return response['auth']['client_token']
126
+ except Exception as e:
127
+ print(f"Authentication attempt #{i} failed: {e}")
128
+ if i == self.max_vault_conn_retry:
129
+ raise # Re-raise the last exception if all retries fail
130
+ # Linear backoff, sleeping for 10, 20, 30... seconds
131
+ time.sleep(10 * i)
132
+ raise Exception("Failed to get Vault auth token after all retries.")
133
+
134
+ def get_credentials(self, secret_path: str, mount_point: str = 'secret/devops') -> Dict:
135
+ try:
136
+ # The hvac library automatically handles different versions of KV secrets engine.
137
+ # For KVv2, the data is nested under response['data']['data'].
138
+ # For KVv1, it's response['data'].
139
+ response = self.client.secrets.kv.read_secret_version(path=secret_path, mount_point=mount_point)
140
+ return response['data']['data']
141
+ except Exception as e:
142
+ print(f"Failed to read secret from '{secret_path}'. Error: {e}")
143
+ raise
144
+
145
+ class MySecretManager(BaseSecretsBackend):
146
+ def __init__(
147
+ self,
148
+ connections_prefix: str = "risk-airflow-connection",
149
+ variables_prefix: str = "risk-airflow-variable",
150
+ config_prefix: str = "risk-airflow-config",
151
+ sep: str = "-",
152
+ **kwargs,
153
+ ) -> None:
154
+ self.cloud_secret_manager = CloudSecretManagerBackend(
155
+ connections_prefix=connections_prefix,
156
+ variables_prefix=variables_prefix,
157
+ config_prefix=config_prefix,
158
+ sep=sep,
159
+ **kwargs
160
+ )
161
+ self.vault_secret_manager = MyVaultManager(self.cloud_secret_manager)
162
+
163
+ super().__init__()
164
+
165
+ def get_conn_value(self, conn_id: str) -> str | None:
166
+ vault_value = self.vault_secret_manager.get_conn_value(conn_id=conn_id)
167
+ if vault_value is not None:
168
+ return vault_value
169
+ return self.cloud_secret_manager.get_conn_value(conn_id=conn_id)
170
+
171
+ def get_variable(self, key: str) -> str | None:
172
+ return self.cloud_secret_manager.get_variable(key=key)
173
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="risk-custom-secrets-provider",
5
+ version="1.0.0",
6
+ packages=find_packages(),
7
+ entry_points={
8
+ "apache_airflow_provider": [
9
+ "risk_custom_secrets_provider = risk_secret_provider.my_secrets:MySecretManager"
10
+ ]
11
+ },
12
+ install_requires=[
13
+ "hvac ==2.3.0",
14
+ "apache-airflow-providers-google ==10.23.0",
15
+ "apache-airflow ==2.9.3",
16
+ ],
17
+ )