11# functions_keyvault.py
22
33import re
4+ import logging
45from config import *
56from functions_authentication import *
67from functions_settings import *
2425 'storage_account' ,
2526 'cognitive_service' ,
2627 'action' ,
27- 'agent'
28+ 'action-addset' ,
29+ 'agent' ,
30+ 'other'
2831]
2932
3033supported_scopes = [
3336 'group'
3437]
3538
39+ supported_action_auth_types = [
40+ 'key' ,
41+ 'servicePrincipal' ,
42+ 'basic' ,
43+ 'connection_string'
44+ ]
45+
3646ui_trigger_word = "Stored_In_KeyVault"
3747
3848def retrieve_secret_from_key_vault (secret_name , scope_value , scope = "global" , source = "global" ):
@@ -51,8 +61,10 @@ def retrieve_secret_from_key_vault(secret_name, scope_value, scope="global", sou
5161 Exception: If retrieval fails or configuration is invalid.
5262 """
5363 if source not in supported_sources :
64+ logging .error (f"Source '{ source } ' is not supported. Supported sources: { supported_sources } " )
5465 raise ValueError (f"Source '{ source } ' is not supported. Supported sources: { supported_sources } " )
5566 if scope not in supported_scopes :
67+ logging .error (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
5668 raise ValueError (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
5769
5870 full_secret_name = build_full_secret_name (secret_name , scope_value , source , scope )
@@ -73,10 +85,12 @@ def retrieve_secret_from_keyvault_by_full_name(full_secret_name):
7385 settings = get_settings ()
7486 enable_key_vault_secret_storage = settings .get ("enable_key_vault_secret_storage" , False )
7587 if not enable_key_vault_secret_storage :
88+ logging .error (f"Key Vault secret storage is not enabled." )
7689 raise Exception ("Key Vault secret storage is not enabled." )
7790
7891 key_vault_name = settings .get ("key_vault_name" , None )
7992 if not key_vault_name :
93+ logging .error (f"Key Vault name is not configured." )
8094 raise Exception ("Key Vault name is not configured." )
8195
8296 try :
@@ -108,15 +122,19 @@ def store_secret_in_key_vault(secret_name, secret_value, scope_value, source="gl
108122 settings = get_settings ()
109123 enable_key_vault_secret_storage = settings .get ("enable_key_vault_secret_storage" , False )
110124 if not enable_key_vault_secret_storage :
125+ logging .error (f"Key Vault secret storage is not enabled." )
111126 raise Exception ("Key Vault secret storage is not enabled." )
112127
113128 key_vault_name = settings .get ("key_vault_name" , None )
114129 if not key_vault_name :
130+ logging .error (f"Key Vault name is not configured." )
115131 raise Exception ("Key Vault name is not configured." )
116132
117133 if source not in supported_sources :
134+ logging .error (f"Source '{ source } ' is not supported. Supported sources: { supported_sources } " )
118135 raise ValueError (f"Source '{ source } ' is not supported. Supported sources: { supported_sources } " )
119136 if scope not in supported_scopes :
137+ logging .error (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
120138 raise ValueError (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
121139
122140
@@ -129,6 +147,7 @@ def store_secret_in_key_vault(secret_name, secret_value, scope_value, source="gl
129147 print (f"Secret '{ full_secret_name } ' stored successfully in Key Vault." )
130148 return full_secret_name
131149 except Exception as e :
150+ logging .error (f"Failed to store secret '{ full_secret_name } ' in Key Vault: { str (e )} " )
132151 raise Exception (f"Failed to store secret '{ full_secret_name } ' in Key Vault: { str (e )} " ) from e
133152
134153def build_full_secret_name (secret_name , scope_value , source , scope ):
@@ -147,8 +166,9 @@ def build_full_secret_name(secret_name, scope_value, source, scope):
147166 ValueError: If the name exceeds 127 characters.
148167 """
149168 full_secret_name = f"{ scope_value } --{ source } --{ scope } --{ secret_name } "
150- if len (full_secret_name ) > 127 :
151- raise ValueError (f"The full secret name '{ full_secret_name } ' exceeds the maximum length of 127 characters." )
169+ if not validate_secret_name_dynamic (full_secret_name ):
170+ logging .error (f"The full secret name '{ full_secret_name } ' is invalid." )
171+ raise ValueError (f"The full secret name '{ full_secret_name } ' is invalid." )
152172 return full_secret_name
153173
154174def validate_secret_name_dynamic (secret_name ):
@@ -169,9 +189,13 @@ def validate_secret_name_dynamic(secret_name):
169189 pattern = rf"^(.+)--({ sources_pattern } )--({ scopes_pattern } )--(.+)$"
170190 match = re .match (pattern , secret_name )
171191 if not match :
192+ print (f"Secret name '{ secret_name } ' does not match the required pattern." )
193+ logging .warning (f"Secret name '{ secret_name } ' does not match the required pattern." )
172194 return False
173195 # Optionally, check length
174196 if len (secret_name ) > 127 :
197+ print (f"Secret name '{ secret_name } ' exceeds the maximum length of 127 characters." )
198+ logging .warning (f"Secret name '{ secret_name } ' exceeds the maximum length of 127 characters." )
175199 return False
176200 return True
177201
@@ -212,10 +236,11 @@ def keyvault_agent_save_helper(agent_dict, scope_value, scope="global"):
212236 full_secret_name = store_secret_in_key_vault (secret_name , value , scope_value , source = source , scope = scope )
213237 updated [key ] = full_secret_name
214238 except Exception as e :
239+ logging .error (f"Failed to store agent key '{ key } ' in Key Vault: { e } " )
215240 raise Exception (f"Failed to store agent key '{ key } ' in Key Vault: { e } " )
216241 return updated
217242
218- def keyvault_agent_get_helper (agent_dict , scope_value , scope = "global" ):
243+ def keyvault_agent_get_helper (agent_dict , scope_value , scope = "global" , return_actual_key = False ):
219244 """
220245 For agent dicts, retrieve sensitive keys from Key Vault if they are stored as Key Vault references.
221246 Only processes 'azure_agent_apim_gpt_subscription_key' and 'azure_openai_gpt_key'.
@@ -224,6 +249,7 @@ def keyvault_agent_get_helper(agent_dict, scope_value, scope="global"):
224249 agent_dict (dict): The agent dictionary to process.
225250 scope_value (str): The value for the scope (e.g., agent id).
226251 scope (str): The scope (e.g., 'user', 'global').
252+ return_actual_key (bool): If True, retrieves the actual secret value from Key Vault. If False, replaces with ui_trigger_word.
227253
228254 Returns:
229255 dict: A new agent dict with sensitive values replaced by Key Vault references.
@@ -244,18 +270,20 @@ def keyvault_agent_get_helper(agent_dict, scope_value, scope="global"):
244270 value = updated [key ]
245271 if validate_secret_name_dynamic (value ):
246272 try :
247- # Uncomment below to actually retrieve the secret value
248- # actual_key = retrieve_secret_from_key_vault(value)
249- # updated[key] = actual_key
250- updated [key ] = ui_trigger_word
273+ if return_actual_key :
274+ actual_key = retrieve_secret_from_key_vault (value )
275+ updated [key ] = actual_key
276+ else :
277+ updated [key ] = ui_trigger_word
251278 except Exception as e :
279+ logging .error (f"Failed to retrieve agent key '{ key } ' from Key Vault: { e } " )
252280 raise Exception (f"Failed to retrieve agent key '{ key } ' from Key Vault: { e } " )
253281 return updated
254282
255283def keyvault_plugin_save_helper (plugin_dict , scope_value , scope = "global" ):
256284 """
257- For plugin dicts, store the auth.key in Key Vault if auth.type is 'key' or 'servicePrincipal ',
258- and replace its value with the Key Vault secret name.
285+ For plugin dicts, store the auth.key in Key Vault if auth.type is 'key', 'servicePrincipal', 'basic', or 'connection_string ',
286+ and replace its value with the Key Vault secret name. Also supports dynamic secret storage for any additionalFields key ending with '__Secret'.
259287
260288 Args:
261289 plugin_dict (dict): The plugin dictionary to process.
@@ -266,28 +294,167 @@ def keyvault_plugin_save_helper(plugin_dict, scope_value, scope="global"):
266294 dict: A new plugin dict with sensitive values replaced by Key Vault references.
267295 Raises:
268296 Exception: If storing a key in Key Vault fails.
297+
298+ Feature:
299+ Any key in additionalFields ending with '__Secret' will be stored in Key Vault and replaced with a Key Vault reference.
300+ This allows plugin writers to dynamically store secrets without custom code.
269301 """
270- source = "plugin"
302+ if scope not in supported_scopes :
303+ logging .error (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
304+ raise ValueError (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
305+ source = "action" # Use 'action' for plugins per app convention
271306 updated = dict (plugin_dict )
272307 plugin_name = updated .get ('name' , 'plugin' )
273308 auth = updated .get ('auth' , {})
274- if not isinstance (auth , dict ):
275- return updated
276- auth_type = auth .get ('type' , None )
277- if auth_type in ('key' , 'servicePrincipal' ) and 'key' in auth and auth ['key' ]:
278- value = auth ['key' ]
279- # If already a Key Vault reference, skip
280- if not validate_secret_name_dynamic (value ):
281- secret_name = plugin_name
282- try :
283- full_secret_name = store_secret_in_key_vault (secret_name , value , scope_value , source = source , scope = scope )
284- # Update the auth dict with the Key Vault reference
285- new_auth = dict (auth )
286- new_auth ['key' ] = full_secret_name
287- updated ['auth' ] = new_auth
288- except Exception as e :
289- raise Exception (f"Failed to store plugin key in Key Vault: { e } " )
309+ if isinstance (auth , dict ):
310+ auth_type = auth .get ('type' , None )
311+ if auth_type in supported_action_auth_types and 'key' in auth and auth ['key' ]:
312+ value = auth ['key' ]
313+ if not validate_secret_name_dynamic (value ):
314+ try :
315+ full_secret_name = store_secret_in_key_vault (plugin_name , value , scope_value , source = source , scope = scope )
316+ new_auth = dict (auth )
317+ new_auth ['key' ] = full_secret_name
318+ updated ['auth' ] = new_auth
319+ except Exception as e :
320+ logging .error (f"Failed to store plugin key in Key Vault: { e } " )
321+ raise Exception (f"Failed to store plugin key in Key Vault: { e } " )
322+ else :
323+ print (f"Auth type '{ auth_type } ' does not require Key Vault storage. Does not match " )
324+
325+ # Handle additionalFields dynamic secrets
326+ additional_fields = updated .get ('additionalFields' , {})
327+ if isinstance (additional_fields , dict ):
328+ new_additional_fields = dict (additional_fields )
329+ for k , v in additional_fields .items ():
330+ if k .endswith ('__Secret' ) and v :
331+ addset_source = 'action-addset'
332+ base_field = k [:- 8 ] # Remove '__Secret'
333+ akv_key = f"{ plugin_name } -{ base_field } " .replace ('__' , '-' )
334+ full_secret_name = build_full_secret_name (akv_key , scope_value , addset_source , scope )
335+ if not validate_secret_name_dynamic (full_secret_name ):
336+ logging .error (f"Generated secret name for additionalField '{ k } ' is not valid." )
337+ raise ValueError (f"Generated secret name for additionalField '{ k } ' is not valid." )
338+ try :
339+ full_secret_name = store_secret_in_key_vault (akv_key , v , scope_value , source = addset_source , scope = scope )
340+ new_additional_fields [k ] = full_secret_name
341+ except Exception as e :
342+ logging .error (f"Failed to store plugin additionalField secret '{ k } ' in Key Vault: { e } " )
343+ raise Exception (f"Failed to store plugin additionalField secret '{ k } ' in Key Vault: { e } " )
344+ updated ['additionalFields' ] = new_additional_fields
345+ return updated
346+ # Helper to retrieve plugin secrets from Key Vault
347+ def keyvault_plugin_get_helper (plugin_dict , scope_value , scope = "global" , return_actual_key = False ):
348+ """
349+ For plugin dicts, retrieve secrets from Key Vault for auth.key and any additionalFields key ending with '__Secret'.
350+ If the value is a Key Vault reference, retrieve the actual secret and replace with ui_trigger_word.
351+
352+ Args:
353+ plugin_dict (dict): The plugin dictionary to process.
354+ scope_value (str): The value for the scope (e.g., plugin id).
355+ scope (str): The scope (e.g., 'user', 'global').
356+
357+ Returns:
358+ dict: A new plugin dict with sensitive values replaced by ui_trigger_word if stored in Key Vault.
359+ """
360+ if scope not in supported_scopes :
361+ logging .error (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
362+ raise ValueError (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
363+ source = "action"
364+ updated = dict (plugin_dict )
365+ plugin_name = updated .get ('name' , 'plugin' )
366+ auth = updated .get ('auth' , {})
367+ if isinstance (auth , dict ):
368+ if 'key' in auth and auth ['key' ]:
369+ value = auth ['key' ]
370+ if validate_secret_name_dynamic (value ):
371+ try :
372+ if return_actual_key :
373+ actual_key = retrieve_secret_from_key_vault (plugin_name , scope_value , scope , source )
374+ new_auth = dict (auth )
375+ new_auth ['key' ] = actual_key
376+ updated ['auth' ] = new_auth
377+ else :
378+ new_auth = dict (auth )
379+ new_auth ['key' ] = ui_trigger_word
380+ updated ['auth' ] = new_auth
381+ except Exception as e :
382+ logging .error (f"Failed to retrieve plugin key from Key Vault: { e } " )
383+ raise Exception (f"Failed to retrieve plugin key from Key Vault: { e } " )
384+
385+ additional_fields = updated .get ('additionalFields' , {})
386+ if isinstance (additional_fields , dict ):
387+ new_additional_fields = dict (additional_fields )
388+ for k , v in additional_fields .items ():
389+ if k .endswith ('__Secret' ) and v and validate_secret_name_dynamic (v ):
390+ addset_source = 'action-addset'
391+ base_field = k [:- 8 ] # Remove '__Secret'
392+ akv_key = f"{ plugin_name } -{ base_field } " .replace ('__' , '-' )
393+ try :
394+ if return_actual_key :
395+ actual_secret = retrieve_secret_from_key_vault (f"{ akv_key } " , scope_value , scope , addset_source )
396+ new_additional_fields [k ] = actual_secret
397+ else :
398+ new_additional_fields [k ] = ui_trigger_word
399+ except Exception as e :
400+ logging .error (f"Failed to retrieve plugin additionalField secret '{ k } ' from Key Vault: { e } " )
401+ raise Exception (f"Failed to retrieve plugin additionalField secret '{ k } ' from Key Vault: { e } " )
402+ updated ['additionalFields' ] = new_additional_fields
290403 return updated
404+ # Helper to delete plugin secrets from Key Vault
405+ def keyvault_plugin_delete_helper (plugin_dict , scope_value , scope = "global" ):
406+ """
407+ For plugin dicts, delete secrets from Key Vault for auth.key and any additionalFields key ending with '__Secret'.
408+ Only deletes if the value is a Key Vault reference.
409+
410+ Args:
411+ plugin_dict (dict): The plugin dictionary to process.
412+ scope_value (str): The value for the scope (e.g., plugin id).
413+ scope (str): The scope (e.g., 'user', 'global').
414+
415+ Returns:
416+ plugin_dict (dict): The original plugin dict.
417+ Raises:
418+ """
419+ if scope not in supported_scopes :
420+ logging .error (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
421+ raise ValueError (f"Scope '{ scope } ' is not supported. Supported scopes: { supported_scopes } " )
422+ settings = get_settings ()
423+ enable_key_vault_secret_storage = settings .get ("enable_key_vault_secret_storage" , False )
424+ key_vault_name = settings .get ("key_vault_name" , None )
425+ if not enable_key_vault_secret_storage or not key_vault_name :
426+ return plugin_dict
427+ source = "action"
428+ plugin_name = plugin_dict .get ('name' , 'plugin' )
429+ auth = plugin_dict .get ('auth' , {})
430+ if isinstance (auth , dict ):
431+ if 'key' in auth and auth ['key' ]:
432+ secret_name = auth ['key' ]
433+ if validate_secret_name_dynamic (secret_name ):
434+ try :
435+ key_vault_url = f"https://{ key_vault_name } { KEY_VAULT_DOMAIN } "
436+ client = SecretClient (vault_url = key_vault_url , credential = get_keyvault_credential ())
437+ client .begin_delete_secret (secret_name )
438+ except Exception as e :
439+ logging .error (f"Error deleting plugin secret '{ secret_name } ' for plugin '{ plugin_name } ': { e } " )
440+ raise Exception (f"Error deleting plugin secret '{ secret_name } ' for plugin '{ plugin_name } ': { e } " )
441+
442+ additional_fields = plugin_dict .get ('additionalFields' , {})
443+ if isinstance (additional_fields , dict ):
444+ for k , v in additional_fields .items ():
445+ if k .endswith ('__Secret' ) and v and validate_secret_name_dynamic (v ):
446+ addset_source = 'action-addset'
447+ base_field = k [:- 8 ] # Remove '__Secret'
448+ akv_key = f"{ plugin_name } -{ base_field } " .replace ('__' , '-' )
449+ try :
450+ keyvault_secret_name = build_full_secret_name (akv_key , scope_value , addset_source , scope )
451+ key_vault_url = f"https://{ key_vault_name } { KEY_VAULT_DOMAIN } "
452+ client = SecretClient (vault_url = key_vault_url , credential = get_keyvault_credential ())
453+ client .begin_delete_secret (keyvault_secret_name )
454+ except Exception as e :
455+ logging .error (f"Error deleting plugin additionalField secret '{ k } ' for plugin '{ plugin_name } ': { e } " )
456+ raise Exception (f"Error deleting plugin additionalField secret '{ k } ' for plugin '{ plugin_name } ': { e } " )
457+ return plugin_dict
291458
292459# Helper to delete agent secrets from Key Vault
293460def keyvault_agent_delete_helper (agent_dict , scope_value , scope = "global" ):
@@ -301,7 +468,7 @@ def keyvault_agent_delete_helper(agent_dict, scope_value, scope="global"):
301468 scope (str): The scope (e.g., 'user', 'global').
302469
303470 Returns:
304- None
471+ agent_dict (dict): The original agent dict.
305472 """
306473 settings = get_settings ()
307474 enable_key_vault_secret_storage = settings .get ("enable_key_vault_secret_storage" , False )
@@ -323,6 +490,7 @@ def keyvault_agent_delete_helper(agent_dict, scope_value, scope="global"):
323490 client .begin_delete_secret (secret_name )
324491 except Exception as e :
325492 logging .error (f"Error deleting secret '{ secret_name } ' for agent '{ agent_name } ': { e } " )
493+ raise Exception (f"Error deleting secret '{ secret_name } ' for agent '{ agent_name } ': { e } " )
326494 return agent_dict
327495
328496def get_keyvault_credential ():
0 commit comments