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.
- risk-custom-secrets-provider-1.0.0/PKG-INFO +6 -0
- risk-custom-secrets-provider-1.0.0/risk_custom_secrets_provider.egg-info/PKG-INFO +6 -0
- risk-custom-secrets-provider-1.0.0/risk_custom_secrets_provider.egg-info/SOURCES.txt +9 -0
- risk-custom-secrets-provider-1.0.0/risk_custom_secrets_provider.egg-info/dependency_links.txt +1 -0
- risk-custom-secrets-provider-1.0.0/risk_custom_secrets_provider.egg-info/entry_points.txt +2 -0
- risk-custom-secrets-provider-1.0.0/risk_custom_secrets_provider.egg-info/requires.txt +3 -0
- risk-custom-secrets-provider-1.0.0/risk_custom_secrets_provider.egg-info/top_level.txt +1 -0
- risk-custom-secrets-provider-1.0.0/risk_secret_provider/__init__.py +0 -0
- risk-custom-secrets-provider-1.0.0/risk_secret_provider/my_secrets.py +173 -0
- risk-custom-secrets-provider-1.0.0/setup.cfg +4 -0
- risk-custom-secrets-provider-1.0.0/setup.py +17 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
risk_secret_provider
|
|
File without changes
|
|
@@ -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,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
|
+
)
|