Skip to content

Commit 3b0b743

Browse files
committed
add keyvault to global actions loading
1 parent def744b commit 3b0b743

File tree

2 files changed

+252
-61
lines changed

2 files changed

+252
-61
lines changed

application/single_app/functions_keyvault.py

Lines changed: 196 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# functions_keyvault.py
22

33
import re
4+
import logging
45
from config import *
56
from functions_authentication import *
67
from functions_settings import *
@@ -24,7 +25,9 @@
2425
'storage_account',
2526
'cognitive_service',
2627
'action',
27-
'agent'
28+
'action-addset',
29+
'agent',
30+
'other'
2831
]
2932

3033
supported_scopes = [
@@ -33,6 +36,13 @@
3336
'group'
3437
]
3538

39+
supported_action_auth_types = [
40+
'key',
41+
'servicePrincipal',
42+
'basic',
43+
'connection_string'
44+
]
45+
3646
ui_trigger_word = "Stored_In_KeyVault"
3747

3848
def 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

134153
def 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

154174
def 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

255283
def 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
293460
def 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

328496
def get_keyvault_credential():

0 commit comments

Comments
 (0)