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 |