1111logger = logging .getLogger (__name__ )
1212
1313_SENSITIVE_KEYS = ("password" , "api_key" , "token" , "secret" )
14+ _MASK = "********"
15+
16+
17+ def _is_sensitive (key : str ) -> bool :
18+ return any (s in key .lower () for s in _SENSITIVE_KEYS )
1419
1520
1621def _sanitize_dict (d : dict ) -> dict :
@@ -19,36 +24,34 @@ def _sanitize_dict(d: dict) -> dict:
1924 for k , v in d .items ():
2025 if isinstance (v , dict ):
2126 result [k ] = _sanitize_dict (v )
22- elif isinstance (v , str ) and any (s in k .lower () for s in _SENSITIVE_KEYS ):
23- result [k ] = "********"
27+ elif isinstance (v , list ):
28+ result [k ] = [
29+ _sanitize_dict (item ) if isinstance (item , dict ) else item for item in v
30+ ]
31+ elif isinstance (v , str ) and _is_sensitive (k ):
32+ result [k ] = _MASK
2433 else :
2534 result [k ] = v
2635 return result
2736
2837
29- def _restore_sensitive (incoming : dict , current : dict ) -> dict :
30- """Replace masked '********' values with the real values from current config.
31-
32- When the frontend submits a config update it sends back the masked
33- placeholder for every sensitive field (password, token, …). Saving that
34- placeholder verbatim would overwrite the real credential with the literal
35- string '********'. This function walks the incoming dict and, wherever it
36- finds the placeholder, substitutes the value that is already stored in the
37- running settings.
38- """
39- result = {}
38+ def _restore_masked (incoming : dict , current : dict ) -> dict :
39+ """Replace masked sentinel values with real values from current config."""
4040 for k , v in incoming .items ():
41- if isinstance (v , dict ):
42- result [k ] = _restore_sensitive (v , current .get (k , {}))
43- elif (
44- isinstance (v , str )
45- and v == "********"
46- and any (s in k .lower () for s in _SENSITIVE_KEYS )
47- ):
48- result [k ] = current .get (k , v )
49- else :
50- result [k ] = v
51- return result
41+ if isinstance (v , dict ) and isinstance (current .get (k ), dict ):
42+ _restore_masked (v , current [k ])
43+ elif isinstance (v , list ) and isinstance (current .get (k ), list ):
44+ cur_list = current [k ]
45+ for i , item in enumerate (v ):
46+ if (
47+ isinstance (item , dict )
48+ and i < len (cur_list )
49+ and isinstance (cur_list [i ], dict )
50+ ):
51+ _restore_masked (item , cur_list [i ])
52+ elif v == _MASK and _is_sensitive (k ):
53+ incoming [k ] = current .get (k , v )
54+ return incoming
5255
5356
5457@router .get ("/get" , dependencies = [Depends (get_current_user )])
@@ -63,10 +66,8 @@ async def get_config():
6366async def update_config (config : Config ):
6467 """Persist and reload configuration from the supplied payload."""
6568 try :
66- incoming = config .dict ()
67- current = settings .dict ()
68- restored = _restore_sensitive (incoming , current )
69- settings .save (config_dict = restored )
69+ config_dict = _restore_masked (config .dict (), settings .dict ())
70+ settings .save (config_dict = config_dict )
7071 settings .load ()
7172 # update_rss()
7273 logger .info ("Config updated" )
0 commit comments