Fix empty key handling in `vault` config, and harmonize warning text
Marco Ricci

Marco Ricci commited on 2024-12-20 11:39:45
Zeige 3 geänderte Dateien mit 73 Einfügungen und 8 Löschungen.


Correctly handle empty `key` settings in the `vault` configuration: if
at the global level, it is equivalent to eliding the setting, but at the
service level, it explicitly disables key usage (even if a global key is
set).  This implies that both cases need different cleanup steps.

Harmonize the warning message for ineffective passphrases for services:
always include the affected service name.

If during configuration import we find a key shadowing a passphrase,
issue the same ineffective passphrase warning as would happen during
interactive configuration or interactive use.

(The tests need minor modifications and reparametrization to continue to
cover all warning cases.)
... ...
@@ -517,7 +517,7 @@ def clean_up_falsy_vault_config_values(  # noqa: C901,PLR0912
517 517
                         )
518 518
                     )
519 519
                     service_obj[key] = ''
520
-            elif key in {'notes', 'key'}:
520
+            elif key == 'notes':
521 521
                 if not js_truthiness(value):
522 522
                     cleanup_completed.append(
523 523
                         CleanupStep(
... ...
@@ -525,6 +525,22 @@ def clean_up_falsy_vault_config_values(  # noqa: C901,PLR0912
525 525
                         )
526 526
                     )
527 527
                     service_obj.pop(key)
528
+            elif key == 'key':
529
+                if not js_truthiness(value):
530
+                    if path == ['global']:
531
+                        cleanup_completed.append(
532
+                            CleanupStep(
533
+                                (*path, key), service_obj[key], 'remove', None
534
+                            )
535
+                        )
536
+                        service_obj.pop(key)
537
+                    else:
538
+                        cleanup_completed.append(
539
+                            CleanupStep(
540
+                                (*path, key), service_obj[key], 'replace', ''
541
+                            )
542
+                        )
543
+                        service_obj[key] = ''
528 544
             elif key == 'length':
529 545
                 if not js_truthiness(value):
530 546
                     cleanup_completed.append(
... ...
@@ -2025,6 +2025,29 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
2025 2025
                 )
2026 2026
         except AssertionError as e:
2027 2027
             err('The configuration file is invalid.  ' + str(e))
2028
+        global_obj = maybe_config.get('global', {})
2029
+        has_key = _types.js_truthiness(global_obj.get('key'))
2030
+        has_phrase = _types.js_truthiness(global_obj.get('phrase'))
2031
+        if has_key and has_phrase:
2032
+            logger.warning(
2033
+                'Setting a global passphrase is ineffective '
2034
+                'because a key is also set.'
2035
+            )
2036
+        for service_name, service_obj in maybe_config['services'].items():
2037
+            has_key = _types.js_truthiness(
2038
+                service_obj.get('key')
2039
+            ) or _types.js_truthiness(global_obj.get('key'))
2040
+            has_phrase = _types.js_truthiness(
2041
+                service_obj.get('phrase')
2042
+            ) or _types.js_truthiness(global_obj.get('phrase'))
2043
+            if has_key and has_phrase:
2044
+                logger.warning(
2045
+                    (
2046
+                        'Setting a service passphrase is ineffective '
2047
+                        'because a key is also set: %s'
2048
+                    ),
2049
+                    json.dumps(service_name),
2050
+                )
2028 2051
         if overwrite_config:
2029 2052
             put_config(maybe_config)
2030 2053
         else:
... ...
@@ -2153,12 +2176,18 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
2153 2176
                 except AssertionError as e:
2154 2177
                     err('The configuration file is invalid.  ' + str(e))
2155 2178
                 if 'key' in settings:
2179
+                    if service:
2156 2180
                         logger.warning(
2157 2181
                             (
2158
-                            'Setting a %s passphrase is ineffective '
2159
-                            'because a key is also set.'
2182
+                                'Setting a service passphrase is ineffective '
2183
+                                'because a key is also set: %s'
2160 2184
                             ),
2161
-                        settings_type,
2185
+                            json.dumps(service),
2186
+                        )
2187
+                    else:
2188
+                        logger.warning(
2189
+                            'Setting a global passphrase is ineffective '
2190
+                            'because a key is also set.'
2162 2191
                         )
2163 2192
             if not view.maps[0]:
2164 2193
                 settings_type = 'service' if service else 'global'
... ...
@@ -221,7 +221,7 @@ def is_harmless_config_import_warning(record: tuple[str, int, str]) -> bool:
221 221
         ),
222 222
         (
223 223
             'Setting a service passphrase is ineffective '
224
-            'because a key is also set.'
224
+            'because a key is also set:'
225 225
         ),
226 226
     ]
227 227
     return any(tests.warning_emitted(w, [record]) for w in possible_warnings)
... ...
@@ -479,8 +479,17 @@ class TestCLI:
479 479
         ), 'expected known output'
480 480
 
481 481
     @pytest.mark.parametrize(
482
-        'config',
482
+        ['config', 'command_line'],
483 483
         [
484
+            pytest.param(
485
+                {
486
+                    'global': {'key': DUMMY_KEY1_B64},
487
+                    'services': {},
488
+                },
489
+                ['--config', '-p'],
490
+                id='global',
491
+            ),
492
+            pytest.param(
484 493
                 {
485 494
                     'services': {
486 495
                         DUMMY_SERVICE: {
... ...
@@ -489,18 +498,26 @@ class TestCLI:
489 498
                         },
490 499
                     },
491 500
                 },
501
+                ['--config', '-p', '--', DUMMY_SERVICE],
502
+                id='service',
503
+            ),
504
+            pytest.param(
492 505
                 {
493 506
                     'global': {'key': DUMMY_KEY1_B64},
494 507
                     'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()},
495 508
                 },
509
+                ['--config', '-p', '--', DUMMY_SERVICE],
510
+                id='service-over-global',
511
+            ),
496 512
         ],
497 513
     )
498
-    def test_206_setting_service_phrase_thus_overriding_key_in_config(
514
+    def test_206_setting_phrase_thus_overriding_key_in_config(
499 515
         self,
500 516
         monkeypatch: pytest.MonkeyPatch,
501 517
         running_ssh_agent: tests.RunningSSHAgentInfo,
502 518
         caplog: pytest.LogCaptureFixture,
503 519
         config: _types.VaultConfig,
520
+        command_line: list[str],
504 521
     ) -> None:
505 522
         with monkeypatch.context():
506 523
             monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket)
... ...
@@ -516,7 +533,7 @@ class TestCLI:
516 533
             ):
517 534
                 _result = runner.invoke(
518 535
                     cli.derivepassphrase_vault,
519
-                    ['--config', '-p', '--', DUMMY_SERVICE],
536
+                    command_line,
520 537
                     input=DUMMY_PASSPHRASE,
521 538
                     catch_exceptions=False,
522 539
                 )
... ...
@@ -529,6 +546,9 @@ class TestCLI:
529 546
         assert tests.warning_emitted(
530 547
             'Setting a service passphrase is ineffective ',
531 548
             caplog.record_tuples,
549
+        ) or tests.warning_emitted(
550
+            'Setting a global passphrase is ineffective ',
551
+            caplog.record_tuples,
532 552
         ), 'expected known warning message'
533 553
         assert all(map(is_warning_line, result.stderr.splitlines(True)))
534 554
         assert all(
535 555