Skip to content

Commit 59d9b59

Browse files
authored
Merge pull request #589 from SAP/DIARCHERS-610
DIARCHERS-610: generate batch token to connect vault
2 parents f1b7946 + c0c982f commit 59d9b59

File tree

1 file changed

+135
-44
lines changed

1 file changed

+135
-44
lines changed

src/api/handlers/job_api.py

Lines changed: 135 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#pylint: disable=too-many-lines,too-few-public-methods,too-many-locals,too-many-statements,too-many-branches
1+
#pylint: disable=too-many-lines,too-few-public-methods,too-many-locals,too-many-statements,too-many-branches
22
import os
33
import json
44
import time
@@ -43,34 +43,117 @@ def delete_file(path):
4343
except Exception as error:
4444
logger.warning("Failed to delete file: %s", error)
4545

46-
47-
def get_token_by_app_role(app_role_url, role_id, secret_id):
48-
app_role = {'role_id': role_id, 'secret_id': secret_id}
49-
json_data = json.dumps(app_role)
50-
for i in range(0, 10):
51-
res = requests.post(url=app_role_url, data=json_data, verify=False)
52-
if res.status_code == 200:
53-
json_res = json.loads(res.content)
54-
token = json_res['auth']['client_token']
55-
return token
56-
time.sleep(5)
57-
err_msg = "Getting token from Vault error even tried 10 times, url is {}, API response is {}:{}".format(app_role_url, res.status_code, res.text)
58-
abort(400, err_msg)
59-
60-
61-
def get_value_from_vault(url, token, secret_key, verify):
62-
for i in range(0, 10):
63-
response = requests.get(url=url, headers={'X-Vault-Token': token}, verify=verify)
64-
if response.status_code == 200:
65-
json_res = json.loads(response.content)
66-
if json_res['data'].get('data') and isinstance(json_res['data'].get('data'), dict):
67-
value = json_res['data'].get('data').get(secret_key)
68-
else:
69-
value = json_res['data'].get(secret_key)
70-
return value
71-
time.sleep(5)
72-
err_msg = "Getting value from Vault error even tried 10 times, url is {}, API response is {}:{}".format(url, response.status_code, response.text)
73-
abort(400, err_msg)
46+
class Vault():
47+
def __init__(self, base_url, namespace, version, role_id, secret_id):
48+
self.base_url = base_url
49+
self.namespace = namespace
50+
self.version = version
51+
self.role_id = role_id
52+
self.secret_id = secret_id
53+
self.token = None
54+
self.policies = None
55+
56+
def get_token_by_app_role(self):
57+
self.policies = self.get_policies(self.token)
58+
if self.token and self.policies:
59+
return self.token
60+
app_role = {'role_id': self.role_id, 'secret_id': self.secret_id}
61+
app_role_url = self.base_url + '/v1/' + self.namespace + '/auth/approle/login' if self.namespace else self.base_url + '/v1/auth/approle/login'
62+
json_data = json.dumps(app_role)
63+
for i in range(0, 10):
64+
res = requests.post(url=app_role_url, data=json_data, verify=False)
65+
if res.status_code == 200:
66+
json_res = json.loads(res.content)
67+
self.token = json_res['auth']['client_token']
68+
self.policies = self.get_policies(self.token)
69+
return self.token
70+
time.sleep(5)
71+
err_msg = "Getting token from Vault error even tried 10 times, url is {}, API response is {}:{}".format(app_role_url, res.status_code, res.text)
72+
abort(400, err_msg)
73+
74+
def get_policies(self, token):
75+
"""Return the policies associated with the provided token."""
76+
if not token:
77+
return None
78+
try:
79+
lookup_url = self.base_url + '/v1/' + self.namespace + '/auth/token/lookup-self' if self.namespace else self.base_url + 'v1/auth/token/lookup-self'
80+
res = requests.get(url=lookup_url, headers={"X-Vault-Token": token}, verify=False)
81+
if res.status_code == 200:
82+
json_res = json.loads(res.content)
83+
policies = json_res['data']['policies']
84+
return policies
85+
return None
86+
except Exception as e:
87+
logger.debug("Token validation failed: %s", str(e))
88+
return None
89+
90+
def generate_batch_token(self, service_token, ttl="1h"):
91+
"""
92+
Generate a batch token using AppRole credentials.
93+
:param service_token: A service token generated when logging in with AppRole.
94+
:param ttl: Time-to-live for the batch token.
95+
:return: The generated batch token.
96+
"""
97+
try:
98+
if self.policies is None:
99+
self.policies = self.get_policies(service_token) or ['default']
100+
if self.policies and 'token-creator' in self.policies:
101+
self.policies.remove('token-creator')
102+
103+
batch_payload = {
104+
"type": "batch",
105+
"policies": self.policies or ['default'],
106+
"ttl": ttl
107+
}
108+
url = self.base_url + '/v1/' + self.namespace + '/auth/token/create' if self.namespace else self.base_url + '/v1/auth/token/create'
109+
110+
for i in range(0, 10):
111+
res = requests.post(url=url, json=batch_payload, headers={"X-Vault-Token": service_token}, verify=False)
112+
if res.status_code == 200:
113+
json_res = json.loads(res.content)
114+
token = json_res['auth']['client_token']
115+
return token
116+
elif res.status_code == 403:
117+
logger.info("Getting batch token from Vault forbidden: {}".format(res.text))
118+
return None
119+
time.sleep(5)
120+
msg = "Getting batch token from Vault failed even tried 10 times, url is {}, API response is {}:{}".format(
121+
url, res.status_code, res.text)
122+
logger.info(msg)
123+
return None
124+
except Exception as e:
125+
logger.info("Exception when getting batch token from Vault: {}".format(e))
126+
return None
127+
128+
def _get_api_url(self, secret_path):
129+
url = self.base_url
130+
if not self.namespace:
131+
self.namespace = ''
132+
if self.version == 'v1':
133+
url += '/v1/' + self.namespace + '/' + secret_path if self.namespace else '/v1/' + secret_path
134+
elif self.version == 'v2':
135+
paths = secret_path.split('/')
136+
url += '/v1/' + self.namespace + '/' + paths[0] + '/data/' + '/'.join(paths[1:]) if self.namespace else '/v1/' + paths[0] + '/data/' + '/'.join(paths[1:])
137+
return url
138+
139+
def get_value_from_vault(self, token, secret_path, secret_key, verify):
140+
try:
141+
url = self._get_api_url(secret_path)
142+
for i in range(0, 10):
143+
response = requests.get(url=url, headers={'X-Vault-Token': token}, verify=verify, timeout=30)
144+
if response.status_code == 200:
145+
json_res = json.loads(response.content)
146+
if json_res['data'].get('data') and isinstance(json_res['data'].get('data'), dict):
147+
value = json_res['data'].get('data').get(secret_key)
148+
else:
149+
value = json_res['data'].get(secret_key)
150+
return value
151+
time.sleep(5)
152+
err_msg = "Getting value from Vault error even tried 10 times, url is {}, API response is {}:{}".format(url, response.status_code, response.text)
153+
abort(400, err_msg)
154+
except Exception as e:
155+
err_msg = "Getting value from Vault exception: {}, url is {}".format(str(e), url)
156+
abort(400, err_msg)
74157

75158

76159
@api.route("/api/job/job", doc=False)
@@ -278,6 +361,8 @@ def get(self):
278361
''', [data['project']['id']])
279362

280363
is_fork = data['job'].get('fork', False)
364+
# Cache Vault instances per vault name+project so we create them only once
365+
vault_cache = {}
281366

282367
def get_secret_type(name):
283368
try:
@@ -300,10 +385,10 @@ def get_auth_type(res):
300385
def get_secret(name):
301386
secret_type = get_secret_type(name)
302387
if secret_type == 'vault':
303-
vault = json.loads(name)
304-
vault_name = vault['$vault']
305-
secret_path = vault['$vault_secret_path']
306-
secret_key = vault['$vault_secret_key']
388+
vault_cfg = json.loads(name)
389+
vault_name = vault_cfg['$vault']
390+
secret_path = vault_cfg['$vault_secret_path']
391+
secret_key = vault_cfg['$vault_secret_key']
307392

308393
result = g.db.execute_one("""
309394
SELECT url, version, token, ca, namespace, role_id, secret_id FROM vault WHERE name = %s and project_id = %s
@@ -313,30 +398,36 @@ def get_secret(name):
313398
abort(400, "Cannot get Vault '%s' in project '%s' " % (vault_name, data['project']['id']))
314399

315400
url, version, token, ca, namespace, role_id, secret_id = result[0], result[1], result[2], result[3], result[4], result[5], result[6]
316-
if not namespace:
317-
namespace = ''
318-
if version == 'v1':
319-
url += '/v1/' + namespace + '/' + secret_path if namespace else '/v1/' + secret_path
320-
elif version == 'v2':
321-
paths = secret_path.split('/')
322-
url += '/v1/' + namespace + '/' + paths[0] + '/data/' + '/'.join(paths[1:]) if namespace else '/v1/' + paths[0] + '/data/' + '/'.join(paths[1:])
323401
# choose validate way
324402
validate_res = get_auth_type(result)
403+
404+
# key vault per project to avoid collisions
405+
vault_key = f"{vault_name}-{data['project']['id']}"
406+
vault_client = vault_cache.get(vault_key)
407+
if not vault_client:
408+
vault_client = Vault(url, namespace, version, role_id, secret_id)
409+
vault_cache[vault_key] = vault_client
410+
325411
if validate_res == 'token':
326412
logger.info('validate way is token')
413+
token_to_use = token
327414
elif validate_res == 'appRole':
328-
app_role_url = result[0] + '/v1/' + namespace + '/auth/approle/login' if namespace else result[0] + '/v1/auth/approle/login'
329-
token = get_token_by_app_role(app_role_url, role_id, secret_id)
415+
logger.info('validate way is appRole')
416+
token_to_use = vault_client.get_token_by_app_role()
417+
batch_token = vault_client.generate_batch_token(token_to_use)
418+
if batch_token:
419+
token_to_use = batch_token
330420
else:
331421
abort(400, "Validate way is '%s' ! result is '%s' " % (validate_res, result))
332422

333423
if not ca:
334-
return get_value_from_vault(url, token, secret_key, False)
424+
logger.info('Start to get value fron vault %s %s %s' % (token_to_use, secret_path, secret_key))
425+
return vault_client.get_value_from_vault(token_to_use, secret_path, secret_key, False)
335426
else:
336427
with tempfile.NamedTemporaryFile(delete=False) as f:
337428
f.write(ca)
338429
f.flush() # ensure all data written
339-
return get_value_from_vault(url, token, secret_key, f.name)
430+
return vault_client.get_value_from_vault(token_to_use, secret_path, secret_key, f.name)
340431
else:
341432
if is_fork:
342433
abort(400, 'Access to secret %s is not allowed from a fork' % name)

0 commit comments

Comments
 (0)