Marco Ricci commited on 2024-10-15 12:47:49
Zeige 2 geänderte Dateien mit 71 Einfügungen und 0 Löschungen.
The `derivepassphrase` command-line largely, and vault(1) completely, treats an empty service name the same as if no service name had been supplied, switching over to global operation instead of service-specific operation (or erroring out in case a service must be given). For `derivepassphrase`, this is mostly a user interface issue – the underlying machinery supports empty service names –, but kept for compatibility with vault(1). However, this is very easy to diagnose, and the user would benefit from seeing a warning about a seemingly omitted service name. So do that, and issue a warning upon encountering an empty service name on the command-line or in an imported configuration. (The warning message changes slightly in each case.) Also, add explicit tests for these two scenarios that trigger the warning.
... | ... |
@@ -1358,6 +1358,17 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1358 | 1358 |
msg = f'{opt_str} does not take a SERVICE argument' |
1359 | 1359 |
raise click.UsageError(msg) |
1360 | 1360 |
|
1361 |
+ if service == '': # noqa: PLC1901 |
|
1362 |
+ click.echo( |
|
1363 |
+ ( |
|
1364 |
+ f'{PROG_NAME}: Warning: An empty SERVICE is not ' |
|
1365 |
+ f'supported by vault(1). For compatibility, this will be ' |
|
1366 |
+ f'treated as if SERVICE was not supplied, i.e., it will ' |
|
1367 |
+ f'error out, or operate on global settings.' |
|
1368 |
+ ), |
|
1369 |
+ err=True, |
|
1370 |
+ ) |
|
1371 |
+ |
|
1361 | 1372 |
if edit_notes: |
1362 | 1373 |
assert service is not None |
1363 | 1374 |
configuration = get_config() |
... | ... |
@@ -1429,6 +1440,15 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1429 | 1440 |
f'{json.dumps(step.old_value)}.' |
1430 | 1441 |
) |
1431 | 1442 |
click.echo(err_msg, err=True) |
1443 |
+ if '' in maybe_config['services']: |
|
1444 |
+ err_msg = ( |
|
1445 |
+ f'{PROG_NAME}: Warning: An empty SERVICE is not ' |
|
1446 |
+ f'supported by vault(1), and the empty-string service ' |
|
1447 |
+ f'settings will be inaccessible and ineffective. ' |
|
1448 |
+ f'To ensure that vault(1) and {PROG_NAME} see the settings, ' |
|
1449 |
+ f'move them into the "global" section.' |
|
1450 |
+ ) |
|
1451 |
+ click.echo(err_msg, err=True) |
|
1432 | 1452 |
form = cast( |
1433 | 1453 |
Literal['NFC', 'NFD', 'NFKC', 'NFKD'], |
1434 | 1454 |
maybe_config.get('global', {}).get( |
... | ... |
@@ -621,6 +621,57 @@ class TestCLI: |
621 | 621 |
result = tests.ReadableResult.parse(_result) |
622 | 622 |
assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
623 | 623 |
|
624 |
+ def test_211a_empty_service_name_causes_warning( |
|
625 |
+ self, |
|
626 |
+ monkeypatch: pytest.MonkeyPatch, |
|
627 |
+ ) -> None: |
|
628 |
+ def expected_warning_line(line: str) -> bool: |
|
629 |
+ return is_harmless_config_import_warning_line(line) or ( |
|
630 |
+ ' Warning: An empty SERVICE is not supported by vault(1)' |
|
631 |
+ in line |
|
632 |
+ ) |
|
633 |
+ |
|
634 |
+ monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
|
635 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
636 |
+ with tests.isolated_vault_config( |
|
637 |
+ monkeypatch=monkeypatch, |
|
638 |
+ runner=runner, |
|
639 |
+ config={'services': {}}, |
|
640 |
+ ): |
|
641 |
+ _result = runner.invoke( |
|
642 |
+ cli.derivepassphrase_vault, |
|
643 |
+ ['--config', '--length=30', '--', ''], |
|
644 |
+ catch_exceptions=False, |
|
645 |
+ ) |
|
646 |
+ result = tests.ReadableResult.parse(_result) |
|
647 |
+ assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
|
648 |
+ assert result.stderr is not None, 'expected known error output' |
|
649 |
+ assert all( |
|
650 |
+ expected_warning_line(line) |
|
651 |
+ for line in result.stderr.splitlines(False) |
|
652 |
+ ), 'expected known error output' |
|
653 |
+ assert cli._load_config() == { |
|
654 |
+ 'global': {'length': 30}, |
|
655 |
+ 'services': {}, |
|
656 |
+ }, 'requested configuration change was not applied' |
|
657 |
+ _result = runner.invoke( |
|
658 |
+ cli.derivepassphrase_vault, |
|
659 |
+ ['--import', '-'], |
|
660 |
+ input=json.dumps({'services': {'': {'length': 40}}}), |
|
661 |
+ catch_exceptions=False, |
|
662 |
+ ) |
|
663 |
+ result = tests.ReadableResult.parse(_result) |
|
664 |
+ assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
|
665 |
+ assert result.stderr is not None, 'expected known error output' |
|
666 |
+ assert all( |
|
667 |
+ expected_warning_line(line) |
|
668 |
+ for line in result.stderr.splitlines(False) |
|
669 |
+ ), 'expected known error output' |
|
670 |
+ assert cli._load_config() == { |
|
671 |
+ 'global': {'length': 30}, |
|
672 |
+ 'services': {'': {'length': 40}}, |
|
673 |
+ }, 'requested configuration change was not applied' |
|
674 |
+ |
|
624 | 675 |
@pytest.mark.parametrize( |
625 | 676 |
['options', 'service'], |
626 | 677 |
[ |
627 | 678 |