Marco Ricci commited on 2024-12-20 17:00:48
Zeige 3 geänderte Dateien mit 94 Einfügungen und 3 Löschungen.
When configuring a `vault` service, or the global settings, a new configuration option `--unset FIELD` will unset the specified `FIELD` from the service or global settings prior to and addition to applying the requested settings changes. This way, the user can update all settings on the command-line (except for the "notes") without manually editing and reimporting a configuration export. (It is permissible to only unset settings, without applying any other configuration changes.)
| ... | ... |
@@ -1635,6 +1635,28 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
| 1635 | 1635 |
help='overwrite or merge (default) the existing configuration', |
| 1636 | 1636 |
cls=CompatibilityOption, |
| 1637 | 1637 |
) |
| 1638 |
+@click.option( |
|
| 1639 |
+ '--unset', |
|
| 1640 |
+ 'unset_settings', |
|
| 1641 |
+ multiple=True, |
|
| 1642 |
+ type=click.Choice([ |
|
| 1643 |
+ 'phrase', |
|
| 1644 |
+ 'key', |
|
| 1645 |
+ 'length', |
|
| 1646 |
+ 'repeat', |
|
| 1647 |
+ 'lower', |
|
| 1648 |
+ 'upper', |
|
| 1649 |
+ 'number', |
|
| 1650 |
+ 'space', |
|
| 1651 |
+ 'dash', |
|
| 1652 |
+ 'symbol', |
|
| 1653 |
+ ]), |
|
| 1654 |
+ help=( |
|
| 1655 |
+ 'with --config, also unsets the given setting; ' |
|
| 1656 |
+ 'may be specified multiple times' |
|
| 1657 |
+ ), |
|
| 1658 |
+ cls=CompatibilityOption, |
|
| 1659 |
+) |
|
| 1638 | 1660 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
| 1639 | 1661 |
@standard_logging_options |
| 1640 | 1662 |
@click.argument('service', required=False)
|
| ... | ... |
@@ -1662,6 +1684,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1662 | 1684 |
export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
| 1663 | 1685 |
import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
| 1664 | 1686 |
overwrite_config: bool = False, |
| 1687 |
+ unset_settings: Sequence[str] = (), |
|
| 1665 | 1688 |
) -> None: |
| 1666 | 1689 |
"""Derive a passphrase using the vault(1) derivation scheme. |
| 1667 | 1690 |
|
| ... | ... |
@@ -1758,6 +1781,10 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1758 | 1781 |
`--merge-existing` (False). Controls whether config saving |
| 1759 | 1782 |
and config importing overwrite existing configurations, or |
| 1760 | 1783 |
merge them section-wise instead. |
| 1784 |
+ unset_settings: |
|
| 1785 |
+ Command-line argument `--unset`. If given together with |
|
| 1786 |
+ `--config`, unsets the specified settings (in addition to |
|
| 1787 |
+ any other changes requested). |
|
| 1761 | 1788 |
|
| 1762 | 1789 |
""" # noqa: D301 |
| 1763 | 1790 |
logger = logging.getLogger(PROG_NAME) |
| ... | ... |
@@ -2189,13 +2216,20 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 2189 | 2216 |
'Setting a global passphrase is ineffective ' |
| 2190 | 2217 |
'because a key is also set.' |
| 2191 | 2218 |
) |
| 2192 |
- if not view.maps[0]: |
|
| 2219 |
+ if not view.maps[0] and not unset_settings: |
|
| 2193 | 2220 |
settings_type = 'service' if service else 'global' |
| 2194 | 2221 |
msg = ( |
| 2195 | 2222 |
f'Cannot update {settings_type} settings without '
|
| 2196 | 2223 |
f'actual settings' |
| 2197 | 2224 |
) |
| 2198 | 2225 |
raise click.UsageError(msg) |
| 2226 |
+ for setting in unset_settings: |
|
| 2227 |
+ if setting in view.maps[0]: |
|
| 2228 |
+ msg = ( |
|
| 2229 |
+ f'Attempted to unset and set --{setting} '
|
|
| 2230 |
+ f'at the same time.' |
|
| 2231 |
+ ) |
|
| 2232 |
+ raise click.UsageError(msg) |
|
| 2199 | 2233 |
subtree: dict[str, Any] = ( |
| 2200 | 2234 |
configuration['services'].setdefault(service, {}) # type: ignore[assignment]
|
| 2201 | 2235 |
if service |
| ... | ... |
@@ -2203,6 +2237,9 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 2203 | 2237 |
) |
| 2204 | 2238 |
if overwrite_config: |
| 2205 | 2239 |
subtree.clear() |
| 2240 |
+ else: |
|
| 2241 |
+ for setting in unset_settings: |
|
| 2242 |
+ subtree.pop(setting, None) |
|
| 2206 | 2243 |
subtree.update(view) |
| 2207 | 2244 |
assert _types.is_vault_config( |
| 2208 | 2245 |
configuration |
| ... | ... |
@@ -1304,6 +1304,32 @@ contents go here |
| 1304 | 1304 |
error=custom_error |
| 1305 | 1305 |
), 'expected error exit and known error message' |
| 1306 | 1306 |
|
| 1307 |
+ def test_225f_store_config_fail_unset_and_set_same_settings( |
|
| 1308 |
+ self, |
|
| 1309 |
+ monkeypatch: pytest.MonkeyPatch, |
|
| 1310 |
+ ) -> None: |
|
| 1311 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
| 1312 |
+ with tests.isolated_vault_config( |
|
| 1313 |
+ monkeypatch=monkeypatch, |
|
| 1314 |
+ runner=runner, |
|
| 1315 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}},
|
|
| 1316 |
+ ): |
|
| 1317 |
+ _result = runner.invoke( |
|
| 1318 |
+ cli.derivepassphrase_vault, |
|
| 1319 |
+ [ |
|
| 1320 |
+ '--config', |
|
| 1321 |
+ '--unset=length', |
|
| 1322 |
+ '--length=15', |
|
| 1323 |
+ '--', |
|
| 1324 |
+ DUMMY_SERVICE, |
|
| 1325 |
+ ], |
|
| 1326 |
+ catch_exceptions=False, |
|
| 1327 |
+ ) |
|
| 1328 |
+ result = tests.ReadableResult.parse(_result) |
|
| 1329 |
+ assert result.error_exit( |
|
| 1330 |
+ error='Attempted to unset and set --length at the same time.' |
|
| 1331 |
+ ), 'expected error exit and known error message' |
|
| 1332 |
+ |
|
| 1307 | 1333 |
def test_226_no_arguments(self, monkeypatch: pytest.MonkeyPatch) -> None: |
| 1308 | 1334 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 1309 | 1335 |
with tests.isolated_config( |
| ... | ... |
@@ -2752,17 +2778,27 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
| 2752 | 2778 |
target=configuration, |
| 2753 | 2779 |
config=configuration, |
| 2754 | 2780 |
setting=setting.filter(bool), |
| 2781 |
+ maybe_unset=strategies.sets( |
|
| 2782 |
+ strategies.sampled_from(_valid_properties), |
|
| 2783 |
+ max_size=3, |
|
| 2784 |
+ ), |
|
| 2755 | 2785 |
overwrite=strategies.booleans(), |
| 2756 | 2786 |
) |
| 2757 | 2787 |
def set_globals( |
| 2758 | 2788 |
self, |
| 2759 | 2789 |
config: _types.VaultConfig, |
| 2760 | 2790 |
setting: _types.VaultConfigGlobalSettings, |
| 2791 |
+ maybe_unset: set[str], |
|
| 2761 | 2792 |
overwrite: bool, |
| 2762 | 2793 |
) -> _types.VaultConfig: |
| 2763 | 2794 |
cli._save_config(config) |
| 2795 |
+ config_global = config.get('global', {})
|
|
| 2796 |
+ maybe_unset = set(maybe_unset) - setting.keys() |
|
| 2764 | 2797 |
if overwrite: |
| 2765 |
- config['global'] = {}
|
|
| 2798 |
+ config['global'] = config_global = {}
|
|
| 2799 |
+ elif maybe_unset: |
|
| 2800 |
+ for key in maybe_unset: |
|
| 2801 |
+ config_global.pop(key, None) # type: ignore[misc] |
|
| 2766 | 2802 |
config.setdefault('global', {}).update(setting)
|
| 2767 | 2803 |
assert _types.is_vault_config(config) |
| 2768 | 2804 |
# NOTE: This relies on settings_obj containing only the keys |
| ... | ... |
@@ -2774,6 +2810,7 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
| 2774 | 2810 |
'--config', |
| 2775 | 2811 |
'--overwrite-existing' if overwrite else '--merge-existing', |
| 2776 | 2812 |
] |
| 2813 |
+ + [f'--unset={key}' for key in maybe_unset]
|
|
| 2777 | 2814 |
+ [ |
| 2778 | 2815 |
f'--{key}={value}'
|
| 2779 | 2816 |
for key, value in setting.items() |
| ... | ... |
@@ -2791,6 +2828,10 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
| 2791 | 2828 |
config=configuration, |
| 2792 | 2829 |
service=strategies.sampled_from(_known_services), |
| 2793 | 2830 |
setting=setting.filter(bool), |
| 2831 |
+ maybe_unset=strategies.sets( |
|
| 2832 |
+ strategies.sampled_from(_valid_properties), |
|
| 2833 |
+ max_size=3, |
|
| 2834 |
+ ), |
|
| 2794 | 2835 |
overwrite=strategies.booleans(), |
| 2795 | 2836 |
) |
| 2796 | 2837 |
def set_service( |
| ... | ... |
@@ -2798,11 +2839,17 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
| 2798 | 2839 |
config: _types.VaultConfig, |
| 2799 | 2840 |
service: str, |
| 2800 | 2841 |
setting: _types.VaultConfigServicesSettings, |
| 2842 |
+ maybe_unset: set[str], |
|
| 2801 | 2843 |
overwrite: bool, |
| 2802 | 2844 |
) -> _types.VaultConfig: |
| 2803 | 2845 |
cli._save_config(config) |
| 2846 |
+ config_service = config['services'].get(service, {})
|
|
| 2847 |
+ maybe_unset = set(maybe_unset) - setting.keys() |
|
| 2804 | 2848 |
if overwrite: |
| 2805 |
- config['services'][service] = {}
|
|
| 2849 |
+ config['services'][service] = config_service = {}
|
|
| 2850 |
+ elif maybe_unset: |
|
| 2851 |
+ for key in maybe_unset: |
|
| 2852 |
+ config_service.pop(key, None) # type: ignore[misc] |
|
| 2806 | 2853 |
config['services'].setdefault(service, {}).update(setting)
|
| 2807 | 2854 |
assert _types.is_vault_config(config) |
| 2808 | 2855 |
# NOTE: This relies on settings_obj containing only the keys |
| ... | ... |
@@ -2814,6 +2861,7 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
| 2814 | 2861 |
'--config', |
| 2815 | 2862 |
'--overwrite-existing' if overwrite else '--merge-existing', |
| 2816 | 2863 |
] |
| 2864 |
+ + [f'--unset={key}' for key in maybe_unset]
|
|
| 2817 | 2865 |
+ [ |
| 2818 | 2866 |
f'--{key}={value}'
|
| 2819 | 2867 |
for key, value in setting.items() |
| 2820 | 2868 |