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
22import os
33import json
44import 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