Marco Ricci commited on 2024-12-12 11:59:47
Zeige 2 geänderte Dateien mit 68 Einfügungen und 12 Löschungen.
In the "vault" derivation scheme, commands that edit or import a configuration actually merge with the existing configuration, for compatibility with the original vault(1). However, sometimes it is desired to replace the existing configuration instead, when importing or when setting a (service or global) configuration. We introduce a new `--overwrite-existing` command-line option that signals exactly that, and a `--merge-existing` option which signals the (previously implicit) default merging behavior.
| ... | ... |
@@ -1299,6 +1299,12 @@ class StorageManagementOption(OptionGroupOption): |
| 1299 | 1299 |
""" |
| 1300 | 1300 |
|
| 1301 | 1301 |
|
| 1302 |
+class CompatibilityOption(OptionGroupOption): |
|
| 1303 |
+ """Compatibility and incompatibility options for the CLI.""" |
|
| 1304 |
+ |
|
| 1305 |
+ option_group_name = 'Options concerning compatibility with other tools' |
|
| 1306 |
+ |
|
| 1307 |
+ |
|
| 1302 | 1308 |
def _validate_occurrence_constraint( |
| 1303 | 1309 |
ctx: click.Context, |
| 1304 | 1310 |
param: click.Parameter, |
| ... | ... |
@@ -1530,6 +1536,13 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
| 1530 | 1536 |
help='import saved settings from file PATH', |
| 1531 | 1537 |
cls=StorageManagementOption, |
| 1532 | 1538 |
) |
| 1539 |
+@click.option( |
|
| 1540 |
+ '--overwrite-existing/--merge-existing', |
|
| 1541 |
+ 'overwrite_config', |
|
| 1542 |
+ default=False, |
|
| 1543 |
+ help='overwrite or merge (default) the existing configuration', |
|
| 1544 |
+ cls=CompatibilityOption, |
|
| 1545 |
+) |
|
| 1533 | 1546 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
| 1534 | 1547 |
@standard_logging_options |
| 1535 | 1548 |
@click.argument('service', required=False)
|
| ... | ... |
@@ -1556,6 +1569,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1556 | 1569 |
clear_all_settings: bool = False, |
| 1557 | 1570 |
export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
| 1558 | 1571 |
import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
| 1572 |
+ overwrite_config: bool = False, |
|
| 1559 | 1573 |
) -> None: |
| 1560 | 1574 |
"""Derive a passphrase using the vault(1) derivation scheme. |
| 1561 | 1575 |
|
| ... | ... |
@@ -1647,6 +1661,11 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1647 | 1661 |
must be open for reading and yield `str` values. Otherwise, |
| 1648 | 1662 |
a filename to open for reading. Using `-` for standard |
| 1649 | 1663 |
input is supported. |
| 1664 |
+ overwrite_config: |
|
| 1665 |
+ Command-line arguments `--overwrite-existing` (True) and |
|
| 1666 |
+ `--merge-existing` (False). Controls whether config saving |
|
| 1667 |
+ and config importing overwrite existing configurations, or |
|
| 1668 |
+ merge them section-wise instead. |
|
| 1650 | 1669 |
|
| 1651 | 1670 |
""" # noqa: D301 |
| 1652 | 1671 |
logger = logging.getLogger(PROG_NAME) |
| ... | ... |
@@ -1665,6 +1684,8 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1665 | 1684 |
group = StorageManagementOption |
| 1666 | 1685 |
elif isinstance(param, LoggingOption): |
| 1667 | 1686 |
group = LoggingOption |
| 1687 |
+ elif isinstance(param, CompatibilityOption): |
|
| 1688 |
+ group = CompatibilityOption |
|
| 1668 | 1689 |
elif isinstance(param, OptionGroupOption): |
| 1669 | 1690 |
raise AssertionError( # noqa: DOC501,TRY003,TRY004 |
| 1670 | 1691 |
f'Unknown option group for {param!r}' # noqa: EM102
|
| ... | ... |
@@ -1897,8 +1918,12 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1897 | 1918 |
cast(dict[str, Any], value), |
| 1898 | 1919 |
form=form, |
| 1899 | 1920 |
) |
| 1921 |
+ if overwrite_config: |
|
| 1922 |
+ put_config(maybe_config) |
|
| 1923 |
+ else: |
|
| 1900 | 1924 |
configuration = get_config() |
| 1901 |
- merged_config: collections.ChainMap[str, Any] = collections.ChainMap( |
|
| 1925 |
+ merged_config: collections.ChainMap[str, Any] = ( |
|
| 1926 |
+ collections.ChainMap( |
|
| 1902 | 1927 |
{
|
| 1903 | 1928 |
'services': collections.ChainMap( |
| 1904 | 1929 |
maybe_config['services'], |
| ... | ... |
@@ -1912,6 +1937,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1912 | 1937 |
if 'global' in configuration |
| 1913 | 1938 |
else {},
|
| 1914 | 1939 |
) |
| 1940 |
+ ) |
|
| 1915 | 1941 |
new_config: Any = {
|
| 1916 | 1942 |
k: dict(v) if isinstance(v, collections.ChainMap) else v |
| 1917 | 1943 |
for k, v in sorted(merged_config.items()) |
| ... | ... |
@@ -2022,10 +2048,14 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 2022 | 2048 |
f'actual settings' |
| 2023 | 2049 |
) |
| 2024 | 2050 |
raise click.UsageError(msg) |
| 2025 |
- if service: |
|
| 2026 |
- configuration['services'].setdefault(service, {}).update(view) # type: ignore[typeddict-item]
|
|
| 2027 |
- else: |
|
| 2028 |
- configuration.setdefault('global', {}).update(view) # type: ignore[typeddict-item]
|
|
| 2051 |
+ subtree: dict[str, Any] = ( |
|
| 2052 |
+ configuration['services'].setdefault(service, {}) # type: ignore[assignment]
|
|
| 2053 |
+ if service |
|
| 2054 |
+ else configuration.setdefault('global', {})
|
|
| 2055 |
+ ) |
|
| 2056 |
+ if overwrite_config: |
|
| 2057 |
+ subtree.clear() |
|
| 2058 |
+ subtree.update(view) |
|
| 2029 | 2059 |
assert _types.is_vault_config( |
| 2030 | 2060 |
configuration |
| 2031 | 2061 |
), f'Invalid vault configuration: {configuration!r}'
|
| ... | ... |
@@ -2525,19 +2525,26 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2525 | 2525 |
|
| 2526 | 2526 |
@stateful.rule( |
| 2527 | 2527 |
settings_obj=stateful.consumes(settings), |
| 2528 |
+ overwrite=strategies.booleans(), |
|
| 2528 | 2529 |
) |
| 2529 | 2530 |
def set_globals( |
| 2530 | 2531 |
self, |
| 2531 | 2532 |
settings_obj: _types.VaultConfigGlobalSettings, |
| 2533 |
+ overwrite: bool, |
|
| 2532 | 2534 |
) -> None: |
| 2533 |
- self.current_config['global'] = settings_obj |
|
| 2535 |
+ if overwrite: |
|
| 2536 |
+ self.current_config['global'] = {}
|
|
| 2537 |
+ self.current_config.setdefault('global', {}).update(settings_obj)
|
|
| 2534 | 2538 |
assert _types.is_vault_config(self.current_config) |
| 2535 | 2539 |
# NOTE: This relies on settings_obj containing only the keys |
| 2536 | 2540 |
# "length", "repeat", "upper", "lower", "number", "space", |
| 2537 | 2541 |
# "dash" and "symbol". |
| 2538 | 2542 |
_result = self.runner.invoke( |
| 2539 | 2543 |
cli.derivepassphrase_vault, |
| 2540 |
- ['--config'] |
|
| 2544 |
+ [ |
|
| 2545 |
+ '--config', |
|
| 2546 |
+ '--overwrite-existing' if overwrite else '--merge-existing', |
|
| 2547 |
+ ] |
|
| 2541 | 2548 |
+ [f'--{key}={value}' for key, value in settings_obj.items()],
|
| 2542 | 2549 |
catch_exceptions=False, |
| 2543 | 2550 |
) |
| ... | ... |
@@ -2556,20 +2563,29 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2556 | 2563 |
@stateful.rule( |
| 2557 | 2564 |
service=known_services, |
| 2558 | 2565 |
settings_obj=stateful.consumes(settings), |
| 2566 |
+ overwrite=strategies.booleans(), |
|
| 2559 | 2567 |
) |
| 2560 | 2568 |
def set_service( |
| 2561 | 2569 |
self, |
| 2562 | 2570 |
service: str, |
| 2563 | 2571 |
settings_obj: _types.VaultConfigServicesSettings, |
| 2572 |
+ overwrite: bool, |
|
| 2564 | 2573 |
) -> None: |
| 2565 |
- self.current_config['services'][service] = settings_obj |
|
| 2574 |
+ if overwrite: |
|
| 2575 |
+ self.current_config['services'][service] = {}
|
|
| 2576 |
+ self.current_config['services'].setdefault(service, {}).update(
|
|
| 2577 |
+ settings_obj |
|
| 2578 |
+ ) |
|
| 2566 | 2579 |
assert _types.is_vault_config(self.current_config) |
| 2567 | 2580 |
# NOTE: This relies on settings_obj containing only the keys |
| 2568 | 2581 |
# "length", "repeat", "upper", "lower", "number", "space", |
| 2569 | 2582 |
# "dash" and "symbol". |
| 2570 | 2583 |
_result = self.runner.invoke( |
| 2571 | 2584 |
cli.derivepassphrase_vault, |
| 2572 |
- ['--config'] |
|
| 2585 |
+ [ |
|
| 2586 |
+ '--config', |
|
| 2587 |
+ '--overwrite-existing' if overwrite else '--merge-existing', |
|
| 2588 |
+ ] |
|
| 2573 | 2589 |
+ [f'--{key}={value}' for key, value in settings_obj.items()]
|
| 2574 | 2590 |
+ ['--', service], |
| 2575 | 2591 |
catch_exceptions=False, |
| ... | ... |
@@ -2627,13 +2643,23 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2627 | 2643 |
|
| 2628 | 2644 |
@stateful.rule( |
| 2629 | 2645 |
config=stateful.consumes(configurations), |
| 2646 |
+ overwrite=strategies.booleans(), |
|
| 2647 |
+ ) |
|
| 2648 |
+ def import_configuraton( |
|
| 2649 |
+ self, |
|
| 2650 |
+ config: _types.VaultConfig, |
|
| 2651 |
+ overwrite: bool, |
|
| 2652 |
+ ) -> None: |
|
| 2653 |
+ self.current_config = ( |
|
| 2654 |
+ self.fold_configs(config, self.current_config) |
|
| 2655 |
+ if not overwrite |
|
| 2656 |
+ else config |
|
| 2630 | 2657 |
) |
| 2631 |
- def import_configuraton(self, config: _types.VaultConfig) -> None: |
|
| 2632 |
- self.current_config = self.fold_configs(config, self.current_config) |
|
| 2633 | 2658 |
assert _types.is_vault_config(self.current_config) |
| 2634 | 2659 |
_result = self.runner.invoke( |
| 2635 | 2660 |
cli.derivepassphrase_vault, |
| 2636 |
- ['--import', '-'], |
|
| 2661 |
+ ['--import', '-'] |
|
| 2662 |
+ + (['--overwrite-existing'] if overwrite else []), |
|
| 2637 | 2663 |
input=json.dumps(config), |
| 2638 | 2664 |
catch_exceptions=False, |
| 2639 | 2665 |
) |
| 2640 | 2666 |