1- """Unified secret store service for the application."""
1+ """Unified secret store service for the application.
2+
3+ Logic summary:
4+
5+ 1. Fetch credential config from state store.
6+
7+ 2. Determine mode: Multi-key if (credentialSource == 'direct' OR secret-path is defined), Single-key otherwise.
8+
9+ 3. Fetch secrets accordingly: Multi-key uses secret_path if credentialSource == "agent" else credential_guid, Single-key fetches each field individually.
10+
11+ 4. Merge & resolve secrets.
12+ """
213
314import collections .abc
415import copy
516import json
617import uuid
18+ from enum import Enum
719from typing import Any , Dict
820
921from dapr .clients import DaprClient
2335logger = get_logger (__name__ )
2436
2537
38+ class CredentialSource (Enum ):
39+ """Enumeration of credential source types."""
40+
41+ DIRECT = "direct"
42+ AGENT = "agent"
43+
44+
45+ class SecretMode (Enum ):
46+ """Enumeration of secret retrieval modes."""
47+
48+ MULTI_KEY = "multi-key"
49+ SINGLE_KEY = "single-key"
50+
51+
2652class SecretStore :
27- """Unified secret store service for handling secret management."""
53+ """Unified secret store service for handling secret management across providers ."""
2854
2955 @classmethod
3056 async def get_credentials (cls , credential_guid : str ) -> Dict [str , Any ]:
@@ -33,6 +59,8 @@ async def get_credentials(cls, credential_guid: str) -> Dict[str, Any]:
3359 This method retrieves credential configuration from the state store and resolves
3460 any secret references by fetching actual values from the secret store.
3561
62+ Supports Multi-key mode (direct / has secret-path) and Single-key mode (no secret-path, non-direct).
63+
3664 Args:
3765 credential_guid (str): The unique GUID of the credential configuration to resolve.
3866
@@ -48,7 +76,6 @@ async def get_credentials(cls, credential_guid: str) -> Dict[str, Any]:
4876 >>> creds = await SecretStore.get_credentials("db-cred-abc123")
4977 >>> print(f"Connecting to {creds['host']}:{creds['port']}")
5078 >>> # Password is automatically resolved from secret store
51-
5279 >>> # Handle resolution errors
5380 >>> try:
5481 ... creds = await SecretStore.get_credentials("invalid-guid")
@@ -62,13 +89,44 @@ async def _get_credentials_async(credential_guid: str) -> Dict[str, Any]:
6289 credential_guid , StateType .CREDENTIALS
6390 )
6491
65- # Fetch secret data from secret store
66- secret_key = credential_config .get ("secret-path" , credential_guid )
67- secret_data = SecretStore .get_secret (secret_key = secret_key )
92+ credential_source_str = credential_config .get (
93+ "credentialSource" , CredentialSource .DIRECT .value
94+ )
95+ try :
96+ credential_source = CredentialSource (credential_source_str )
97+ except ValueError :
98+ credential_source = CredentialSource .DIRECT
99+ secret_path = credential_config .get ("secret-path" )
100+
101+ secret_data : Dict [str , Any ] = {}
102+
103+ # Decide mode
104+ if credential_source == CredentialSource .DIRECT or secret_path :
105+ mode = SecretMode .MULTI_KEY
106+ else :
107+ mode = SecretMode .SINGLE_KEY
108+
109+ # Multi-key secret fetch (direct or has secret-path)
110+ if mode == SecretMode .MULTI_KEY :
111+ key_to_fetch = (
112+ secret_path
113+ if credential_source == CredentialSource .AGENT
114+ else credential_guid
115+ )
116+ try :
117+ logger .debug (f"Fetching multi-key secret from '{ key_to_fetch } '" )
118+ secret_data = cls .get_secret (secret_key = key_to_fetch )
119+ except Exception as e :
120+ logger .warning (
121+ f"Failed to fetch secret bundle '{ key_to_fetch } ': { e } "
122+ )
123+
124+ # Single-key mode → per-field secret lookup
125+ else :
126+ secret_data = cls ._fetch_single_key_secrets (credential_config )
68127
69- # Resolve credentials
70- credential_source = credential_config .get ("credentialSource" , "direct" )
71- if credential_source == "direct" :
128+ # Merge or resolve references
129+ if credential_source == CredentialSource .DIRECT :
72130 credential_config .update (secret_data )
73131 return credential_config
74132 else :
@@ -78,12 +136,59 @@ async def _get_credentials_async(credential_guid: str) -> Dict[str, Any]:
78136 # Run async operations directly
79137 return await _get_credentials_async (credential_guid )
80138 except Exception as e :
81- logger .error (f"Error resolving credentials: { str (e )} " )
139+ logger .error (f"Error resolving credentials for { credential_guid } : { str (e )} " )
82140 raise CommonError (
83141 CommonError .CREDENTIALS_RESOLUTION_ERROR ,
84142 f"Failed to resolve credentials: { str (e )} " ,
85143 )
86144
145+ # Secret resolution helpers
146+
147+ @classmethod
148+ def _fetch_single_key_secrets (
149+ cls , credential_config : Dict [str , Any ]
150+ ) -> Dict [str , Any ]:
151+ """Fetch secrets in single-key mode by looking up each field individually.
152+
153+ Args:
154+ credential_config: The credential configuration dictionary
155+
156+ Returns:
157+ Dictionary containing collected secret values
158+ """
159+ logger .debug ("Single-key mode: fetching secrets per field" )
160+ collected = {}
161+ for field , value in credential_config .items ():
162+ if isinstance (value , str ):
163+ try :
164+ single_secret = cls .get_secret (value )
165+ if single_secret :
166+ for k , v in single_secret .items ():
167+ # Only filter out None and empty strings, not all falsy values.
168+ # This preserves valid secret values like False, 0, 0.0 which are
169+ # legitimate secret values that should not be excluded.
170+ if v is None or v == "" :
171+ continue
172+ collected [k ] = v
173+ except Exception as e :
174+ logger .debug (f"Skipping '{ field } ' → '{ value } ' ({ e } )" )
175+ elif field == "extra" and isinstance (value , dict ):
176+ # Recursively process string values in the extra dictionary
177+ for extra_key , extra_value in value .items ():
178+ if isinstance (extra_value , str ):
179+ try :
180+ single_secret = cls .get_secret (extra_value )
181+ if single_secret :
182+ for k , v in single_secret .items ():
183+ if v is None or v == "" :
184+ continue
185+ collected [k ] = v
186+ except Exception as e :
187+ logger .debug (
188+ f"Skipping 'extra.{ extra_key } ' → '{ extra_value } ' ({ e } )"
189+ )
190+ return collected
191+
87192 @classmethod
88193 def resolve_credentials (
89194 cls , credential_config : Dict [str , Any ], secret_data : Dict [str , Any ]
@@ -106,7 +211,6 @@ def resolve_credentials(
106211 >>> secrets = {"db_password_key": "actual_secret_password"}
107212 >>> resolved = SecretStore.resolve_credentials(config, secrets)
108213 >>> print(resolved) # {"host": "db.example.com", "password": "actual_secret_password"}
109-
110214 >>> # Resolution with nested 'extra' fields
111215 >>> config = {
112216 ... "host": "db.example.com",
@@ -125,11 +229,8 @@ def resolve_credentials(
125229 # Apply the same substitution to the 'extra' dictionary if it exists
126230 if "extra" in credentials and isinstance (credentials ["extra" ], dict ):
127231 for key , value in list (credentials ["extra" ].items ()):
128- if isinstance (value , str ):
129- if value in secret_data :
130- credentials ["extra" ][key ] = secret_data [value ]
131- elif value in secret_data .get ("extra" , {}):
132- credentials ["extra" ][key ] = secret_data ["extra" ][value ]
232+ if isinstance (value , str ) and value in secret_data :
233+ credentials ["extra" ][key ] = secret_data [value ]
133234
134235 return credentials
135236
@@ -143,7 +244,7 @@ def get_deployment_secret(cls) -> Dict[str, Any]:
143244
144245 Returns:
145246 Dict[str, Any]: Deployment configuration data, or empty dict if
146- component is unavailable or fetch fails.
247+ component is unavailable or fetch fails.
147248
148249 Examples:
149250 >>> # Get deployment configuration
@@ -153,15 +254,14 @@ def get_deployment_secret(cls) -> Dict[str, Any]:
153254 ... print(f"Region: {config.get('region')}")
154255 >>> else:
155256 ... print("No deployment configuration available")
156-
157257 >>> # Use in application initialization
158258 >>> deployment_config = SecretStore.get_deployment_secret()
159259 >>> if deployment_config.get('debug_mode'):
160260 ... logging.getLogger().setLevel(logging.DEBUG)
161261 """
162262 if not is_component_registered (DEPLOYMENT_SECRET_STORE_NAME ):
163263 logger .warning (
164- f"Deployment secret store component '{ DEPLOYMENT_SECRET_STORE_NAME } ' is not registered"
264+ f"Deployment secret store component '{ DEPLOYMENT_SECRET_STORE_NAME } ' not registered. "
165265 )
166266 return {}
167267
@@ -182,7 +282,7 @@ def get_secret(
182282
183283 Args:
184284 secret_key (str): Key of the secret to fetch from the secret store.
185- component_name (str, optional ): Name of the Dapr component to fetch from.
285+ component_name (str): Name of the Dapr component to fetch from.
186286 Defaults to SECRET_STORE_NAME.
187287
188288 Returns:
@@ -199,7 +299,6 @@ def get_secret(
199299 >>> # Get database credentials
200300 >>> db_secret = SecretStore.get_secret("database-credentials")
201301 >>> print(f"Host: {db_secret.get('host')}")
202-
203302 >>> # Get from specific component
204303 >>> api_secret = SecretStore.get_secret(
205304 ... "api-keys",
@@ -217,7 +316,7 @@ def get_secret(
217316 return cls ._process_secret_data (dapr_secret_object .secret )
218317 except Exception as e :
219318 logger .error (
220- f"Failed to fetch secret using component { component_name } : { str (e )} "
319+ f"Failed to fetch secret using component ' { component_name } ' : { str (e )} "
221320 )
222321 raise
223322
@@ -235,17 +334,34 @@ def _process_secret_data(cls, secret_data: Any) -> Dict[str, Any]:
235334 if isinstance (secret_data , collections .abc .Mapping ):
236335 secret_data = dict (secret_data )
237336
238- # If the dict has a single key and its value is a JSON string, parse it
239- if len (secret_data ) == 1 and isinstance (next (iter (secret_data .values ())), str ):
337+ # Handle single-key secrets gracefully
338+ if len (secret_data ) == 1 :
339+ k , v = next (iter (secret_data .items ()))
340+ return cls ._handle_single_key_secret (k , v )
341+
342+ return secret_data
343+
344+ # Utility helpers
345+
346+ @classmethod
347+ def _handle_single_key_secret (cls , key : str , value : Any ) -> Dict [str , Any ]:
348+ """Handle single-key secret by attempting to parse JSON value.
349+
350+ Args:
351+ key: The secret key.
352+ value: The secret value (may be a JSON string).
353+
354+ Returns:
355+ Dictionary with parsed JSON if value is valid JSON dict, otherwise {key: value}.
356+ """
357+ if isinstance (value , str ):
240358 try :
241- parsed = json .loads (next ( iter ( secret_data . values ())) )
359+ parsed = json .loads (value )
242360 if isinstance (parsed , dict ):
243- secret_data = parsed
244- except Exception as e :
245- logger .error (f"Failed to parse secret data: { e } " )
361+ return parsed
362+ except Exception :
246363 pass
247-
248- return secret_data
364+ return {key : value }
249365
250366 @classmethod
251367 def apply_secret_values (
@@ -275,7 +391,6 @@ def apply_secret_values(
275391 ... "db_password": "secure_db_password"
276392 ... }
277393 >>> resolved = SecretStore.apply_secret_values(source, secrets)
278-
279394 >>> # With nested extra fields
280395 >>> source = {
281396 ... "host": "api.example.com",
@@ -328,7 +443,6 @@ async def save_secret(cls, config: Dict[str, Any]) -> str:
328443 ... }
329444 >>> guid = await SecretStore.save_secret(config)
330445 >>> print(f"Stored credentials with GUID: {guid}")
331-
332446 >>> # Later retrieve these credentials
333447 >>> retrieved = await SecretStore.get_credentials(guid)
334448 """
0 commit comments